Webpack 2024 前前端架构老鸟的分享(二)
三、Webpack 生命周期和工作原理
3.1 生命周期概述
Webpack 的运行过程就是一个遵循生命周期的过程,在生命周期的各个阶段会执行对应的方法和插件,这些方法和插件就是生命周期事件。生命周期的主要事件有:
3.2 工作原理和流程
-
初始化(Initializing)
- 读取并解析 Webpack 配置文件
- 创建 Compiler 对象并装载所有配置项
-
编译(Compiling)
- 创建 Compilation 对象
- 构建模块依赖图谱(Module Graph)
- 进行一系列编译
-
完成(Completion)
- 对编译后的模块进行优化处理
- 生成 Chunk 资源文件
-
发出(Emitting)
- 将 Chunk 资源文件输出到指定位置
-
输出(Output)
- 将资源文件输出到指定的输出路径
-
输出完成(Done)
- 清理资源并输出统计信息
四、Webpack 高级概念和优化策略
4.1 Tree Shaking
优势
Tree Shaking 的优点包括:
- 减小文件大小,加快加载速度。
- 提高性能,降低资源消耗。
- 清除未使用的代码,优化代码质量。
其原理是基于静态代码分析和模块依赖图,识别未被实际使用的代码,并将其从最终生成的 bundle 中移除。
使用场景
- 在生产环境下自动开启
- 必须使用 ES6 Module 语法
- 需要配合 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
优势
- 提高资源加载速度
- 提高缓存利用率
- 并行加载资源
实现方式
- 入口点分割
- 动态导入(import())
- 按需加载
最佳实践
- 入口点分割
// 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 项目目录结构
- 创建项目目录结构:
mkdir project
cd project
mkdir src dist src/components src/utils src/assets src/views src/router src/store
- 创建入口文件和 HTML 文件:
touch src/index.js src/index.html
- 创建配置文件:
touch .babelrc .eslintrc .gitignore webpack.config.js
- 初始化 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 环境配置
上面关于配置的废话优点多注释都在代码里,下面是个简单的示例大家自己酌情使用。 并没有完全使用到之前使用的所有插件,有需要的自己动手尝试。
- 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 中使用这个缓存组
}
}
}
}
};
- 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 // 开启模块热替换
}
});
- 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 前前端架构老鸟的分享(三)》以及第三篇结束后的总篇。 创作不易。
多多支持。
蟹蟹🦀🦀。