本文将会对webpack配置上进行优化,同时学习如何开发 Loader 和 Plugin.

1.优化构建速度

1.1 构建费时分析

1.安装speed-measure-webpack-plugin

  1. npm i -D speed-measure-webpack-plugin
  1. 2.修改配置
  1. ...
  2. // 费时分析
  3. const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
  4. const smp = new SpeedMeasurePlugin();
  5. ...
  6. const config = {...}
  7. module.exports = (env, argv) => {
  8. // 这里可以通过不同的模式修改 config 配置
  9. return smp.wrap(config);
  10. }
  1. 3.执行打包<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/28976061/1660030345595-fe468fd3-4260-4b7f-b01a-7f320a3e3714.png#clientId=u4d00c177-d504-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=279&id=u8880fb5f&margin=%5Bobject%20Object%5D&name=image.png&originHeight=349&originWidth=916&originalType=binary&ratio=1&rotation=0&showTitle=false&size=42577&status=done&style=none&taskId=u94c293ce-82dd-428d-adc0-a6ef2fdfaff&title=&width=732.8)<br />💡值得注意的是,有些情况下不会兼容该插件的新版本,需要进行降级处理。但为了使用费时分析而对插件进行降级或者修改配置的写法是不大划算的。

1.2 优化 resolve 配置

1.2.1 alias
alias 用来创建 importrequire的别名,用来简化模块的引用,项目中基本都需要进行配置

  1. const path = require('path')
  2. ...
  3. // 路径处理方法
  4. function resolve(dir){
  5. return path.join(__dirname, dir);
  6. }
  7. const config = {
  8. ...
  9. resolve:{
  10. // 配置别名
  11. alias: {
  12. '~': resolve('src'),
  13. '@': resolve('src'),
  14. 'components': resolve('src/components'),
  15. }
  16. }
  17. };
  1. 配置完成后,我们就可以在项目中简化路径。
  1. // 使用 src 别名 ~
  2. import '~/fonts/iconfont.css'
  3. // 使用 src 别名 @
  4. import '@/fonts/iconfont.css'
  5. // 使用 components 别名
  6. import footer from "components/footer";

1.2.1 extensions
在 webpack 中,默认配置如下:

  1. const config = {
  2. //...
  3. resolve: {
  4. // extensions: ['.js', '.json', '.wasm'], // 默认配置
  5. extensions: ['.ts', '...'], // 手动配置,此处也利用 扩展运算符...保留默认配置
  6. },
  7. };
  1. 当用户引入模块时不带扩展名,例如:
  1. import file from '../path/to/file';

那么webpack 就会按照extensions 配置的数组从左到右的顺序尝试去解析模块。

1.2.3 modules
告诉 webpack 解析模块时应该搜索的目录,配置如下:

  1. const path = require('path');
  2. // 路径处理方法
  3. function resolve(dir){
  4. return path.join(__dirname, dir);
  5. }
  6. const config = {
  7. //...
  8. resolve: {
  9. modules: [resolve('src'), 'node_modules'],
  10. },
  11. };
  1. 此处告诉 webpack 优先 src 目录下查找需要解析的文件,会大大节省查找时间。

1.2.4 resolveLoader
resolveLoader 与上面的 resolve 对象的属性集合相同, 但仅用于解析 webpack 的 loader 包。一般情况下保持默认配置就可以了,但如果你有自定义的 Loader 就需要配置一下,不配可能会因为找不到 loader 报错.
例如:我们在 loader 文件夹下面,放着我们自己写的 loader,配置如下:

  1. const path = require('path');
  2. // 路径处理方法
  3. function resolve(dir){
  4. return path.join(__dirname, dir);
  5. }
  6. const config = {
  7. //...
  8. resolveLoader: {
  9. modules: ['node_modules',resolve('loader')]
  10. },
  11. };

1.3 externals

externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法。
例如,从 CDN 引入 jQuery,而不是将其打包:
1.引入连接

  1. <script
  2. src="https://code.jquery.com/jquery-3.1.0.js"
  3. integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
  4. crossorigin="anonymous"
  5. ></script>

2.配置

  1. const config = {
  2. //...
  3. externals: {
  4. jquery: 'jQuery',
  5. },
  6. };
  1. 3.使用 jQuery
  1. import $ from 'jquery';
  2. $('.my-element').animate(/* ... */);

我们可以用这样的方法来剥离不需要改动的一些依赖,大大节省打包构建的时间。

