Webpack 2024 前前端架构老鸟的分享(二)

225 阅读7分钟

Mastering Webpack.gif

Webpack 2024 前前端架构老鸟的分享(二)

三、Webpack 生命周期和工作原理

3.1 生命周期概述

Webpack 的运行过程就是一个遵循生命周期的过程,在生命周期的各个阶段会执行对应的方法和插件,这些方法和插件就是生命周期事件。生命周期的主要事件有:

3.2 工作原理和流程

  1. 初始化(Initializing)

    • 读取并解析 Webpack 配置文件
    • 创建 Compiler 对象并装载所有配置项
  2. 编译(Compiling)

    • 创建 Compilation 对象
    • 构建模块依赖图谱(Module Graph)
    • 进行一系列编译
  3. 完成(Completion)

    • 对编译后的模块进行优化处理
    • 生成 Chunk 资源文件
  4. 发出(Emitting)

    • 将 Chunk 资源文件输出到指定位置
  5. 输出(Output)

    • 将资源文件输出到指定的输出路径
  6. 输出完成(Done)

    • 清理资源并输出统计信息

四、Webpack 高级概念和优化策略

4.1 Tree Shaking

优势

Tree Shaking 的优点包括:

  1. 减小文件大小,加快加载速度。
  2. 提高性能,降低资源消耗。
  3. 清除未使用的代码,优化代码质量。

其原理是基于静态代码分析和模块依赖图,识别未被实际使用的代码,并将其从最终生成的 bundle 中移除。

使用场景

  1. 在生产环境下自动开启
  2. 必须使用 ES6 Module 语法
  3. 需要配合 Terser 等压缩工具使用

最佳实践

一般情况下,生产环境下 Webpack 会自动开启 Tree Shaking,如果没有开启可以手动在配置文件中加入:

module.exports = {
  mode: 'production', 
  optimization: {
    usedExports: true
  }
}

示例

未使用 Tree Shaking:

// utils.js
export function add(a, b) {
  return a + b;
}
​
export function minus(a, b) {
  return a - b; 
}
​
// index.js
import { add } from './utils';
​
console.log(add(1, 2));

使用 Tree Shaking 后,minus 函数会被自动删除。

4.2 Code Splitting

优势

  • 提高资源加载速度
  • 提高缓存利用率
  • 并行加载资源

实现方式

  1. 入口点分割
  2. 动态导入(import())
  3. 按需加载

最佳实践

  • 入口点分割
// webpack.config.js
module.exports = {
  entry: {
    main: './src/index.js',
    vendor: './src/vendor.js'
  }
}
  • 动态导入
// index.js
import('./utils').then(utils => {
  utils.default();
});
  • 按需加载
// webpack.config.js 
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'async'
    }
  }
}

4.3 缓存策略

  • 文件指纹

    通过为输出文件添加哈希值,可以有效防止浏览器缓存旧文件。

// webpack.config.js
module.exports = {  
  output: {
    filename: '[name].[chunkhash].js'
  }
}
  • 缓存清理 webpack5弃用了第三方的clean-webpack-plugin,取而代之的则是将清楚之前的打包缓存集成到out的属性中
module.exports = {

  // 出口
  output: {
    path: utils.resolve("../dist"),
    filename: "static/js/[name]." + ID + ".js",
    publicPath: "/", // 打包后的资源的访问路径前缀
    clean: true, //每次构建清除dist包
  }
}
  • 缓存压缩

    安装npm install terser-webpack-plugin启用资源压缩和缓存压缩文件可以减少传输体积,提高加载速度。

    const TerserPlugin = require("terser-webpack-plugin");
    module.exports = {
      optimization: {
       //打包的内容可在这里拆分文件和压缩css js 和拆分第三方的插件
        minimize: true, // 启用资源压缩
        minimizer: [
          new TerserPlugin({
            terserOptions: {
              compress: true, // 启用压缩
              mangle: true, // 启用混淆
            },
            extractComments: false, // 不提取注释
          })]
      }
    }
    

4.4 HMR(Hot Module Replacement)

可以在运行时更新各种模块,而无需进行完全刷新。webpack5简化了热更新的配置将其更简单的融合到了devServer的hot属性中

