跳到主要内容

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 转译为 es5

  • file-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 代码转换为 CSS

  • css-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-plugincss-minimize-webpack-plugin 的配合使用 即先使用mini-css-extract-plugin将css代码抽离成单独文件,之后使用 css-minimize-webpack-plugin对css代码进行压缩

  • copy-webpack-plugin: 在构建的时候,复制静态资源到打包目录。

  • compression-webpack-plugin: 生产环境采用gzip压缩JS和CSS

  • ParalleUglifyPlugin: 多进程并行压缩js

  • webpack-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:

  1. 导出一个函数: Loader 本质上是一个导出函数的 Node.js 模块。这个函数接收源代码作为参数。

Loader 函数主要接受以下参数:

  • source: 字符串类型,表示模块的源代码。这是 Loader 主要处理的对象,你可以对它进行解析、转换、添加或删除内容等操作。
  • map: 对象类型,可选参数,表示 SourceMap 信息,用于代码调试。如果你对源代码进行了转换,最好也更新 SourceMap,以便于调试。
  • meta: 对象类型,可选参数,用于传递自定义的元数据。例如,你可以通过 meta 传递一些解析器选项或者其他信息给下一个 Loader。
  1. 处理源代码: 在函数内部,你可以对源代码进行任何你想做的处理,例如转换语法,添加代码,或者替换内容等。

在 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 的路径和参数。
  1. 返回结果: 最后,你需要返回处理后的代码。

6 如何编写 plugin

// simple-plugin.js
class SimplePlugin {
apply(compiler) {
// 使用 compiler.hooks.done 钩子在编译完成后打印信息
compiler.hooks.done.tap('SimplePlugin', (stats) => {
console.log('编译完成!');
});
}
}

module.exports = SimplePlugin;

如何写 Plugin:

  1. 创建一个类: Plugin 是一个拥有 apply 方法的类。
  2. 注册钩子:apply 方法中,你可以使用 compiler 对象注册各种钩子函数,以便在 Webpack 构建过程的不同阶段执行自定义逻辑。
  3. 编写钩子函数: 钩子函数接收 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 文件指纹

  1. 概念:打包产物的文件后缀 hash

  2. 作用:

在发布版本时,通过文件指纹来区分修改的文件和未修改的文件;浏览器通过文件指纹是否改变来决定使用缓存文件还是请求新文件。

  1. 种类:
  • 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
  • 转换:遍历 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 中被移除。

标记的流程如下:

  1. make 阶段:收集模块导出变量并记录到模块依赖关系图中
  2. seal 阶段:遍历模块依赖关系图并标记那些导出变量有没有被使用
  3. 构建 阶段:利用 Terser 将没有被用到的导出语句删除

注意事项

  • 避免无作用的重复赋值
  • 确保你的库或依赖项的 package.json 文件中包含 sideEffects: false 属性,表明该库不包含副作用 (side effect)。
  • 使用 #pure标记函数无副作用(这种做法在开源项目的源码中经常出现,如pinia、reactive....等)
  • 如果你使用的是 Babel 或其他 transpiler,你需要确保它们不会移除 import/export 语句。(babel-loader需要将 babel-preset-envmodules 配置为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 以多进程的方式运行资源加载逻辑
    • 通过工具或者插件分析性能并优化
  • 生产环境:生产环境我们需要的是更小的体积,更稳定又快的性能
    • 压缩代码:使用 UglifyJsPluginParallelUglifyPlugin来压缩代码,利用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.usedExportstrue 启动标记功能
    • 配置 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')}
    • 优化 resolve.mainFilelds 配置:用于配置第三方模块使用哪个入口文件 -为了减少搜索范围,可以使用resolve: {mainFields: ['main']}
      • 如果想优先使用ESModule版本的话,设置resolve: {mainFields: ['jsnext:main', 'main']}
    • 配置resolve.alias:resolve.alias通过别名将原导入路径映射成一个新的导入路径
    • 配置resolve.extensions : 引入文件时省略数组内的后缀名
    • 配置resolve.noParse : 省略对指定文件的处理
  • 代码压缩
    • 使用 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 导入的资源的地址
  • 为不同的环境配置对应的配置文件
    • 使用 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',
}),
],
};