1.4 缩小范围

在配置 loader 的时候,我们需要更精确的去指定 loader 的作用目录或者需要排除的目录,通过使用includeexclude 两个配置项可以实现这个功能。

  • **include**:符合条件的模块进行解析
  • **exclude**:排除符合条件的模块,不解析;其优先级更高 ```javascript const path = require(‘path’);

// 路径处理方法 function resolve(dir){ return path.join(__dirname, dir); }

const config = { //… module: { noParse: /jquery|lodash/, rules: [ { test: /.js$/i, include: resolve(‘src’), exclude: /node_modules/, use: [ ‘babel-loader’, ] }, // … ] } };

  1. <a name="Jp0lo"></a>
  2. ### 1.5 noParse
  3. - 不需要解析依赖的第三方大型类库等,可以通过这个字段进行配置,以提高构建速度
  4. - 使用 noParse 进行忽略的模块文件中不会解析` import`、`require` 等语法
  5. ```javascript
  6. const config = {
  7. //...
  8. module: {
  9. noParse: /jquery|lodash/,
  10. rules:[...]
  11. }
  12. };

1.6 IgnorePlugin

防止在 import 或 require 调用时,生成以下正则表达式匹配的模块:

  • requestRegExp 匹配(test)资源请求路径的正则表达式。
  • contextRegExp 匹配(test)资源上下文(目录)的正则表达式。
    1. new webpack.IgnorePlugin({ resourceRegExp, contextRegExp });

    1.7 多进程配置

    💡注意:实际上在小型项目中,开启多进程打包反而会增加时间成本,因为启动进程和进程间通信都会有一定开销。