配置 HMR

// webpack.config.js
module.exports = {
  devServer: {
    hot: true//热更新
  }
}

4.5 多线程/多实例构建

通过 happypack或thread-loader 等工具可以启用多线程/多实例构建,提高构建速度。

HappyPack 示例

HappyPack 是一个能够通过多进程模型,来加速构建速度的工具。它可以将每个 Loader 的处理过程放到单独的 worker 池(worker pool)中,并行处理多个任务。使用 HappyPack,你需要对每个需要并行处理的 Loader 进行相应的配置。

// webpack.config.js
const HappyPack = require('happypack');

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/, // 匹配 JavaScript 文件
        use: 'happypack/loader?id=babel', // 使用 HappyPack 中的 babel loader
        exclude: /node_modules/ // 排除 node_modules 目录
      }
    ]
  },
  plugins: [
    new HappyPack({
      id: 'babel', // 定义 HappyPack 的 id,用于区分不同的 loader
      threads: 4, // 启动 4 个线程来处理任务
      loaders: ['babel-loader'] // 使用 babel-loader 进行转译
    })
  ]
};

4.6 持久化缓存

webpack5弃用了cache-loader、hard-source-webpack-plugin 等插件。 通过内部的cache对象简单的配置即可开启持久化缓存,提高二次构建速度。

cache 示例

// webpack.config.js
module.exports = {
    // webpack5的缓存升级成内置的
  // 使用 webpack 5 内置的持久化缓存功能,你就不再需要手动安装和配置 cache-loader,Webpack 将会自  动处理构建过程中的缓存机制,从而提高构建性能。

  cache: {
    // 使用持久化缓存
    type: 'filesystem',
    // 可选的缓存目录,默认为 node_modules/.cache/webpack
    cacheDirectory: utils.resolve(__dirname, '.webpack_cache'),
  },
}

4.7 DllPlugin 和 DllReferencePlugin配合optimization的splitChunks对象拆分代码

DllPlugin 和 DllReferencePlugin 可以将常用的第三方库提取到单独的 dll 文件中,实现按需加载,提高构建速度。

DllPlugin 示例

// webpack.dll.config.js
const webpack = require('webpack'); // 导入 Webpack 模块

module.exports = {
  entry: { // 定义 DLL 包的入口点
    vendor: [ // DLL 入口点的键
      'react', // React 库
      'react-dom' // React DOM 库
    ]
  },
  output: { // 配置 DLL 包的输出设置
    filename: '[name].dll.js', // 使用模板生成 DLL 文件名(例如:vendor.dll.js)
    path: path.resolve(__dirname, 'dist'), // 输出目录的绝对路径
    library: '[name]_dll' // 加载 DLL 时暴露的全局变量名称
  },
  plugins: [
    new webpack.DllPlugin({ // 配置 DllPlugin
      path: path.resolve(__dirname, 'dist', '[name]-manifest.json'), // DLL 清单文件的路径
      name: '[name]_dll' // 全局变量和清单文件的名称
    })
  ]
};

DllReferencePlugin 示例

// webpack.config.js
const webpack = require('webpack'); // 导入 Webpack 模块

module.exports = {
  plugins: [
    new webpack.DllReferencePlugin({
      manifest: require('./dist/vendor-manifest.json') // 引用 DLL 清单文件
    })
  ]
};

optimization的splitChunks对象

const path = require('path');
const webpack = require('webpack');

