一篇文章带你掌握 Webpack 中常用 loader 和 plugin 的作用

883 阅读7分钟

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

平常我们开发时很少摆弄 webpack 配置,都是大佬给我们配好了,我们吭呲吭呲写业务代码就好了,对很多常用的 loader 和 plugin 大概知道它是干嘛的,但实际上底下做了什么事情却像隔了层雾。

所以我就写了这篇文章,希望你看完这篇文章后能拨开这层雾,对常用的 loader 和 plugin 的作用有所了解。

常用 loader

webpack 的 loader 是用来对单个文件进行处理的。或者可以叫做 转换器,它对匹配的文件应用对应的多个 loader 按顺序处理,最终返回一个 JS 内容的字符串,让 webpack 可以实现模块化。核心要点为:

  • loader 最后都是返回一个 JS 内容的字符串。

  • loader 其实是转换器,对源文件进行处理。

  • 配置中多个串联的 loader 的执行顺序是从后往前。

babel-loader

babel-loader 可以说是 Webpack 搭建项目的标配了,因为它实现了一个核心的兼容功能:将新版本的 ES 语法转换为能在绝大多数浏览器上运行的 ES5 语法。

{
  module: {
    rules: [
      {
        test: /\.(js|jsx|ts|tsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
      // ...
    ]
  }
}

因为 babel 的配置比较复杂,所以我们通常不在 webpack 上进行配置,而是使用 babel 专用的配置文件。参考

  • babel.config.js。全局配置(项目级,执行目录下)
  • .babelrc。局部配置(目录级别),会与项目级的 babel.config.js 进行合并。如果当前目录没有 .babelrc,向上找一个最近的。如果这个文件在项目根目录外,不会应用该文件同级目录下的 .babelrc。

此外 babel.config.js 是 JS 文件,可以引入模块;而 .babelrc 则是 JSON 文件。

我们通常需要在 babel 的配置文件上加上一些预设集(preset),所谓的预设集就是将一些 babel 常见的转换进行打包。这里不展开讲

css-loader

css-loader 将 CSS 文件编译成一个对象,记录了一些必要的信息。这个对象不能直接用,需要配合 style-loader,实现样式的挂载。

// index.css
.red {
  background-color: #f04;
}

上面的样式会转换为下面这样子的对象。

image.png

css-loader 的一个重要功能是支持 CSS 模块化(CSS-module) 的实现,是组件化开发的做 CSS 组件化的一个流行方案,配置大概如下。

options: {
  modules: {
    localIdentName: '[name]__[local]__[hash:base64:5]',
  },
},

style-loader

style-loader 处理 css-loader 返回的对象,将 CSS 内容放到 style 标签下,再放到 DOM 树下(默认是放到 <head> 下,可使通过 options.insert 修改挂载位置)。注意这里没有产生 CSS 文件,而是将 CSS 文件的内容以字符串的形式保存在脚本中。

实际生产环境中,我们还是需要提取出 CSS 文件的。对此我们改为使用 MiniCssExtractPlugin.loader。

MiniCssExtractPlugin.loader

mini-css-extract-plugin 插件的作用是提取 CSS 内容为单独的文件。默认是将所有的 CSS 内容都打包到一个 CSS 文件里。mini-css-extract-plugin 本身是个插件,但它自身还提供了 loader。使用 loader 的时候必须同时使用对应的 plugin,否则会报错。

一般来说我们会根据构建环境,确定是使用 style-loader 还是 MiniCssExtractPlugin.loader:

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = (env) => ({
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          env.production ? MiniCssExtractPlugin.loader : 'style-loader',
          'css-loader',
          // ...
        ]
      }
    ]
  },
  // ...
  plugins: [
    new MiniCssExtractPlugin(),
  ]
})

如果你使用了 html-webpack-plugin 插件,它能识别出 mini-css-extract-plugin 生成的 CSS 文件名,以 link 的方式嵌入到生成的 html 下。html-webpack-plugin 说到:

If you have any CSS assets in webpack's output (for example, CSS extracted with the mini-css-extract-plugin) then these will be included with <link> tags in the HTML head.

postcss-loader

postcss-loader 是用于处理样式的一个工具,支持各种好用的插件。通常我们会单独使用一个 postcss.config.js 来进行配置(当然也可以直接在 webpack 配置上进行配置)。

