Webpack
1 概念
Webpack 是一种用于构建 JavaScript
应用程序的静态模块打包器,它能够以一种相对一致且开放的处理方式,加载应用中的所有资源文件(图片、CSS、视频、字体文件等),并将其合并打包成浏览器兼容的 Web 资源文件。
- 模块的打包:通过打包整合不同的模块文件保证各模块之间的引用和执行
- 代码编译:通过丰富的
loader
可以将不同格式文件如.sass
/.vue
/.jsx
转译为浏览器可以执行的文件 - 扩展功能:通过社区丰富的
plugin
可以扩展 Webpack 强大的功能,例如代码分割、代码混淆、代码压缩、按需加载等
2 loader 和 plugin
- Loader:
Loader 本质上是一个函数,负责代码的转译,即对接收到的内容进行转换后将转换后的结果返回。配置Loader通过在 modules.rules
中以数组的形式配置
- Plugin:
Plugin 本质上是一个带有apply(compiler)
的函数或类,基于 tapable 这个事件流框架来监听 webpack 构建/打包过程中发布的 hooks ,通过自定义的逻辑和功能来改变输出结果。 Plugin 通过 plugins
以数组的形式配置,例如代码分割、代码混淆、代码压缩、按需加载等
总结:
Loader 主要负责将代码转译为 webpack 可以处理的JavaScript代码,而 Plugin 更多的是负责通过接入 webpack 构建过程来影响构建过程以及产物的输出,Loader 的职责相对比较单一简单,而 Plugin 更为丰富多样
3 常见 loader
babel-loader
:将 es6 转译为 es5file-loader
:(内置)可以指定要复制和放置资源文件的位置,以及如何使用版本哈希命名以获得更好的缓存,并在代码中通过 URL 去引用输出的文件url-loader
:(内置)和file-loader
功能相似,但是可以通过指定阈值来根据文件大小使用不同的处理方式(小于阈值则返回base64格式编码并将文件的data-url
内联到bundle
中)raw-loader
:(内置)加载文件原始内容image-webpack-loader
: 加载并压缩图片资源awesome-typescirpt-loader
: 将 typescript 转换为 javaScript 并且性能优于ts-loader
sass-loader
/less-loader
: 将 SCSS/LESS 代码转换为 CSScss-loader
: 加载 CSS 代码 支持模块化、压缩、文件导入等功能特性style-loader
: 把 CSS 代码注入到 js 中,通过DOM
操作去加载CSS代码source-map-loader
: 加载额外的Source Map
文件eslint-loader
: 通过 ESlint 检查 js 代码cache-loader
: 可以在一些开销较大的Loader
之前添加可以将结果缓存到磁盘中,提高构建的效率thread-loader
: 多线程打包,加快打包速度
4 常见 plugin
define-plugin
: 定义环境变量(webpack4之后可以通过指定mode:production/development
实现同样效果)web-webpack-plugin
:为单页面应用输出 HTML 性能优于html-webpack-plugin
clean-webpack-plugin
: 每次打包时删除上次打包的产物, 保证打包目录下的文件都是最新的webpack-merge
: 用来合并公共配置文件,常用(例如分别配置webpack.common.config.js/ webpack.dev.config.js/webpack.production.config.js
并将其合并)ignore-plugin
: 忽略指定的文件,可以加快构建速度terser-webpack-plugin
:压缩ES6的代码(tree-shaking)mini-css-extract-plugin
: 将CSS提取为独立文件,支持按需加载css-minimize-webpack-plugin
:压缩CSS代码
css文件的压缩需要mini-css-extract-plugin
和css-minimize-webpack-plugin
的配合使用 即先使用mini-css-extract-plugin
将css代码抽离成单独文件,之后使用 css-minimize-webpack-plugin
对css代码进行压缩
copy-webpack-plugin
: 在构建的时候,复制静态资源到打包目录。compression-webpack-plugin
: 生产环境采用gzip
压缩JS和CSSParalleUglifyPlugin
: 多进程并行压缩jswebpack-bundle-analyzer
: 可视化webpack输出文件大小的根据speed-measure-webpack-plugin
: 用于分析各个loader和plugin的耗时,可用于性能分析webpack-dashboard
: 可以更友好地展示打包相关信息
5 如何编写 loader
// simple-loader.js
module.exports = function (source) {
// 获取 loader 的选项
const options = this.getOptions();
// 对源代码进行简单的字符串替换
const result = source.replace(/console\.log\(/g, `console.info(${options.prefix},`);
// 返回转换后的代码
return result;
};
如何写 Loader:
- 导出一个函数: Loader 本质上是一个导出函数的 Node.js 模块。这个函数接收源代码作为参数。
Loader 函数主要接受以下参数:
- source: 字符串类型,表示模块的源代码。这是 Loader 主要处理的对象,你可以对它进行解析、转换、添加或删除内容等操作。
- map: 对象类型,可选参数,表示 SourceMap 信息,用于代码调试。如果你对源代码进行了转换,最好也更新 SourceMap,以便于调试。
- meta: 对象类型,可选参数,用于传递自定义的元数据。例如,你可以通过 meta 传递一些解析器选项或者其他信息给下一个 Loader。
- 处理源代码: 在函数内部,你可以对源代码进行任何你想做的处理,例如转换语法,添加代码,或者替换内容等。
在 Loader 函数内部,
this
指向 Webpack 提供的 Loader 上下文对象 (LoaderContext
),它包含了许多工具函数和信息,例如:
- this.getOptions(): 获取 Loader 的配置选项,例如在
webpack.config.js
中通过options
属性配置的参数。- this.emitFile(): 将处理后的文件输出到 Webpack 的输出目录。
- this.resolve(): 解析模块路径,类似于 Node.js 中的
require.resolve()
。- this.context: 当前 Loader 所处理的模块所在的目录。
- this.request: 当前 Loader 的请求字符串,包含了 Loader 的路径和参数。
- 返回结果: 最后,你需要返回处理后的代码。
6 如何编写 plugin
// simple-plugin.js
class SimplePlugin {
apply(compiler) {
// 使用 compiler.hooks.done 钩子在编译完成后打印信息
compiler.hooks.done.tap('SimplePlugin', (stats) => {
console.log('编译完成!');
});
}
}
module.exports = SimplePlugin;
如何写 Plugin:
- 创建一个类: Plugin 是一个拥有
apply
方法的类。 - 注册钩子: 在
apply
方法中,你可以使用compiler
对象注册各种钩子函数,以便在 Webpack 构建过程的不同阶段执行自定义逻辑。 - 编写钩子函数: 钩子函数接收 Webpack 提供的参数,可以访问编译过程中的各种信息,例如模块,资源,代码块等。
资源处理和编译相关:
- entryOption(context, entry, name): 在设置入口配置后触发,可以用来修改入口配置。2
- beforeRun(compiler)、run(compiler): 在编译开始前/后触发。
- beforeCompile(params)、 compile(params): 在编译器开始/完成一次新的编译时触发。
- thisCompilation(compilation)、compilation(compilation, params): 在一次新的 compilation 创建/完成时触发, 可以用来访问 compilation 对象。
- make(compilation): 在创建 chunk 之前触发,可以用来创建/修改 chunk。
- normalModuleFactory(factory): 在创建
NormalModuleFactory
后触发,可以用来修改模块工厂的配置。- buildModule(module): 在模块构建开始时触发,可以用来获取或修改模块信息。
- afterCompile(compilation): 在编译完成后触发,可以用来访问编译后的资源。
输出相关:
- shouldEmit(compilation): 在确定是否输出资源之前触发,可以用来阻止资源输出。
- emit(compilation): 在资源输出到输出目录之前触发,可以用来修改输出资源的内容。
- afterEmit(compilation): 在资源输出到输出目录之后触发。
其他:
- done(stats): 在编译完成后触发,可以用来获取编译结果信息。
- watchRun(compiler): 在监听模式下,每次文件变化重新编译时触发。
- watchClose(compiler): 在监听模式下,结束监听时触发。
7 文件指纹
概念:打包产物的文件后缀 hash
作用:
在发布版本时,通过文件指纹来区分修改的文件和未修改的文件;浏览器通过文件指纹是否改变来决定使用缓存文件还是请求新文件。
- 种类:
Hash
:和整个项目的构建相关,只要项目有修改(compilation
实例改变),Hash
就会更新Contenthash
:和文件的内容有关,只有内容发生改变时才会修改Chunkhash
:不同的 entry 会构建出不同的 chunk
如何使用:
- JS文件:使用
Chunkhash
- CSS文件:使用
Contenthash
- 图片等静态资源: 使用
hash
module.exports = {
// ... 其他配置 ...
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[chunkhash].js', // JS 文件使用 Chunkhash
chunkFilename: '[name].[chunkhash].js',
clean: true, // 每次构建前清理 /dist 文件夹
},
module: {
rules: [
// ... 其他规则 ...
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
generator: {
filename: 'images/[name].[hash][ext]', // 图片使用 Hash
},
},
],
},
plugins: [
// ... 其他插件 ...
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash].css', // CSS 文件使用 Contenthash
}),
],
};
8 babel 原理
babel 可以将代码转译为其他版本的 js 代码,并且对目标环境不支持的 api 自动 polyfill
。而 babel 实现这些功能的流程是 解析(parse)-转换(transfrom)-生产(generator)
解析:根据代码生成对应的 AST 结构
- 进行代码分析,将代码分割成 token 流(语法单元数组),再根据 token 流生成对应的
AST
- 进行代码分析,将代码分割成 token 流(语法单元数组),再根据 token 流生成对应的
转换:遍历
AST
节点并生成新的AST
节点生成:根据新的
AST
生成目标代码
9 hmr 原理
webpack 将静态资源托管在 WDS 上,而 WDS 又和浏览器通过 webSocket
建立联系,而当 webpack 监听到文件变化时,就会向浏览器推送更新并携带新的 hash
与之前的 hash
进行对比,浏览器接收到 hash
事件后,加载变更的增量模块并触发变更模块的 module.hot.accept
回调执行变更逻辑。
10 tree shaking 原理
Tree-Shaking 是一种基于 ES Module 规范的优化打包产物技术,它会在运行过程中静态分析模块之间的导入导出,确定 ESM 模块中哪些导出值未曾其它模块使用,并将其删除,以此实现打包产物的优化。
使用 Tree shaking的三必要条件
- 使用 ESM 规范编写模块代码
mode: 'production'
: 设置 webpack 的运行模式为生产模式,这是开启 tree-shaking 的前提条件。生产模式会自动开启其他优化选项,例如代码压缩、混淆等。optimization: { usedExports: true }
: 启用usedExports
选项,告诉 webpack 在构建过程中分析代码并标记每个模块中实际使用的导出内容,从而识别未使用的代码。
原理
- webpack 会分析代码的依赖关系,构建一个依赖图
- 通过分析代码中 import 语句,webpack 能够判断每个模块哪些导出内容是被实际使用的
- 未使用的导出内容会被标记为
unused exports
,并在最终的 bundle 中被移除。
标记的流程如下:
- make 阶段:收集模块导出变量并记录到模块依赖关系图中
- seal 阶段:遍历模块依赖关系图并标记那些导出变量有没有被使用
- 构建 阶段:利用 Terser 将没有被用到的导出语句删除
注意事项
- 避免无作用的重复赋值
- 确保你的库或依赖项的
package.json
文件中包含sideEffects: false
属性,表明该库不包含副作用 (side effect)。 - 使用
#pure
标记函数无副作用(这种做法在开源项目的源码中经常出现,如pinia、reactive....等) - 如果你使用的是 Babel 或其他 transpiler,你需要确保它们不会移除
import/export
语句。(babel-loader
需要将babel-preset-env
的modules
配置为false
) - 使用支持 Tree shaking 的包
- 优化导出值的粒度
//正确做法
const a = 'a';
const b = 'b';
export {
a,
b
}
//错误做法
export default {
a: 'a',
b: 'b'
}
11 构建性能优化
- 开发环境:开发环境我们需要的是更快的构建速度、模块热替换、更加友好的 Source map
- 通过
cache: { type: 'systemfile'}
开启缓存构建可以加快二次构建的效率 - 通过模块热替换可以做到局部更新变化,提高开发效率
- 根据设置
devtool: cheap-eval-source-map
在保证构建效率的同时又能进行代码调试 - 使用 Thread-loader 以多进程的方式运行资源加载逻辑
- 通过工具或者插件分析性能并优化
- 通过
- 生产环境:生产环境我们需要的是更小的体积,更稳定又快的性能
- 压缩代码:使用
UglifyJsPlugin
和ParallelUglifyPlugin
来压缩代码,利用css-loader-minimize)
来压缩css - 利用 CDN:可以使用 CDN 来加快对静态资源的访问,提高用户的使用体验
Tree Shaking
: 删除没用到的代码- 提取公共第三方库: 使用
SplitChunksPlugin
插件来进行公共模块抽取 - 使用 TerserWebpackPlugin 多进程执行代码压缩
- 压缩代码:使用
12 构建流程
构建流程
- 整合构建配置: 从配置文件和
Shell
语句中读取与合并并计算出最终的参数 - 开始编译: 用上一步得到的参数初始化
Complier
对象,加载所有配置的插件,执行compiler
对象的run
方法开始编译流程 - 确定入口: 根据
entry
找出入口文件 - 递归编译模块:从入口文件开始,根据配置的
loader
对模块进行转译,如果该模块还有依赖的模块,则递归对这些模块进行翻译,通过递归上述操作直到对所有模块都进行转译 - 依赖关系图: 在经过
Loader
翻译完所有模块后,得到了每个模块转译后的内容以及模块之间的依赖关系图 - 输出chunk: 根据依赖关系,生成 Chunk, 再把每个
Chunk
转换成一个单独的文件加入到输出列表中 - 写入文件系统: 根据输出项的配置,将文件内容写到文件系统
从资源转换角度看
compiler.make
阶段entry
文件以dependence
对象形式加入compilation
的依赖列表 ,dependence
对象记录了entry
的相关信息- 根据
dependency
创建 对应的module
对象,之后读入module
对应的文件内容, 调用loader-runner
对内容做转化, 转化结果若有对其他依赖则继续读入依赖资源, 重复此过程直到所有的依赖均被转换为module
compilation.seal
阶段- 遍历
module
集合, 根据entry
配置以及引入资源的方式, 将module
分配到不同的Chunk
Chunk
之间最终形成ChunkGraph
结构- 遍历
ChunkGraph
调用compilation.emitAssets
方法标记chunk
的输出规则, 及转换为assets
集合
- 遍历
compiler.emitAssets
阶段- 将
assets
写入文件系统
- 将
13 Webpack 优化
- 使用高版本的 webpack 和 node
- 多进程构建(转译):使用
thread-loader
- 使用 Tree shaking 删除多余模块导出
- 配置
optimization.usedExports
为true
启动标记功能 - 配置
mode = production
- 配置 `optimization.minimize = true
- 配置
- 使用 Scope Hoisting 合并模块
- Scope Hoisting用于将符合条件的多个模块合并到同一个函数空间中,从而减少产物体积,优化性能。
- 开启方法:
mode = 'production'
开启生产模式- 使用
optimization.concatenateModules
配置项 - 使用
ModuleConcatenationPlugin
插件
- 开启模块热替换
- 可以通过
devServer:{hot:true}
或者 - 忽略部分很少变化的大文件如 node_modules 提高构建效率
- 可以通过
- 监控产物体积
- 监控产物体积可以帮助我们分析项目的性能,避免项目体积过大带来的资源消耗
- 通过 performance 配置项来自定义各种阈值或参数
- 缩小文件的搜索范围
- 优化 loader 配置:可以通过
test/ include / exclude
来指定文件的loader命中的文件范围,可以通过指定include
来使 loader 只处理那些需要被处理的模块 - 优化
resolves.modules
配置:用于指定 webpack 去哪些路径下寻找第三方模块- 例如当所有第三方模块都放在 node_modules时可以配置
resolve: {modules: path.resolve(__dirname, 'node_modules')}
- 例如当所有第三方模块都放在 node_modules时可以配置
- 优化
resolve.mainFilelds
配置:用于配置第三方模块使用哪个入口文件 -为了减少搜索范围,可以使用resolve: {mainFields: ['main']}
- 如果想优先使用ESModule版本的话,设置
resolve: {mainFields: ['jsnext:main', 'main']}
- 如果想优先使用ESModule版本的话,设置
- 配置
resolve.alias
:resolve.alias
通过别名将原导入路径映射成一个新的导入路径 - 配置
resolve.extensions
: 引入文件时省略数组内的后缀名 - 配置
resolve.noParse
: 省略对指定文件的处理
- 优化 loader 配置:可以通过
- 代码压缩
- 使用
terser-webpack-plugin
压缩ES6代码 - 使用
ParalleUglifyPlugin
多进程压缩代码 - 使用
css-minimize-webpack-plugin
对css代码进行压缩 - 使用
html-minimizer-webpack-plugin
压缩html代码
- 使用
- 使用 CDN 加速
- 将静态资源存储在 CDN 上,同时配置各自的 publicPath,可以加快对静态资源的访问速度,减少流量消耗
- 通过
output.publicPath
设置 JavaScript 文件地址 - 通过
WebPlugin.stylePublicPath
设置 CSS 文件的地址 - 通过
css-loader.publicPath
设置被 CSS 导入的资源的地址
- 通过
- 将静态资源存储在 CDN 上,同时配置各自的 publicPath,可以加快对静态资源的访问速度,减少流量消耗
- 为不同的环境配置对应的配置文件
- 使用
webpack-merge
处理多环境配置文件
- 使用
- 使用缓存构建
- 配置
cache: {type: 'systemfile'}
开启构建缓存,可以大幅提高二次构建的速度
- 配置
- 使用 DllPlugin:使用 DllPlugin 进行分包,使用
DllReferencePlugin
引用mainfext.json
, 通过将一些很少变动的代码先打包成静态资源,避免重复编译来提高构建性能 - 提取公共代码
- 使用
splitChunkPlugin
提取公共代码,减少代码体积
- 使用
- 动态 Polyfill:使用
polyfill-service
只返回给用户需要的polyfill - 使用可视化工具来分析性能
- 使用 UnusedWebpackPlugin 分析未被使用到的文件
- 使用 Webpack Dashboard 以命令行的形式输出编译过程的各种信息
- 使用 Webpack Bundle Analyzer 分析重复的模块或者没被用到的模块
- 使用
--json=stats,json
将构建过程中的信息都输出到指定文件 - 使用 Webpack Analysis 官方提供的可视化分析工具
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin'); // 代码压缩
const { CleanWebpackPlugin } = require('clean-webpack-plugin'); // 清理输出目录
module.exports = {
mode: 'production', // 设置为 production 模式
entry: {
index: './src/index.js',
about: './src/about.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[chunkhash].js',
chunkFilename: '[name].[chunkhash].js',
clean: true, // 每次构建前清理 /dist 文件夹
},
cache: { // 开启缓存
type: 'filesystem',
buildDependencies: {
config: [__filename], // 指定依赖文件
},
},
optimization: {
minimize: true, // 开启代码压缩
minimizer: [
new TerserPlugin(), // 使用 TerserPlugin 进行压缩
],
splitChunks: { // 代码拆分
chunks: 'all',
cacheGroups: { // cacheGroups 中的 vendor 组会将所有来自 node_modules 的模块打包到一个名为 vendor.js 的文件中。
vendor: {
test: /[\\/]node_modules[\\/]/, // 匹配 node_modules 目录
name: 'vendor', // 输出的文件名
chunks: 'all', // 对所有类型的模块进行分割
},
},
},
usedExports: true, // 启用 unusedExports 选项
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
generator: {
filename: 'images/[name].[hash][ext]',
},
},
],
},
plugins: [
new CleanWebpackPlugin(),
// 以下代码用来配置 DllPlugin 分包
// vendor-manifest.json 和 vendor.dll.js 将会生成在 dist 文件夹下
new webpack.DllPlugin({
name: 'vendor',
path: path.join(__dirname, 'dist', 'vendor-manifest.json'),
}),
// 使用 vendor.dll.js 包含了 vendor 库
new webpack.DllReferencePlugin({
manifest: require('./dist/vendor-manifest.json'),
}),
new HtmlWebpackPlugin({
filename: 'index.html',
template: './public/index.html',
chunks: ['index', 'vendor'], // 关联 index.html 和 index.js, vendor.dll.js 的 chunk
}),
new HtmlWebpackPlugin({
filename: 'about.html',
template: './public/about.html',
chunks: ['about', 'vendor'], // 关联 about.html 和 about.js, vendor.dll.js 的 chunk
}),
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash].css',
}),
],
};