module.exports = {
  optimization: {
    // 优化配置
    splitChunks: {
      // 使用 splitChunks 来拆分代码块
      cacheGroups: {
        // 缓存组配置
        react: {
          // 匹配规则,匹配 react 和 react-dom
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'react', // 输出的 chunk 名称
          chunks: 'all' // 对所有类型的 chunk 生效
        },
        reactRouter: {
          // 匹配规则,匹配 react-router
          test: /[\\/]node_modules[\\/](react-router)[\\/]/,
          name: 'reactRouter', // 输出的 chunk 名称
          chunks: 'all' // 对所有类型的 chunk 生效
        },
        axios: {
          // 匹配规则,匹配 axios
          test: /[\\/]node_modules[\\/](axios)[\\/]/,
          name: 'axios', // 输出的 chunk 名称
          chunks: 'all' // 对所有类型的 chunk 生效
        },
        common: {
          // 匹配规则,匹配其他 node_modules 中的模块
          test: /[\\/]node_modules[\\/]/,
          name: 'common', // 输出的 chunk 名称
          chunks: 'all', // 对所有类型的 chunk 生效
          minSize: 20000, // 模块的最小大小(字节)
          minChunks: 2, // 要生成的 chunk 的最小数量
          maxAsyncRequests: 30, // 按需加载时并行请求的最大数量
          maxInitialRequests: 30, // 入口点并行请求的最大数量
          enforceSizeThreshold: 50000 // 强制执行最小和最大大小限制
        },
        default: {
          // 默认配置
          minChunks: 2, // 最小 chunk 数量
          priority: -20, // 优先级
          reuseExistingChunk: true // 重用已经存在的 chunk
        }
      }
    }
  }
};

4.8 分析构建性能

使用 webpack-bundle-analyzer、speed-measure-webpack-plugin 等工具可以分析构建性能瓶颈,优化构建速度。

webpack-bundle-analyzer 示例

// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
​
module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}

五、Webpack 插件开发和实战

5.1 插件开发基础

Webpack 插件是一个具有 apply 方法的 JavaScript 对象,apply 方法会被 Webpack Compiler 调用,可以在不同生命周期钩子函数中执行相关任务。

class MyPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
      // 发射资源前执行插件逻辑
      callback();
    });
}
​
module.exports = MyPlugin;

5.2 常用插件开发实战

  • 自动生成 HTML 文件插件
const fs = require('fs');
const path = require('path');
​// 自动生成 HTML 文件插件
class HtmlGeneratorPlugin {
  apply(compiler) {
  // 注册 emit 钩子,该钩子在将资源输出到目标目录之前触发
    compiler.hooks.emit.tapAsync('HtmlGeneratorPlugin', (compilation, callback) => {
    // 生成 HTML 内容
      const htmlContent = `
        <!DOCTYPE html>
        <html>
          <head>
            <meta charset="UTF-8">
            <title>Webpack App</title>
          </head>
          <body>
            <script src="${compilation.assets['main.js'].publicPath}"></script>
          </body>
        </html>
      `;
​     // 将 HTML 内容添加到输出资源中
      compilation.assets['index.html'] = {
            source: () => htmlContent, // 返回 HTML 内容 
            size: () => htmlContent.length // 返回 HTML 内容的长度
      };
​    //回调函数
      callback();
    });
  }
}
​
module.exports = HtmlGeneratorPlugin;
  • 自动清理输出目录插件
const fs = require('fs');
const path = require('path');
​// 自动清理输出目录插件
class CleanOutputPlugin {
  constructor(options) {
    this.outputPath = options.outputPath;
  }
​
  apply(compiler) {
  // 注册 done 钩子,该钩子在编译完成后触发
    compiler.hooks.done.tap('CleanOutputPlugin', (stats) => {
      const outputPath = this.outputPath || stats.compilation.outputOptions.path;// 获取输出路径// 获取输出目录下的所有文件
      const files = fs.readdirSync(outputPath);
      // 遍历并删除每个文件
      for (const file of files) {
        fs.unlinkSync(path.join(outputPath, file));
      }
    });
  }
}
​
module.exports = CleanOutputPlugin;
  • Webpack Validator 插件

这是一个比较复杂的插件开发实例,用于校验 Webpack 配置文件的规则。包括检查配置项是否存在、类型是否正确、值是否在允许范围等。

// webpack-validator.js
const schema = require('./config-schema.json');
const Ajv = require('ajv');
const ajv = new Ajv({allErrors: true});
const validate = ajv.compile(schema);
​// Webpack 配置校验插件
class WebpackValidator {
  apply(compiler) {
  // 注册 run 钩子,该钩子在开始编译前触发
    compiler.hooks.run.tap('WebpackValidator', () => {
    // 使用 Ajv 进行配置校验
      const valid = validate(compiler.options);
      // 如果配置不合法,则输出错误信息并退出进程
      if (!valid) {
        console.error('Webpack configuration is invalid:');
        validate.errors.forEach(error => {
          console.error(`${error.dataPath} ${error.message}`);
        });
        process.exit(1);
      }
    });
  }
}
​
module.exports = WebpackValidator;