它的强大在于它能够使用许多好用的插件,如:

  • autoprefixer:给选择器或样式规则添加浏览器专属的前缀,处理浏览器兼容问题。如 ::placeholder {} 会转换为各种浏览器专属选择器,有 ::-webkit-input-placeholder {} input::-ms-input-placeholder {} 等。还比如 transform 属性。autoprefixer 几乎是 postcss 的标配插件,因为它处理了 CSS 兼容问题。
  • postcss-pxtorem:将 css 里的 px 转换为 rem 单位,这在开发基于 rem 适配的 H5 页面需要用到。

less-loader / sass-loader 等

通常我们都不会直接写 CSS,而是会使用 less、sass 之类的预处理器,主要是要用到 CSS 所没有的 选择器嵌套语法,该语法极高地提高了我们编写样式的效率。CSS 预处理器几乎是所有项目的标配。

根据项目不同,我们会选择使用不同的 CSS 预处理器,对此我们需要安装不同的预处理器 loader。

假设我们使用 less-loader,那我们除了要安装 less-loader,还要安装 less,否则在导入 less 文件时就会报错。其他预处理器 loader 也同样道理。配置大概这个样子

{
  test: /\.less$/,
  use: [
    env.production ? MiniCssExtractPlugin.loader : 'style-loader',
    'css-loader',
    'postcss-loader',
    'less-loader'
  ]
},

需要注意的是 less-loader 必须放到 postcss-loader 的后方,因为 loader 的执行顺序是从右往左的, postcss-loader 无法处理 less 的语法,比如 //,需要先通过 less-loader 转换为 CSS 后,postcss-loader 才能够理解和处理。

file-loader

file-loader 的作用是将 JS 文件中导入的资源使用哈希重命名(默认情况),放到构建目录。然后脚本中可以拿到这个新的资源名,放到要用到的地方,比如 img.url。配置写法大概是这样的:

{
  test: /\.(png|jpe?g|gif)$/,
  use: 'file-loader'
}

不过 webpack 5 后这个 loader 做了内置(不需要额外安装 file-loader),并换了个概念:asset module。在 webpack 5 中,我们可以这样写:

{
  test: /\.(png|jpe?g|gif)$/,
  type: 'asset/resource'
}

url-loader

url-loader 会将引入的资源转换为 base64 URI。原资源并不会被复制到构建目录,它成为了很长很长的字符串编码。base64 会将 3 个字节转换为 4 个字节,从而导致资源变大,尤其不适用于大文件。对于小的资源损耗较低,但却能减少一个 HTTP 请求。base64 一般用于优化网络请求,减少小资源请求数。

所以通常我们会给一个阀值(通常为 8194 字节,大概 8K 的样子)。文件小于指定大小时使用 base64(url-loader),否则只是拷贝资源(file-loader)。

{
  test: /\.(png|jpe?g|gif)$/,
  use: [
    {
      loader: 'url-loader',
      options: {
        limit: 8194, // 单位为字节
      }
    } 
    // 这里你不需要写 file-loader,但你得安装了。
    // 否则文件太大时会报错为:Cannot find module 'file-loader'
  ]
}

使用上面这种写法,如果资源大于或等于 8194 字节,url-loader 就会自动地找到 file-loader 去处理(可以使用 options.fallback 来指定不符合条件时使用的 loader)。

同样,webpack 5 也进行了内置该 loader,名为 asset/inline。写法为 type: asset/inlinetype: 'asset', parser: {dataUrlCondition: {maxSize: 8 * 1024}}。后者是加了阀值的写法。

raw-loader

raw-loader 负责提取文件的内容,转换为字符串形式。一般用于提取文本文件,如 txtjson。webpack 5 将其内置了,写法为 type: 'asset/source'

我尝试提取了二进制文件的内容,结果得到了一堆乱码。

常见 plugin

plugin 可以在 webpack 的一些生命周期钩子函数上做处理,在合适的时机调用 webpack 提供的 API,改变输出结果。

通过使用 plugin,我们能够实现对 webpack 功能的加强,解决 loader 无法处理的穿插在 webpack 打包过程中的各种事情。

html-webpack-plugin

html-webpack-plugin 是一个简化将打包文件放到 HTML 过程的插件,因为我们打包的文件会产生多个文件,可能会有 hash 值,可能会拆包,如果全都要自己一个个引入 html 文件里,无疑非常繁琐且容易写错。这个插件可以帮我们简化这方面的工作。