1.安装

  1. npm i -D thread-loader
  1. 配置在 [thread-loader](https://link.juejin.cn/?target=https%3A%2F%2Fwebpack.docschina.org%2Floaders%2Fthread-loader%2F%23root) 之后的 loader 都会在一个单独的 worker 池(worker pool)中运行。<br />2.配置
  1. const config = {
  2. //...
  3. module: {
  4. noParse: /jquery|lodash/,
  5. rules: [
  6. {
  7. test: /\.js$/i,
  8. include: resolve('src'),
  9. exclude: /node_modules/,
  10. use: [
  11. {
  12. loader: 'thread-loader', // 开启多进程打包
  13. options: {
  14. worker: 3,
  15. }
  16. },
  17. 'babel-loader',
  18. ]
  19. },
  20. // ...
  21. ]
  22. }
  23. };

1.8 利用缓存

利用缓存可以大幅提升重复构建的速度
1.8.1 babel-loader 开启缓存

  • babel 在转译 js 过程中时间开销比价大,将 babel-loader 的执行结果缓存起来,重新打包的时候,直接读取缓存
  • 缓存位置: node_modules/.cache/babel-loader

具体配置如下:

  1. const config = {
  2. module: {
  3. noParse: /jquery|lodash/,
  4. rules: [
  5. {
  6. test: /\.js$/i,
  7. include: resolve('src'),
  8. exclude: /node_modules/,
  9. use: [
  10. // ...
  11. {
  12. loader: 'babel-loader',
  13. options: {
  14. cacheDirectory: true // 启用缓存
  15. }
  16. },
  17. ]
  18. },
  19. // ...
  20. ]
  21. }
  22. }

那其他的 loader 如何将结果缓存呢?cache-loader 就可以帮我们完成这件事情。

1.8.2 cache-loader

  • 缓存一些性能开销比较大的 loader 的处理结果
  • 缓存位置:node_modules/.cache/cache-loader
  1. 安装

    1. npm i -D cache-loader
  2. 修改配置

    1. const config = {
    2. module: {
    3. // ...
    4. rules: [
    5. {
    6. test: /\.(s[ac]|c)ss$/i, //匹配所有的 sass/scss/css 文件
    7. use: [
    8. // 'style-loader',
    9. MiniCssExtractPlugin.loader,
    10. 'cache-loader', // 获取前面 loader 转换的结果
    11. 'css-loader',
    12. 'postcss-loader',
    13. 'sass-loader',
    14. ]
    15. },
    16. // ...
    17. ]
    18. }
    19. }

1.8.3 hard-source-webpack-plugin

  • hard-source-webpack-plugin 为模块提供了中间缓存,重复构建时间大约可以减少 80%,但是在 webpack5 中已经内置了模块缓存,不需要再使用此插件。

1.8.4 cache持久化缓存
通过配置 cache 缓存生成的 webpack 模块和 chunk,来改善构建速度。

  1. const config = {
  2. cache: {
  3. type: 'filesystem',
  4. },
  5. };

2.优化构建结果

2.1 构建结果分析

借助插件 webpack-bundle-analyzer 我们可以直观的看到打包结果中,文件的体积大小、各模块依赖关系、文件是够重复等问题,极大的方便我们在进行项目优化的时候,进行问题诊断。
1.安装后修改配置:

  1. // 引入插件
  2. const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
  3. const config = {
  4. // ...
  5. plugins:[
  6. // ...
  7. // 配置插件
  8. new BundleAnalyzerPlugin({
  9. // analyzerMode: 'disabled', // 不启动展示打包报告的http服务器
  10. // generateStatsFile: true, // 是否生成stats.json文件
  11. })
  12. ],
  13. };
  1. 2.修改启动命令:
  1. "scripts": {
  2. // ...
  3. "analyzer": "cross-env NODE_ENV=prod webpack --progress --mode production"
  4. },

3.执行编译命令npm run analyzer

2.2 压缩 CSS

1.安装optimize-css-assets-webpack-plugin

  1. $ npm install -D optimize-css-assets-webpack-plugin

2.修改配置

  1. // ...
  2. // 压缩css
  3. const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
  4. // ...
  5. const config = {
  6. // ...
  7. optimization: {
  8. minimize: true,
  9. minimizer: [
  10. // 添加 css 压缩配置
  11. new OptimizeCssAssetsPlugin({}),
  12. ]
  13. },
  14. // ...
  15. }
  16. // ...

2.3 压缩 JS

在生产环境下打包默认会开启 js 压缩,但是当我们手动配置 optimization 选项之后,就不再默认对 js 进行压缩,需要我们手动去配置。

因为 webpack5 内置了terser-webpack-plugin 插件,所以我们不需重复安装,直接引用就可以了,具体配置如下:

  1. const TerserPlugin = require('terser-webpack-plugin');
  2. const config = {
  3. // ...
  4. optimization: {
  5. minimize: true, // 开启最小化
  6. minimizer: [
  7. // ...
  8. new TerserPlugin({})
  9. ]
  10. },
  11. // ...
  12. }

2.4 清除无用的 CSS

purgecss-webpack-plugin 会单独提取 CSS 并清除用不到的 CSS
1.安装后添加配置:

  1. // ...
  2. const PurgecssWebpackPlugin = require('purgecss-webpack-plugin')
  3. const glob = require('glob'); // 文件匹配模式
  4. // ...
  5. function resolve(dir){
  6. return path.join(__dirname, dir);
  7. }
  8. const PATHS = {
  9. src: resolve('src')
  10. }
  11. const config = {
  12. plugins:[ // 配置插件
  13. // ...
  14. new PurgecssPlugin({
  15. paths: glob.sync(`${PATHS.src}/**/*`, {nodir: true})
  16. }),
  17. ]
  18. }
  1. 3. index.html 新增节点
  1. <head>
  2. <meta charset="UTF-8">
  3. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  4. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  5. <title>ITEM</title>
  6. </head>
  7. <body>
  8. <p></p>
  9. <!-- 使用字体图标文件 -->
  10. <i class="iconfont icon-member"></i>
  11. <div id="imgBox"></div>
  12. <!-- 新增 div,设置 class used -->
  13. <div class="used"></div>
  14. </body>
  15. </html>

4.在 main.css 中添加样式

  1. .used {
  2. width: 200px;
  3. height: 200px;
  4. background: #ccc;
  5. }
  6. .unused {
  7. background: chocolate;
  8. }
  1. 5.执行打包,发现只有`.use`被保留下来,而`.unused`没有被保存。

2.5 Tree-shaking

Tree-shaking 作用是剔除没有使用的代码,以降低包的体积

  • webpack 默认支持,需要在 **.bablerc**里面设置 module:false,即可在生产环境下默认开启

    1. module.exports = {
    2. presets: [
    3. [
    4. "@babel/preset-env",
    5. {
    6. module: false,
    7. useBuiltIns: "entry",
    8. corejs: "3.9.1",
    9. targets: {
    10. chrome: "58",
    11. ie: "11",
    12. },
    13. },
    14. ],
    15. ],
    16. plugins: [
    17. ["@babel/plugin-proposal-decorators", { legacy: true }],
    18. ["@babel/plugin-proposal-class-properties", { loose: true }],
    19. ]
    20. };

    2.6 Scope Hoisting

    Scope Hoisting 即作用域提升,原理是将多个模块放在同一个作用域下,并重命名防止命名冲突,通过这种方式可以减少函数声明和内存开销

  • webpack 默认支持,在生产环境下默认开启

  • 只支持 es6 代码

    3. 优化运行时体验

    运行时优化的核心就是提升首屏的加载速度,主要的方式就是

  • 降低首屏加载文件体积,首屏不需要的文件进行预加载或者按需加载

    3.1入口点分割

    配置多个打包入口,多页打包,这里不过多介绍

    3.2 splitChunks 分包配置

    optimization.splitChunks 是基于 SplitChunksPlugin 插件实现的
    默认情况下,它只会影响到按需加载的 chunks,因为修改 initial chunks 会影响到项目的 HTML 文件中的脚本标签。
    webpack 将根据以下条件自动拆分 chunks:

  • 新的 chunk 可以被共享,或者模块来自于 node_modules 文件夹

  • 新的 chunk 体积大于 20kb(在进行 min+gz 之前的体积)
  • 当按需加载 chunks 时,并行请求的最大数量小于或等于 30
  • 当加载初始化页面时,并发请求的最大数量小于或等于 30

1.默认配置介绍

  1. module.exports = {
  2. //...
  3. optimization: {
  4. splitChunks: {
  5. chunks: 'async', // 有效值为 `all`,`async` 和 `initial`
  6. minSize: 20000, // 生成 chunk 的最小体积(≈ 20kb)
  7. minRemainingSize: 0, // 确保拆分后剩余的最小 chunk 体积超过限制来避免大小为零的模块
  8. minChunks: 1, // 拆分前必须共享模块的最小 chunks 数。
  9. maxAsyncRequests: 30, // 最大的按需(异步)加载次数
  10. maxInitialRequests: 30, // 打包后的入口文件加载时,还能同时加载js文件的数量(包括入口文件)
  11. enforceSizeThreshold: 50000,
  12. cacheGroups: { // 配置提取模块的方案
  13. defaultVendors: {
  14. test: /[\/]node_modules[\/]/,
  15. priority: -10,
  16. reuseExistingChunk: true,
  17. },
  18. default: {
  19. minChunks: 2,
  20. priority: -20,
  21. reuseExistingChunk: true,
  22. },
  23. },
  24. },
  25. },
  26. };

3.3 代码懒加载

针对首屏加载不太需要的一些资源,可以采用懒加载的方式。
这里给出一个点击图片加载描述的例子:
1.新建图片描述信息

  1. // desc.js
  2. const ele = document.createElement('div')
  3. ele.innerHTML = '我是图片描述'
  4. module.exports = ele

2.点击图片引进描述

  1. // index.js
  2. import './main.css';
  3. import './sass.scss'
  4. import logo from '../public/avatar.png'
  5. import '@/fonts/iconfont.css'
  6. const a = 'Hello btqf'
  7. console.log(a)
  8. const img = new Image()
  9. img.src = logo
  10. document.getElementById('imgBox').appendChild(img)
  11. // 按需加载
  12. img.addEventListener('click', () => {
  13. import('./desc').then(({ default: element }) => {
  14. console.log(element)
  15. document.body.appendChild(element)
  16. })
  17. })
  1. 3.查看效果
  2. - 点击前

image.png
image.png

  1. - 点击后

image.png
image.png

3.4 prefetch 与 preload

上面我们使用异步加载的方式引入图片的描述,但是如果需要异步加载的文件比较大时,在点击的时候去加载也会影响到我们的体验,这个时候我们就可以考虑使用 prefetch 来进行预拉取.
3.4.1 prefetch
prefetch (预获取):浏览器空闲的时候进行资源的拉取.
对上面的代码进行改动

  1. // desc.js
  2. // 按需加载
  3. img.addEventListener('click', () => {
  4. import( /* webpackPrefetch: true */ './desc').then(({ default: element }) => {
  5. console.log(element)
  6. document.body.appendChild(element)
  7. })
  8. })

3.4.2 preload

  • preload (预加载):提前加载后面会用到的关键资源
  • ⚠️ 因为会提前拉取资源,如果不是特殊需要,谨慎使用

示例:

  1. import(/* webpackPreload: true */ 'ChartingLibrary');

关于webpack进阶的讲述大概就到这里了,欢迎大家的指正!