Webpack 备忘录

Webpack 属于在项目中配置一次就很少改动的那种工具,但这样就导致新项目再配置 Webpack 时会有些生疏,所以将 Webpack 核心概念及常用配置记录如下。

Webpack 4.x 之前的核心概念有四个:entry,output,loaders,plugins,4.x 之后增加了 mode。含义如下:

  • Entry:指定 webpack 从哪个 file 开始构建他的依赖关系图,可以有一个和多个,推荐对象表示法;
  • Output:webpack 构建完成后的 bundles 输出位置及 name,其中 name 一般根据 entry 的 file name 动态生成;
  • Loaders:webpack 可以将非 js 文件(css、图像等)通过 loaders 处理成可供我们的应用使用的 modules。也因此在配置文件中,loaders 写在module对象下面,因为 loaders 处理后返回的是有效 modules。
  • Plugins:如果需要额外的功能,比如压缩代码、提取 css、分析打包性能,可使用插件完成;

    Loaders 将特定文件转换为有效 module,而 plugin 扩展性更强

  • Mode:接受developmentproductionnone三个值,一般指定为前两个值的一种,webpack 内部针对不同环境做优化。

2)配置

下面介绍的配置都是module.exports的直接属性,比如:

module.exports = {
entry: {},
output: {},
// loaders 配置位置有些特殊,不知直接写 loaders 对象,而是将它鞋子啊 module 对象下面
// 因为 loaders 处理完后返回的是有效 modules
// 不是 复数!!!
module: {},
// plugin 是数组
// 是 复数!!!
plugins: []
};

同时,文件头引入相应依赖的过程省略了,实际开发中请自行引入。

2.1 Entry

Entry 指定哪些文件作为 webpack 打包入口,可以有一个或者多个:

entry: {
index: './src/js/view/index.js',
people: './src/js/view/people.js',
login: './src/js/view/login.js',
}

2.2 Output

Output 指明 Webpack 处理完成后的 bundles 输入位置及文件 name:

output: {
// 输入目录,这里使用了 node 的 path 模块
path: path.resolve(__dirname, 'dist'),
// [] 里面的内容表示变量
filename: 'js/[name].[contenthash:8].js',
// 静态资源在 html 引用的公共目录
publicPath: '/',
}

Output 配置相对复杂:

2.2.1 output.path

即输出文件目录,推荐使用绝对路径,其中__dirname在 Node 的 path 模块中表示当前目录(即 webpack.config.js 的目录)的绝对路径,resolve方法可以拼接两个目录;

后一个目录可以加/也可以不加,此处无差别

2.2.2 output.filename

表示输出文件名称,这里注意,[]中是变量,其中name由在 entry 中定义的文件名决定,并且,有多少个输入文件,对应有多少个输出文件,后面的contenthash:8表示 chunk 的 hash 值,后文介绍;

实际输出文件一般多于输入文件,因为提取公共代码、设置 sourcemap 都会单独生成输出文件

2.2.3 output.publickPath

这个配置想最为复杂,它的作用是配合 loaders 或 plugins 中设置的资源路径,指定静态文件(css、js、img)插入 html 中的引用路径,简单讲,就是 对输出的静态资源进行目录管理。这里直接使用 详解 Webpack2 的那些路径的描述,只稍作说明。

静态资源最终引用路径计算公式:

html 静态资源路径 = output.publicPath + loaders/plugins 中设置的资源路径

比如:

// publicPath 设置
output.publicPath = '/static/'
// JS output.filename 配置
{
filename: 'js/[name].js'
}
// JS 最终访问路径为
output.publicPath + 'js/[name].js' = '/static/js/[name].js'
// 图片资源, file-loader,这里省略了 file-loader 其他配置,只列出指定输出路径及文件名的配置
{
name: 'img/[name].[ext]'
}
// 图片最终的访问路径为
output.publicPath + 'img/[name].[ext]' = '/static/img/[name].[ext]'
// CSS,ExtractTextPlugin 为提取 CSS 的插件
new ExtractTextPlugin("css/style.css")
// CSS 最终访问路径为
output.publicPath + 'css/style.css' = '/static/css/style.css'

我们上述指定publicPath为绝对路径,实际上,也支持相对路径,相对于index.html,用的比较少。

publicPath默认为空字符串,但为了使输出目录更加条理,推荐使用publicPath对静态资源进行目录管理。

2.3 Loaders

Loaders 的配置写在module对象中,如前所述,因为 loaders 最终返回的是有效 modules,故使用了module命名,注意是单数。这节介绍 loaders 配置及常用 loaders 两部分内容。

2.3.1 loaders 配置

loaders 规则写在 module.rules 里面(不知道为什么不直接写在 module 中),其中 rules 是个数组,可接受一个或多个 loader 配置。两点需要注意:

1)如果某类型需要多个 loader 进行处理,在 use 中按 从右往左 的顺序流式处理;

2)每个 loader 可以进行额外配置。