使用

 // 引入自定义插件
const { HtmlGeneratorPlugin, CleanOutputPlugin, WebpackValidator } = require('./plugins');
module.exports = {
   ...
   plugins: [ new HtmlGeneratorPlugin(), // 自动生成HTML文件插件
   ...
}


六、Webpack 在项目中的实践和最佳实践

6.1 项目目录结构

  1. 创建项目目录结构
mkdir project
cd project
mkdir src dist src/components src/utils src/assets src/views src/router src/store
  1. 创建入口文件和 HTML 文件
touch src/index.js src/index.html
  1. 创建配置文件
touch .babelrc .eslintrc .gitignore webpack.config.js
  1. 初始化 npm 项目
npm init -y

这里使用的node版本是v18.19.0,如果觉版本切换比较麻烦可以看一下我的另一篇文章NVM node版本管理

现在,我们已经创建了项目所需的目录结构和配置文件。接下来,我们可以根据需要填充这些文件和目录,并配置 webpack 和其他工具。

project
├── dist
├── src
│   ├── components
│   ├── utils
│   ├── assets
│   ├── views
│   ├── router
│   ├── store
│   ├── index.js
│   ├── index.html
├── .babelrc
├── .eslintrc  
├── .gitignore
├── package.json
├── webpack.base.config.js
├── webpack.dev.config.js
├── webpack.prod.config.js

这里不用大家一步步安装了,直接复制我的package.json,动手能力强的还是希望大家自己一步步安装去遇见问题解决问题

{
  "name": "webpack-demo2",
  "version": "1.0.0",
  "description": "A webpack demo project",
  "main": "index.js",
  "scripts": {
    "start": "webpack serve --open",
    "build": "webpack --mode production"
  },
  "keywords": [
    "webpack",
    "demo"
  ],
  "author": "Alben",
  "license": "MIT",
  "devDependencies": {
    "@babel/core": "^7.16.7",
    "@babel/preset-env": "^7.16.7",
    "@babel/preset-react": "^7.16.7",
    "@babel/preset-typescript": "^7.16.7",
    "babel-loader": "^8.2.3",
    "css-loader": "^6.5.1",
    "html-webpack-plugin": "^5.5.0",
    "postcss-loader": "^6.2.1",
    "style-loader": "^3.3.1",
    "terser-webpack-plugin": "^5.2.5",
    "vue-loader": "^16.8.3",
    "vue-template-compiler": "^2.6.14",
    "webpack": "^5.68.0",
    "webpack-cli": "^4.9.1",
    "webpack-dev-server": "^4.7.3",
    "webpack-merge": "^5.8.0"
  },
  "dependencies": {
    "vue": "^2.6.14",
    "vue-router": "^3.5.3"
  }
}

6.2 环境配置

上面关于配置的废话优点多注释都在代码里,下面是个简单的示例大家自己酌情使用。 并没有完全使用到之前使用的所有插件,有需要的自己动手尝试。

  1. webpack.base.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');

module.exports = {
  // 入口文件配置
  entry: './src/index.js',
  // 输出文件配置
  output: {
    path: path.resolve(__dirname, 'dist'), // 输出文件路径
    filename: '[name].[contenthash].js', // 输出文件名,[name] 会根据 entry 中的键名替换
    publicPath: '/' // 输出文件的公共路径
  },
  // 模块配置
  module: {
    rules: [
      {
        test: /\.js$/, // 匹配规则,使用正则表达式匹配以 .js 结尾的文件
        exclude: /node_modules/, // 排除 node_modules 目录
        use: {
          loader: 'babel-loader', // 使用 babel-loader 处理匹配到的文件
          options: {
            presets: ['@babel/preset-env'] // 使用 @babel/preset-env 进行转译
          }
        }
      },
      {
        test: /\.css$/, // 匹配规则,使用正则表达式匹配以 .css 结尾的文件
        use: ['style-loader', 'css-loader', 'postcss-loader'] // 使用 style-loader、css-loader 和 postcss-loader 处理匹配到的文件
      },
      {
        test: /\.(png|jpg|gif)$/, // 匹配规则,使用正则表达式匹配以 .png、.jpg 或 .gif 结尾的文件
        use: [
          {
            type: "asset/resource", //webpack5 不再需要使用url-loader或者file-loader
            options: {
              name: '[name].[ext]', // 输出文件名,[name] 会替换为原文件名,[ext] 会替换为原文件扩展名
              outputPath: 'images/' // 输出文件的路径
            }
          }
        ]
      }
    ]
  },
  // 插件配置
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html', // 使用 ./src/index.html 文件作为模板
      filename: 'index.html' // 输出文件名
    }),
    new webpack.HashedModuleIdsPlugin() // 根据模块的相对路径生成一个四位数的 hash 作为模块 id
  ],
  // 优化配置
  optimization: {
    runtimeChunk: 'single', // 提取 webpack 运行时代码到单独的文件
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/, // 匹配规则,匹配 node_modules 目录下的文件
          name: 'vendors', // 输出 chunk 的名称
          chunks: 'all' // 在所有的 chunk 中使用这个缓存组
        }
      }
    }
  }
};
  1. webpack.dev.config.js
