简介
webpack是一个用于现代JavaScript应用程序的静态模块打包工具。用于前端代码的工程化。webpack处理应用程序时,它会在内部构建一个 依赖图(dependency graph),此依赖图对应映射到项目所需的每个模块,并生成一个或多个bundle。webpack + loader可以支持多种语言和预处理器语法编写的模块。loader向webpack描述了如何处理非原生模块,并将相关依赖引入到你的 bundles中。webpack还支持可自由扩展的插件(plugin)体系。这是webpack的 支柱 功能。插件目的在于解决loader无法实现的其他功能。webpack天生支持如下模块类型:ECMAScriptCommonJSAMDAssets``WebAssembly.
接下来,我们以一个 React + SASS 的简单web项目,来学习webpack的基础配置。
以下配置都是基于
webpack 4.30.0。
基础配置
首先,我们要安装webpack系列npm包 (这些 loader 和 plugins,后面会逐步介绍):
// package.json
{
"devDependencies": {
/** webpack **/
"webpack": "4.30.0",
"webpack-cli": "3.3.12",
"webpack-dev-server": "^3.7.2", // 本地调试用
/** loader **/
"css-loader": "2.1.1",
"style-loader": "^2.0.0",
"file-loader": "^6.2.0",
"node-sass": "^4.0.0",
"sass-loader": "^7.1.0",
"@babel/preset-env": "7.4.3",
"@babel/preset-react": "7.0.0",
/** plugins **/
"html-webpack-plugin": "4.0.0",
"mini-css-extract-plugin": "^0.6.0",
"webpack-bundle-analyzer": "^4.4.0"
}
}
webpack打包,需要做一系列的配置,而且 本地调试 和 生产打包 的配置是有一些差异的。一般情况下,我们在项目的根目录下设置两个配置文件:
|
|- webpack.config.js # 生产打包
|- webpack.config.dev.js # 本地调试
// package.json
{
//...
"scripts": {
"start": "webpack-dev-server --config webpack.config.dev.js",
"build": "webpack-cli"
}
//...
}
webpack配置文件可以直接返回一个json对象,或者一个返回json对象的函数。
// webpack.config.js
export default {
//...
}
//or
export default () => {
return {
//...
}
}
接下来,我们开始具体配置:
entry
web项目中,我们一直有主JS,或者入口JS的概念。webpack要打包,构建依赖图谱。肯定也需要一个入口文件。webpack 通过 entry 来配置一个(或多个)入口。默认值是 ./src/index.js。
const path = require('path')
module.exports = {
entry: './src/pages/app.js'
}
或者对象,可以标示入口文件name(一般用于多个)
module.exports = {
entry: {
app: './src/pages/app.js'
}
}
output
有打包输入(entry),就有打包输出。webpack通过 output 来配置输出文件信息。默认值是 ./dist/main.js,其他生成文件默认放置在 ./dist 文件夹中。
module.exports = {
output: {
path: path.resolve(__dirname, 'build'), // 指定输出目录
filename: '[name].js', // 指定输出文件名称; [name] 动态使用entry指定的名称,对应上面是:app
}
}
高阶配置
实际项目中,我们静态资源会使用CDN,或者通过其他特有目录引入。
这个时候,我们需要通过 publicPath来配置.
编译时指定:
module.exports = {
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].js',
publicPath: '/my-project/static/resource/',
}
}
运行时指定:
module.exports = {
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].js',
publicPath: './', // 可随意填写,运行时会覆盖为 __webpack_public_path__
}
}
// 重点在这里:
__webpack_public_path__ = window.global_info.staticCDN;
// 如果项目在一个Java/Node容器,staticCDN 的值一般是服务端路由中读取环境变量赋予。
// 如果项目是纯前端,staticCDN的值一般是通过 域名+规则 判断确定。
Loaders
- 面向现代
JS,我们大都不是通过直接编写原生JS来开发项目,而是运用各种框架(React,Vue……),各种JS+语言(TS,JSX……),ES5+语法糖等。 - 但是我们的浏览器只识别原生
JS,虽然主流浏览器也开始兼容绝大多数ES5+语法,但是我们也得考虑兼容性问题。 - 所以,
webpack提供了loaders机制,我们可以通过使用loader用于对模块的源代码进行转换。 - 不同的模块类型,应用不同的
loaders。 loaders是通过module对象下的rules数组来配置。
简单示例:
module.exports = {
module: {
rules: [
{ test: /\.css$/, use: 'css-loader' }, // 对每个 .css 使用 css-loader
{ test: /\.ts$/, use: 'ts-loader' } // 对所有 .ts 文件使用 ts-loader
]
}
};
test:RegExp,用来匹配需要处理的文件类型。use:String | Object | Array,指定具体的loader。上述示例,使用 单个默认配置的loader,可以使用String类型指定即可。
** loader 可以通过 options 对象配置更多参数。带参数配置的单个loader示例:**
module.exports = {
module: {
rules: [
// 图片资源处理
{
test: /\.(png|jpg|jpeg|gif)$/,
use: [
{
loader: require.resolve('file-loader'),
options: { // loader参数配置
esModule: false,
name: './image/[name].[hash:8].[ext]'
}
}
]
}
]
}
}
负责图片处理的 loader 有两种, 一般一起使用:
file-loader可以把js和css中导入的图片替换成正确的地址,并把图片文件输出到对应的位置。文件名是根据文件内容计算出的hash值。url-loader可以把文件通过base64编码后注入到JS或者css中去。图片的数据量太大,会导致JS和CSS文件变大,一般利用url-loader把页面需要的小图片注入到代码中去,以减少加载次数。url-loader通过limit参数来控制,小于limit的图片才会处理。
多个 loader 配置,将按照相反的顺序执行。以支持 scss 为例:
module.exports = {
module: {
rules: [
// 处理sass样式文件
{
test: /\.(css|scss)$/,
// 使用多个 loaders;都使用默认配置;
use: ["style-loader", "css-loader","sass-loader"]
}
]
}
}
sass-loader负责把Scss源码转换为CSS代码,再交给css-loader处理。css-loader会找出@import和URL()这样的导入语句,告诉webpack依赖这些资源。同时还支持css modules,压缩css等功能。style-loader会把css代码转换成字符串,然后注入到JS代码中。通过JS给DOM增加样式。也可以通过plugin(MiniCssExtractPlugin)把css提取到单独的文件中。
插件 (plugin) 可以为 loader 带来更多特性:
module.exports = {
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /(node_modules)/, // 正则表达式,设置该loader需要忽略/排除的目录/文件
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env','@babel/preset-react'],
cacheDirectory: true,
plugins: ['@babel/plugin-transform-runtime','@babel/plugin-proposal-class-properties']
}
}
}
]
}
}
@babel/runtime和@babel/plugin-transform-runtime:babel编译时只转换语法,几乎可以编译所有时新的JavaScript语法,但并不会转化BOM(浏览器)里面不兼容的API。比如Promise,Set,Symbol,Array.from,async等等的一些API。这2个包就是来搞定这些api的。@babel/plugin-proposal-class-properties:用来解析类的属性的。在
babel执行编译的过程中,会从项目的根目录下的.balelrc文件中读取配置。.balelrc是一个json格式的文件,当babel loader配置项很多的时候可以使用。
由此可见, plugins 可以在 loader 配置中配合使用。当然,也可以单独配置使用。下面,我们来详解了解一下plugins。
Plugins
功能: 用于解决
loader无法实现的其他事,进行功能扩展。原理:
webpack plugins是一个具有apply方法的JavaScript对象。apply方法会被webpack compiler调用,并且在整个编译生命周期都可以访问compiler对象。你可以基于次编写自定义插件。用法: 可以通过在
webpack.config.js增加Plugins配置使用。也可以通过NodeAPI的形式使用。
下面,我们通过几个常用的 plugin 来了解如何配置使用:
// 该插件将为你生成一个 HTML5 文件, 在 body 中使用 script 标签引入你所有 webpack 生成的 bundle。
const HtmlWebpackPlugin = require('html-webpack-plugin')
// 一般,plugins都是通过 new 一个自身的实例来使用
// 支持option对象参数配置
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './temp.html', // 可以自定义用于生成Html的模版文件,非必填
filename: 'home.html' // 指定html文件名称;默认为 index.html
})
]
}
** 使用默认配置生成的 html 文件如下:**
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>webpack App</title>
</head>
<body>
<script src="index_bundle.js"></script>
</body>
</html>
多个
entry,对应多个script标签。
前面有提到,有的 plugins 可作为有些 loader 的 option 配置项使用。
还有一些插件需要在Plugins和Loader中都需要增加配置:
// 本插件会将 CSS 提取到单独的文件中
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
module: {
rules: [
{
test: /\.(css|scss)$/,
use: [MiniCssExtractPlugin.loader, "css-loader","sass-loader"]
},
]
},
plugins: [
new MiniCssExtractPlugin({ filename: '[name].css'})
]
}
使用
MiniCssExtractPlugin生成的css文件会在生成的Html文件中通过<link>引入。所以不再需要使用style-loader。style-loader: 将模块导出的内容作为样式并添加到DOM中。
mode
productionordevelopment.用来指定是生产模式还是开发模式。- 在代码中可以通过
process.env.NODE_ENV获取,用来做两种模式的兼容处理。
到此,我们这个简单项目的完整webpack.config.js配置如下:
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = () => {
return {
mode: 'production',
entry: {
app: path.resolve(__dirname, 'src/pages/app.js'),
},
output: {
path: path.resolve(__dirname, 'build'),
publicPath: '/my-project/static/resource/',
filename: '[name].js',
},
resolve: {
extensions: ['.js', '.jsx', '.ts'],
},
module: {
rules: [
{
test: /\.(css|scss)$/,
use: [MiniCssExtractPlugin.loader, "css-loader","sass-loader"]
},
{
test: /\.(js|jsx)$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env','@babel/preset-react'],
cacheDirectory: true,
plugins: ['@babel/plugin-transform-runtime','@babel/plugin-proposal-class-properties']
}
}
},
{
test: /\.(png|jpg|jpeg|gif)$/,
use: [
{
loader: require.resolve('file-loader'),
options: {
esModule: false,
name: './image/[name].[hash:8].[ext]'
}
}
]
}
]
},
plugins: [
new HtmlWebpackPlugin({template: './temp.html', filename: 'home.html'}),
new MiniCssExtractPlugin({ filename: '[name].css'})
],
optimization: {
splitChunks: {
cacheGroups: {
default: {
name: 'common',
chunks: 'initial'
}
}
}
}
}
}
resolve.extensions: 如果文件引入的时候没有后缀名,将自动按配置的文件名匹配查找。optimization: 用于提取公共js配置。
开发模式
我们在本地调试的时候,webpack 配置和生产会有一些差异。比如:
- 生产可能配置特殊的
publicPath,本地则不能配置。 - 本地调试专用的
devServer配置。 - 开发模式专用的一些
plugins。
我们可以设置单独的配置文件(如 webpack.config.dev.js):
// 下面只展示差异点配置
// new plugin 用来做打包后文件的成分分析,以做某些优化。
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = () => {
return {
mode: 'development', // 指定为开发环境
// 下无 publicPath配置
output: {
path: path.resolve(__dirname, 'pcDist'),
filename: '[name].js',
},
// 专用plugin
plugins: [
new BundleAnalyzerPlugin()
],
// 本地调试专用配置
devServer: {
contentBase: path.join(__dirname, 'pcDist'),
port: 8009,
hot:true,
open: true
}
}
}
DevServer 其实是一个方便开发的小型http服务器,是基于webpack-dev-middleware 和 Express 实现的。webpack-dev-middleware 会导出一个函数。该函数接收一个 webpack 的 Compiler 实例作为参数,导出一个 Express 中间件。该中间件具有以下功能:
- 接收
Compiler实例输出的文件,但不会存在硬盘,而是放入内存。 - 往
Expressapp上注册路由,拦截http请求,根据请求路径响应对应的文件。
明显,开发和生产配置大部分还是通用。所以在实际项目中我们使用一个通用配置文件,然后在自有的cli工具库中去抽象,处理,复用。
module,chunk,bundle理解
moule是模块,webpack中一切皆模块,所以module就是我们编写的一个个文件。chunk是指webpack根据文件引用关系生成的chunk文件。一般来说,一个entry对应一个chunk文件。bundle是指webpack最终生成的浏览器可以直接运行的bundle文件。一般来说,一个chunk对应一个bundle文件。但是也有例外,比如我们使用MiniCssExtractPlugin插件会从一个chunk中抽取出单独的css bundle文件。
webpack 异步加载
在使用 webpack 打包的应用中,我们可以使用 require.ensure 进行异步加载,也有人称为代码切割。他其实就是将指定的 js 模块独立导出一个.js 文件,然后使用这个模块的时候,再创建一个 script 对象,加入到 document.head 对象中,浏览器会自动帮我们发起请求,去请求这个 js 文件,然后写个回调函数,让请求到的 js 文件做一些业务操作。
require.ensure 这个函数是一个代码分离的分割线,表示回调里面的 require 是我们想要进行分割出去的,webpack 会打包成单独 js 文件。它的语法如下:
// 语法如下:
`require.ensure(dependencies: String[], callback: function(require), chunkName: String)`
按需加载
webpack4 官方文档提供了模块按需切割加载,配合 es6 的按需加载 import() 方法,可以做到减少首页包体积,加快首页的请求速度,只有其他模块,只有当需要的时候才会加载对应 js。
import()的语法十分简单。该函数只接受一个参数,就是引用包的地址,并且使用了 promise 式的回调,获取加载的包。在代码中所有被 import()的模块,都将打成一个单独的包,放在 chunk 存储的目录下。在浏览器运行到这一行代码时,就会自动请求这个资源,实现异步加载。
常用的优化手段
优化
loader配置: 通过include等配置,不需要处理的文件尽量不处理。异步按需加载:(上面有专门提到)。
分包:提取公共的一些模块 + 缓存提高性能。
SplitChunksPlugin: 提取公共模块,问题是业务模块依赖改变也会影响公共包,哈希会改变,缓存会失效。Dllplugin & DllReferencePlugin: 将指定的公共模块打包成动态链接库形式的js文件。其他模块引用到这些指定的公共模块会直接在公共js文件中加载,不会打包在业务模块中。问题是这些动态链接库文件要独立先行打包,并提前引入。
Tree Shaking: webpack依赖静态的 ES6 模块化语法,分析出都要哪些功能被用到了,然后剔除没有的代码。可以在启动Webpack时带上--optimize-minimize参数,快速接入Tree Shaking;也可以使用UglifyJSPlugin来处理。Scope Hoisting(作用域提升): 分析出模块之间的依赖关系,尽可能的把打散的模块合并到一个函数中去,但前提是不能造成代码冗余。因此只有那些被引用了一次的模块才能被合并。- 使用内置的
ModuleConcatenationPlugin()即可开启。
- 使用内置的