module: {
// 含有 rules,这一个属性
// rules 是数组,每个元素对应一个 loader 配置
// loader 配置本身是个对象
rules: [
{
// 正则匹配文件,最终匹配的是一个路径,具体间解析 zepto 源文件时的设置
test: /\.scss$/,
// 使用的 loader,可以字符串(单个 loader)、数组(多个 loader)
// 如果是数组,从右往左开始解析
use: ['style-loader', 'css-loader', 'scss-loader'],
},
{
test: /\.(png|jpg|jpeg|svg|gif)$/,
// 每个 loader 可进行额外配置
use: [
{
loader: 'file-loader',
options: {
name: 'img/[name].[ext]',
},
},
],
},
],
},

2.3.2 常用 loader

  • babel-loader:es6+语法转换;
  • html-loader:解析 html 文件;
  • css-loader:解析 css 文件;
  • sass-loader:解析 scss/sass 文件;
  • style-loader:将解析后的 css 嵌入 js;
  • file-loader:解析图片文件;
  • url-loader:具有 file-loader 的全部功能,同时可以提取小图片为 base64(如果开启 HTTP2 这样增大 静态资源体积反而不好?);
  • postcss-loader:完成 css 自动化处理,比如添加前缀、压缩 css、自动生成雪碧图等

    postcss 本身支持插件扩展,常用的有 autoprefixer、cssnano、postcss-sprites,更多参考官网介绍

    要在 css-loader 之前处理 css:use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']

Vue 相关

  • vue-loader:解析 vue 文件;
  • vue-style-loader:解析 vue 中的样式文件。

2.4 Plugins

同 loaders 一样,分配置和常用 plugins 两部分

2.4.1 plugins 配置

Plugins 使用相对简单,配置项写在plugins(复数)数组中,元素为插件的实例(通过new调用),生成实例的时候可接收参数,具体看相应插件文档。见下面代码及注释:

plugins: [
// 带有参数
new CleanWebpackPlugin(['dist']),
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFileName: '[id].[contenthash:8].css',
}),
// 直接调用
new UglifyJsPlugin(),
],

2.4.2 常用 plugins

  • html-webpack-plugin:将构建后的静态文件动态插入 html 中;
  • webpack-merge:实际项目中,一般将 webpack 配置文件拆分为 base、dev、pro,这个插件用户合并配置文件;
  • webpack.HotModuleReplacementPlugin:热更新插件,webpack 内置;
  • clean-webpack-plugin:清除指定文件夹,一般是构建的目录(大型项目慎用,更新需要时间);
  • mini-css-extract-plugin:提取 css 文件,减小增量更新成本(需要在module代理 style-loader 处理 css);

    style-loader 先把 css 嵌入 js 使其变成有效 module,这个插件将 css 从 js 中分离出来。这个过程不矛盾,因为最开始 webpack 无法处理 css 文件,所以需要 css-loader, style-loader 处理,嵌入到 js 中的 css mini-css-extract-plugin 可以识别并提出。这个插件是 webpack 4.x 新引入,代替 extract-text-webpack-plugin。

  • optimize-css-assets-webpack-plugin:将提取出的 css 做进一步优化;
  • uglifyjs-webpack-plugin:不仅仅是压缩代码,还进行了 tree shaking 工作
  • webpack-bundle-analyzer:打包后文件图形化展示工具,一目了然各文件体积;
  • ProvidePlugin:webpack 内置,提取第三方库的 api,比如通过 $ 符号调用 jq

    如果不是通过 npm 安装,而是直接在项目中引入,需要配合resolve的别名使用,不然找不到

2.5 Mode

接受developmentproductionnone三个值,一般取前两个值之一,指定构建环境,webpack 本身会做相应优化(配合optimization使用,下文会有更多介绍),同时省去在命令行中指定构建环境的过程。

// 相当于 webpack --mode=production
mode: 'production',

2.6 其他

上述配置对应 Webpack 核心概念,除此之外,还有下面配置项很常用。

2.6.1 optimization

这个选项是在 Webpack 4.x 中引入的,最常用的配置是代替CommonsChunkPlugin提取公共代码。下面直介绍提取公共 JS 代码的配置:

optimization: {
splitChunks: {
// 只对入口文件处理
chunks: 'initial',
cacheGroups: {
vendor: {
// split `src/js/vendor` 目录下共用的代码到`vendor.js`
// 这里指的是第三方库,很少改动,故单独做持久化缓存
// 注意,目前 splitChunks 在 split css 时有 bug?现在只 split js 代码
// 不推荐直接匹配 node_modules 文件夹,如上,因为涉及 css,打包后 css 错误
test: /src\/js\/vendor\//,
name: 'vendor',
// 只要第三方库,只要 > 0B, 就 split 出来
minSize: 0,
// 只要被引用一次,也要分离出来
minChunks: 1,
// priority: 10,
// enforce: true,
},
common: {
// split `src/js/common` 目录下共用的代码到`common.js`
// 这里是自定义的公共 js 代码,改的频率比第三方库要高,但比具体页面的 js 文件低
// 故单独 split 出来做持久化缓存
// 不一定共用才 split ,很少改动的也 split 出来,如上的 vender.js
// 但此处至少两个共用 minChunks 才抽出
test: /src\/js\/common\//,
name: 'common',
minChunks: 2,
minSize: 0,
// priority: 9,
// enforce: true,
},
},
},
},