const { merge } = require('webpack-merge');
const baseConfig = require('./webpack.base.config');

module.exports = merge(baseConfig, {
  mode: 'development', // 开发环境模式
  devtool: 'inline-source-map', // 使用 source map 提供源代码到构建后的代码的映射
  devServer: {
    contentBase: './dist', // 服务的根目录
    hot: true // 开启模块热替换
  }
});
  1. webpack.prod.config.js
const { merge } = require('webpack-merge');
const TerserPlugin = require('terser-webpack-plugin');
// //css压缩 webpack5 弃用改成css-minimizer-webpack-plugin
// const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const CssMinimizerWebpackPlugin = require("css-minimizer-webpack-plugin");
const baseConfig = require('./webpack.base.config');

module.exports = merge(baseConfig, {
  mode: 'production', // 生产环境模式
  devtool: 'source-map', // 使用 source map 提供源代码到构建后的代码的映射
  module: {
    rules: [
      {
        test: /\.css$/, // 匹配规则,使用正则表达式匹配以 .css 结尾的文件
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'] // 使用 MiniCssExtractPlugin.loader、css-loader 和 postcss-loader 处理匹配到的文件
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css', // 输出文件名
      chunkFilename: '[id].[contenthash].css' // 用于按需加载的 chunk 的输出文件名
    })
  ],
  optimization: {
    minimize: true, // 开启代码压缩
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: true, // 开启压缩
          mangle: true // 开启代码混淆
        },
        extractComments: false // 不提取注释
      }),
      new CssMinimizerWebpackPlugin({ // 添加 CssMinimizerWebpackPlugin 实例到 minimizer 数组中   
              parallel: true, // 启用并行压缩 
              minimizerOptions: { 
                  preset: ['default', { discardComments: { removeAll: true } }] // 使用默认配置,并移除所有注释 
            }
      }) 
      ],
    splitChunks: {
      chunks: 'all', // 对所有类型的 chunk 生效
      minSize: 20000, // 模块的最小大小(字节)
      maxAsyncRequests: 30, // 按需加载时并行请求的最大数量
      maxInitialRequests: 30, // 入口点并行请求的最大数量
      automaticNameDelimiter: '~', // 文件名的连接符
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/, // 匹配规则,匹配 node_modules 目录下的文件
          priority: -10 // 优先级
        },
        default: {
          minChunks: 2, // 最小 chunk 数量
          priority: -20, // 优先级
          reuseExistingChunk: true // 重用已经存在的 chunk
        }
      }
    }
  }
});

小结

通过深入学习和实践,相信你对Webpack的使用和优化已经有了更全面的认识。未来,随着Webpack的持续演进,我们期待更多强大的功能和特性的加入。作为技术人员,我们需要保持对Webpack生态的持续关注,并不断学习和探索新的最佳实践,以确保我们始终保持在技术的前沿。

请继续关注《Webpack 2024 前前端架构老鸟的分享(三)》以及第三篇结束后的总篇。 创作不易。

多多支持。

蟹蟹🦀🦀。