clean-webpack-plugin

clean-webpack-plugin 插件用于构建前清空 “构建目录”(output.path 配置指向的目录)。这样做可以防止残留一些多余的文件,比如修改源文件后,导致打包后的新文件的 hash 值发生了变化,使用了旧 hash 的旧的文件就保留下来,实属多余。

我看到了一种蜜汁实现 clean-webpack-plugin 的方法。就是在 package.json 上加上执行脚本:

"scripts": {
  "prebuild": "rm -rf dist/*",
  // ...
}

这样执行 build 脚本前,就会先执行 prebuild 脚本,清空构建目录。某种意义上,确实可以。但如果以后输出目录改了,容易漏改这里。

copy-webpack-plugin

copy-webpack-plugin 插件可以复制多个文件或文件夹到构建目录(build directory,就是打包文件输出的目录)。使用场景:

  • 拷贝 static 文件夹,放一些不通过 import 引入的资源。比如有些比较小众的不提供 ES 模块化文件的第三方库、一些 SDK。使用插件后,我们就可以在模板 html 文件中写上 <script src="/public/lib-a.js"></script>
  • robot.txt 文件的拷贝。
new CopyWebpackPlugin([
  {
    from: path.join(__dirname, 'static'), 
    to: 'static',
    ignore: ['.*']
  },
  {
    from: resolvePath('robots.txt'),
    to: 'robots.txt'
  }
]

webpack.ProvidePlugin

ProvidePlugin 是 webpack 内置的一个插件。作用是自动加载模块,不用每次都写一行 import 或 require,适用于一些经常导入到其他模块的基础模块。注意这个插件并不是把指定的模块注入到全局作用域中,只是让你在模块文件中少写一行引入逻辑。如:

plugins: [
  new webpack.ProvidePlugin({
    $: 'jquery',
    _map: ['lodash', 'map']
  })
]

webpack.DefinePlugin

webpack.DefinePlugin 是 webpack 内置插件,可以实现在编译时将指定的变量替换为其他值。有点像是 C 语言中的宏。

如果作用域中含有同名变量,DefinePlugin 中定义的变量不会覆盖掉它,你可以将其看作比全局作用域还高的一个作用域,当然事实上它们没有注入到作用域中。

本质上来说,就是替换 JS 文件中匹配的变量形式的字符串,替换为指定的字符串。

new webpack.DefinePlugin({
  PRODUCTION: JSON.stringify(!!env.production),
  CONSOLE: 'console.log("我就输出一下")'
})
// 源码
console.log(PRODUCTION)
CONSOLE

// 生产环境使用 DefinePlugin 后,转换为:
console.log(true)
console.log("我就输出一下")

terser-webpack-plugin / uglifyjs-webpack-plugin

uglifyjs-webpack-pluginterser-webpack-plugin 都是代码压缩插件,前者已经废弃,推荐使用后者。

我们进行代码压缩的时候,原来使用的是 uglify-es(uglify-js 不支持 ES6,uglify-es 是它的 harmony 分支,支持 ES6+),但是后来 uglify-es 不再维护了。于是有人 fork 了 uglify-es,创建了 terser,并不停地维护。

webpack 自身内置了代码压缩插件,原来使用的是 uglify-es,在 v4.26.0 之后改为使用 terser。代码压缩插件只会在生产环境的情况下执行,当然我们可以通过设置 optimization.minimizefalse 强行关闭代码压缩功能。

当然你可以使用最新版的 terser 或者做一些配置功能,再单独安装一个 terser-webpack-plugin 插件。和一般插件不同,代码压缩插件配置需要放到 optimization.minimizer 位置。

const TerserPlugin = require('terser-webpack-plugin')

optimization: {
  minimizer: [
    new TerserPlugin({
      parallel: true, // 并行
      terserOptions: {
        compress: {
          drop_console: false // 丢掉 console,减少内存泄漏
        }
      }
    }),
  ]
}

结尾

以上就是常见的 loader 和 plugin 的说明,它们几乎在所有项目中都要用到,有必要花时间去思考理解。

实践是检验真理的唯一标准。如果你想要更好地理解这些 loader 和 plugin,我还是建议自己从零到一进行 webpack 配置,观察不同配置导致的输出结果。

参考