2.6.2 devtool

devtool最常用功能是配置 sourcemap,生产和开发环境一般使用不同的 sourcemap,配置如下:

// 生产环境
devtool: 'source-map',
// 开发环境
devtool: 'cheap-module-eval-source-map',

关于各配置项的含义可参考Devtool和这片文章Webpack 的 devtool 和 source maps

sourcemap 是构建后的代码 map 到源代码的映射表,便于定位 bug,具体的映射关系存储在构建后的.map文件中。

NOTE: 这里我们在生产环境使用的是 source-map 选项而不是其他,是因为目前 webpack 在开启 uglifyjs-webpack-plugin 优化代码后,其他模式下的 sourcemap 选项无效,官网对此也有说明。虽然使用source-map选项生成的映射表比较大,但只有开启开发者工具的时候.map文件才会加载,这意味着 映射表文件的大小不影响正常用户的访问体验,但是否压缩 js 文件对正常用户有直接影响,故,现阶段,生产环境 devtool 使用 source-map 选项。

2.6.3 devServer

开发环境为了更好的开发体验,可以开启热更新等功能,在devServer中配置:

devServer: {
open: true,
hot: true,
},

需要在插件中进行下面配置:

plugins: [
new webpack.HotModuleReplacementPlugin(),
],

NOTE:如果传统多页面项目,在入口文件后面添加下面代码,配合热更新:

// 配合 webpack 配置实现热更新
if (module.hot) {
module.hot.accept();
}

多页面目前只有在 JS 或者 CSS 文件改变的时候实现了热更新,如果是模版(html)文件改变,没有实现热更新(可以实现自动刷新页面,但感觉很鸡肋,如果改动了模版文件,手动刷新)。

devServer 还可以设置反向代理,后续填坑。

2.6.4 resolve

resolve 选项可以指定如何解析 modules,更多是通过设置 alias 告诉 webpack 去哪找文件解析。比如,如果在项目中通过文件的形式引入的 jq,那在使用 ProvidePlugin 对 jq 进行解析的时候,就需要通过设置别名的形式告诉 webpack 去哪找 jq 源文件。

resolve: {
alias: {
// 后面加 $ 符号表示精确匹配
jquery$: path.resolve(__dirname, 'path/jquery.min.js');
}
}
// 解析插件
new webpack.ProvidePlugin({
// 加载 jquery
$: 'jquery',
}),

3)常见需求详细配置

下面记录针对具体需求的完整代码描述。

3.1 引入第三方库

比如,jq,zepto 等,如果通过 CDN 可以在项目中直接使用 $ 符号,但如果是通过 npm 安装到本地,甚至直接将第三方库源文件写在项目中,那是无法直接使用 $ 符号这种调用方式的,需要使用上文介绍的 ProvidePlugin 插件。当然,还有细节需要注意,见下面详细代码。

3.1.1 使用 npm 引入

相对于直接引入,使用 npm 可以省去我们手动指定 module 路径的麻烦,已 zepto 为例:

使用 zepto 时,直接使用 ProvidePlugin 会报错,具体参考这片文章如何在 webpack 中引入未模块化的库,如 Zepto

// 处理 zepto 模块化问题,需要安装 exports-loader 和 script-loader
{
test: require.resolve('zepto'),
use: ['exports-loader?window.Zepto', 'script-loader'],
},
// 给 zepto 设置别名,可以任何名称
plugins: [
new webpack.ProvidePlugin({
$: 'zepto',
}),
],

3.1.2 直接在项目中引入源文件

同 npm 不同之处在于我们需要手动指定 zepto 的路径,如下

// 设置别名
resolve: {
alias: {
zepto$: path.resolve(__dirname, 'src/js/vendor/zepto.min.js'),
},
},
// 处理 zepto 模块化问题
{
// test 路径也要跟着改变,不知道为什么不能用上面指定的别名。。
test: path.resolve(__dirname, 'src/js/vendor/zepto.min.js'),
use: ['exports-loader?window.Zepto', 'script-loader'],
},
// 给 zepto 设置别名,可以任何名称
plugins: [
new webpack.ProvidePlugin({
$: 'zepto',
}),
],

3.2 提取 css

为了能将 css 从 js 中提取出来,需要在 module 中将 style-loader 替换为 MiniCssExtractPlugin.loader,见下面代码:

module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
{
test: /\.scss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
// 这个干嘛用?
chunkFileName: '[id].[contenthash:8].css',
}),
],

3.3 缓存控制

主要一下几个方面:

  • 提取 css,添加 contenthash(使用 mini-css-extract-plugin);
  • 提取公共代码(工具函数等,更改频率相比业务代码要小),添加 contenthash,使用 webpack 自带 splitChunks 提取;
  • 提取第三方库代码(比如 jquery,更改频率相比公共代码还要小),添加 contenthash,使用 webpack 自带 splitChunks 提取;
  • 图片可以直接打 hash(图片文件添加 hash 并不一样,也不会随每次构建改变,还不知原理,反正可以工作,待填坑)。