1. 优化产物
优化打包优先方案: 升级 node、升级 webpack、升级你的编译环境的硬件水平
始终使用最新 Webpack 版本,这算的上是性价比最高的优化手段之一了! 从 Webpack V3,到 V4,再到最新的 V5 版本,虽然构建功能在不断叠加增强,但性能反而不断得到优化提升, 这得益于 Webpack 开发团队始终重视构建性能,在各个大版本之间不厌其烦地重构核心实现,例如:
- V3 到 V4 重写 Chunk 依赖逻辑,将原来的父子树状关系
调整为
ChunkGroup
表达的有序图关系,提升代码分包效率; - V4 到 V5 引入
cache
功能,支持将模块、模块关系图、产物等核心要素 持久化缓存到硬盘,减少重复工作。
A、 webpack可采用做法一
大致方向:优化搜索时间、缩小文件搜索范围、减少不必要的编译等方面
1. 使用 Tree Shaking 清除未引用代码
2. 启用压缩(Uglification) 如删除未使用的代码、缩短变量名等
3. 代码分割(Code Splitting) 可以把代码分成多个 bundle,然后按需加载,从而减少初始加载时间
4. 使用 Externals 减轻体积
将其中常用的第三库(比如vue、vue-router、vuex等)抽离出来,
放置在CDN中,通过<script>来引入,减少打包文件体积
`externals`:用于声明外部资源,Webpack 会直接忽略这部分资源,
跳过这些资源的解析、打包操作
比如echarts、element-ui其实都非常的大,externals 出去 提高编译时间
5. 利用缓存(Caching) [contenthash] 替换 [hash] 或 [chunkhash] 来为输出文件命名,只有当文件内容改变时,文件名称才改变
6. 移除未使用的 CSS 自动去除未使用的 CSS
7. 优化图片 image-webpack-loader等图片压缩插件,可以减小图片文件的体积
B、 webpack可采用做法二
优化 loader 配置
合理使用 resolve.extensions // 解析到文件时自动添加拓展名 : extensions:[".warm",".mjs",".js",".json"]
优化 resolve.modules //
优化 resolve.alias // 别名配置
使用 DLLPlugin 插件 //vue-cil弃用 **webpack 4 有着比 dll 更好的打包性能**, dll其实是缓存
使用 cache-loader
terser 启动多线程 (并行文件加载:
- 多实例并行构建场景建议使用 Parallel-Webpack 实现并行;
- 生产环境下还可配合 `terser-webpack-plugin` 的并行压缩功能,提升整体效率。)
合理使用 sourceMap
优化打包chunk-vendors.js, splitChunks: minSize: 20000, // 依赖包超过20000bit将被单独打包
生产环境的 source map,可以将其设置为 false 以加速生产环境构建,默认值是true
sourceMap: false, //关掉sourcemap 会生成对于调试的完整的.map文件,但同时也会减慢打包速度
parallel: true, //使用多进程并行运行来提高构建速度。默认并发运行数:os.cpus().length - 1。
开启gizp压缩: compression webpack plugin
好文:www.cnblogs.com/ypSharing/p…
压缩CSS
压缩CSS:css-loader?minimize、PurifyCSSPlugin:
需要配合 extract-text-webpack-plugin 使用,
它主要的作用是可以去除没有用到的CSS代码,
类似JS的Tree Shaking
webpack4用mini-css-extract-plugin更优, 替代extract-text-webpack-plugin
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
//...
module: {
rules: [
{
test: /.css$/,
// 注意,这里用的是 `MiniCssExtractPlugin.loader` 而不是 `style-loader`
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
],
},
optimization: {
minimize: true,
minimizer: [
// Webpack5 之后,约定使用 `'...'` 字面量保留默认 `minimizer` 配置
"...",
new CssMinimizerPlugin(),
],
},
// 需要使用 `mini-css-extract-plugin` 将 CSS 代码抽取为单独文件
// 才能命中 `css-minimizer-webpack-plugin` 默认的 `test` 规则
plugins: [new MiniCssExtractPlugin()],
};
1.
css 独立拆包最大的好处就是 js 和 css 的改动,不会影响对方。
比如我改了 js 文件并不会导致 css 文件的缓存失效
而且现在它自动会配合optimization.splitChunks的配置,可以自定义拆分 css 文件
- `mini-css-extract-plugin` 库同时提供 Loader、Plugin 组件,需要同时使用
- `mini-css-extract-plugin` 不能与 `style-loader` 混用,否则报错,所以上述示例中第 9 行需要
判断 `process.env.NODE_ENV` 环境变量决定使用那个 Loader
- `mini-css-extract-plugin` 需要与 `html-webpack-plugin` 同时使用,才能将产物路径以 `link` 标签方式插入到 html 中
2. optimize-css-assets-webpack-plugin 这个插件,它不仅能帮你压缩 css 还能优化你的代码
webpack4 :
//配置
optimization: {
minimizer: [new OptimizeCSSAssetsPlugin()];
}
2. 文件指纹
webpack 中有三种生成哈希值规则的方式,可以用来区分文件是否修改。
- hash 与整个项目有关,项目里有文件修改,所有文件的哈希值都会变化。
- chunkhash 与入口有关,同一入口的文件被视为一个整体,当其中一个文件修改时,同入口的所有文件哈希值发生改变。
- contenthash 只与文件内容有关,文件内容发生改变,才会更改该文件的哈希值
文件指纹
A、
1. hash没有缓存
2. chunkhash : 有缓存但是如果修改css, 也会重新打包
若对应css改变,则构建出来的bundle文件的的chunkhash也会随之改变,但入口文件xx.js的内容并没有改变,所以没有完全达到缓存意义。
3. contenthash :更优的缓存
contenthash 你可以简单理解为是 moduleId + content 所生成的 hash
B、
给css也加contenthash
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
],
C、
通过file-loader或url-loader来处理图片,这时设置图片的hash,需要注意的是[hash]表示的是根据文件内容生成hash
`file-loader`、`url-loader`、`raw-loader` 都并不局限于处理图片,
它们还可以被用于加载任意类型的多媒体或文本文件,使用频率极高,几乎已经成为标配组件!
所以 Webpack5 直接内置了这些能力,开箱即可使用
3、Loader 和 Plugin 的区别
Loader Plugin
典型应用 处理 CSS、图片、字体等资源 代码压缩、资源管理、环境变量注入等
配置方式 在 module.rules 中配置 在 plugins 中配置
作用 转换模块的源代码 扩展 Webpack 的功能
输入/输出 接收源文件,返回转换后的内容 不直接处理文件,而是操作构建过程
Loader:用于转换模块的源代码。适合处理文件级别的任务(如 CSS、图片、字体等)。通过 module.rules 配置。
Plugin:用于扩展 Webpack 的功能。适合处理构建过程级别的任务(如代码压缩、资源优化等)。通过 plugins 配置。
style-loader: 将css添加到DOM的内联样式标签style里
css-loader :允许将css文件通过require的方式引入,并返回css代码
less-loader: 处理less
sass-loader: 处理sass
postcss-loader: 用postcss来处理CSS
autoprefixer-loader: 处理CSS3属性前缀,已被弃用,建议直接使用postcss
file-loader: 分发文件到output目录并返回相对路径
url-loader: 和file-loader类似,但是当文件小于设定的limit时可以返回一个Data Url
html-minify-loader: 压缩HTML
babel-loader :用babel来转换ES6文件到ES
https://blog.csdn.net/weixin_44869002/article/details/105831784
sourse map 可以知道文件位置
css.sourceMap时,我们最好关闭css.extract
Plugin:
Webpack 插件在代码形态上是一个带 apply
方法的对象,
我们可以在 apply
函数中注册各式各样的 Hook 回调,
监听对应事件,之后在回调中修改上下文状态,达到干预 Webpack 构建逻辑的效果
Webpack 虽然功能非常复杂,但本质上还是一个 Node 程序
Webpack Hook 底层的 Tapable 框架
- 初始化阶段:修整配置参数,创建 Compiler、Compilation 等基础对象,
并初始化插件及若干内置工厂、工具类,
并最终根据
entry
配置,找到所有入口模块; - 构建阶段:从
entry
文件开始,调用loader
将模块 转译为 JavaScript 代码,调用 [Acorn] 将代码转换为 AST 结构,遍历 AST 从中找出该模块依赖的模块; 之后 递归 遍历所有依赖模块,找出依赖的依赖,直至遍历所有项目资源后, 构建出完整的 **[模块依赖关系图] - 生成阶段:根据
entry
配置,将模块组装为一个个 Chunk 对象, 之后调用一系列 Template 工厂类翻译 Chunk 代码并封装为 Asset,最后写出到文件系统
「初始化」的重点是根据用户配置设置好构建环境; 「构建阶段」则重在解读文件输入与文件依赖关系; 最后在「生成阶段」按规则组织、包装模块,并翻译为适合能够直接运行的产物包。
为什么需要loader:
loader 本身是一个函数 webpack开箱即用只支持JS和JSON两种文件类型,需要loader将其他类型的文件转为webpack能够处理的文件类型,并添加到依赖图中完成打包。
Plugins: juejin.cn/post/691017… 插件就是一个含有apply方法的类,在apply方法中会传入compiler(即webpack实例)。 我们可以通过调用compiler中暴露的钩子函数,完成在webpack构造过程的相关功能。
class MyExampleWebpackPlugin { apply(compiler) { compiler.hooks.emit.tapAsync( 'MyExampleWebpackPlugin', (compilation, callback) => { ... callback(); } ); } }
Webpack配置文件中,通过entry设置编译入口文件,在output设置编译后的文件目录,通过loader引入其他类型文件,通过plugin来实现一些loader无法达到的功能。
filename:对应于entry里面生成出来的文件名。 chunkFilename:chunkFilename就是未被列在entry中,但有些场景需要被打包出来的文件命名配置。比如按需加载(异步)模块的时候。
Lodash-ES
思路 1:使用 Lodash-ES 替代 Lodash
lodash-es 是 lodash 的 es modules 版本 ,是着具备 ES6 模块化的版本,体积小
如果是使用webpack来进行打包的话,
我们在使用lodash库时, 尽量通过lodash-es来进行导入操作,可以减轻最终生产环境的代码量
思路 2: 使用 babel-plugin-lodash
插件 babel-plugin-lodash 和 lodash-webpack-plugin 能够在打包时去掉不必要的 lodash 代码,减小产物体积。
为了在不修改现有代码的情况下实现按需加载,可以使用 babel-plugin-lodash 插件。
该插件的原理是将 import _ from 'lodash' 转换为按需引入的方式,
例如 import { deepClone } from 'lodash/deepClone'
在 Vite 的配置文件中(通常是 vite.config.js),你需要集成 Babel 插件。
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import babel from '@rollup/plugin-babel';
export default defineConfig({
plugins: [
vue(),
babel({
babelHelpers: 'bundled',
presets: ['@babel/preset-env'],
plugins: ['babel-plugin-lodash'],
extensions: ['.js', '.jsx', '.ts', '.tsx', '.vue'],
exclude: 'node_modules/**', // 排除 node_modules 目录下的文件
}),
],
});
babelHelpers: 'bundled' 是指定 Babel 如何处理辅助函数的选项。bundled 选项告诉 Babel 将辅助函数包含在捆绑包中。
presets: ['@babel/preset-env'] 是 Babel 的预设配置,帮助处理 ES6+ 语法。
在开发阶段保留原始的引入方式,避免构建速度下降。
在生产环境中,通过 Babel 插件进行按需加载,以减小最终打包的体积。
链接:https://juejin.cn/post/7439324075116642355
在整个优化过程中,我们总结了以下几点经验:
逐步替换,分阶段实施:面对大规模的代码替换,不要一蹴而就,而是分阶段实施,并在每个阶段进行详细测试。
自动化测试的重要性:在大规模代码变更后,自动化测试能帮助我们快速验证功能的正确性,避免因手动测试不彻底而导致的上线问题。
工具链的理解与合理使用:深刻理解工具(如 Babel)的工作原理,有助于我们在做技术选型时做出更加合适的决定。
4、使用webpack如何替换字符串
方案 适用场景 配置复杂度 性能影响 插件依赖
DefinePlugin 简单宏替换 ★★☆ ★★ 无
ReplacePlugin 复杂模式匹配 ★★★ ★★ ✔️
(replace-webpack-plugin)
(string-replace-webpack-plugin)
HtmlWebpackPlugin HTML 文件占位符替换 ★★☆ ★★ ✔️
Environment Variables 环境变量注入 ★★☆ ★★ ✔️(dotenv)
Custom Loader 特定文件深度处理 ★★★★ ★★★ ✔️
5、Webpack 和 vite 的原理 以及热更新
(热更新就是更新受影响的模块,但是不刷新页面)
webpack
1. Webpack 核心是项目整体打包,将多个文件打包成一个
首次加载时间过长,bundle文件
webpack热更新复杂,要对css,js不同类型的模块针对性不同配置
热更新慢可能的原因:
-
没有使用合理的 Devtool souce map 导致
-
没有正确使用 exclude/include 处理了不需要处理的如node_modules
-
在开发环境不要压缩代码UglifyJs、提取 css、babel polyfill、计算文件 hash 等不需要的操作
Webpack HMR 特性的执行过程并不复杂,核心:
- 使用
webpack-dev-server
(后面简称 WDS)托管静态资源,同时以 Runtime 方式注入一段处理 HMR 逻辑的客户端代码; - 浏览器加载页面后,与 WDS 建立 WebSocket 连接;
- Webpack 监听到文件变化后,增量构建发生变更的模块,并通过 WebSocket 发送
hash
事件; - 浏览器接收到
hash
事件后,请求manifest
资源文件,确认增量变更范围; - 浏览器加载发生变更的增量模块;
- Webpack 运行时触发变更模块的
module.hot.accept
回调,执行代码变更逻辑; - done。 2. vite核心是基于esm的开发服务器,浏览器请求资源,对应发送资源,启动快,
Webpack 的 HMR 特性底层有两个重点,
一是监听文件变化并通过 WebSocket 发送变更消息;
二是需要客户端配合,通过 module.hot.accept
接口定制特定模块的热替换规则。
Webpack 的热更新基于文件监听和模块依赖图,
适合需要兼容性和复杂构建逻辑的项目, 但随着项目规模增大,更新速度会变慢。
Vite
Vite 的热更新基于 ESM 和浏览器原生模块系统,更新速度极快,适合现代浏览器环境的大型项目。
生产环境中,Vite 使用 Rollup 进行打包。Rollup 同样支持将 CommonJS 模块转换为 ESM。
开发环境:使用 ESBuild 动态将 CommonJS 模块转换为 ESM。
生产环境:使用 Rollup 和 @rollup/plugin-commonjs 插件打包 CommonJS 模块。
生产构建会进行预构建,就是将非esm转为esm
esm浏览器原生支持:通过 <script type="module"> 加载。
esbuild 快得惊人,并且已经是一个在构建库方面比较出色的工具,
但一些针对构建应用的重要功能仍然还在持续开发中 —— 特别是代码分割和 CSS 处理方面
所以生产环境用Rollup 在应用打包方面更加成熟和灵活
6. Vue 3 的 Treeshaking
未使用的导出、未引用的代码、副作用无关的代码
1. 启用 Tree Shaking
// vite.config.js
export default {
optimizeDeps: {
include: ['vue'], // 确保 Vue 核心库被预优化
exclude: ['unused-package'], // 排除未使用的依赖
},
};
若通过 import * as 导入整个模块,Tree Shaking 无法识别具体使用了哪些内容。
解决方案:始终按需导入(如 import { func } from 'module')。
// webpack.config.js (Vue 3 项目)
module.exports = {
configureWebpack: {
output: {
moduleFilename: 'esm.js', // 强制使用 ES6 Modules 格式
},
experiments: {
outputModule: true, // Webpack 5+ 需要此配置
},
},
};
2. 最佳实践:
使用 ES6 Modules 按需导入。
配合 TypeScript 获取更精准的静态分析。
利用动态导入(import())实现代码分割。
通过构建工具(如 Vite/Webpack)启用 Tree Shaking。
3. 副作用
大致可以理解成:一个函数会、或者可能会对函数外部变量产生影响的行为
eg:
function go (url) {
window.location.href = url
}
"sideEffects": [
"./src/some-module.js"
]
"sideEffects": false , 都没有副作用,可以树摇
4. Dead Code
未使用的导出、未引用的代码、副作用无关的代码
// eg: data.js
const data = [1, 2, 3];
export function getData() {
return data;
}
const result = data.reduce((acc, val) => acc + val, 0);
console.log(result);
虽然 reduce 函数被调用了,并且计算出了一个结果,但这个结果并没有被导出或者在其他地方使用。
因此,这段代码对程序的行为没有任何影响(即没有副作用)
https://juejin.cn/post/7276696853893546036
5. babel使tree-sharking失效
默认 webpack 是支持Tree-Shaking的,但在你的项目中可能会因为babel的原因导致它失效
Tree Shaking这个功能是基于ES6 modules 的静态特性检测,来找出未使用的代码,
所以如果你使用了 babel 插件的时候,如:babel-preset-env,
它默认会将模块打包成commonjs,这样就会让Tree Shaking失效了。
6.启动 Tree Shaking 功能必须同时满足两个条件
在 Webpack 中,:
- 配置
optimization.usedExports
为true
,标记模块导入导出列表; - 启动代码优化功能,可以通过如下方式实现:
- 配置
mode = production
- 配置
optimization.minimize = true
- 提供
optimization.minimizer
数组
- 配置
// webpack.config.js module.exports = { mode: "production", optimization: { usedExports: true, }, };
Webpack 会对所有使用 ESM 方案的模块启动 Tree-Shaking
7. Tree-shaking 的实现
一是需要先 「**标记**」 出模块导出值中哪些没有被用过;
二是使用代码压缩插件 —— 如 [Terser]删掉这些没被用到的导出变量。
> 标记功能需要配置 `optimization.usedExports = true` 开启
标记的效果就是删除那些没有被其它模块使用的“**导出语句**”
ESM 下模块之间的依赖关系是高度确定的,与运行状态无关,
编译工具只需要对 ESM 模块做静态分析,就可以从代码字面量中推断出
哪些模块值未曾被其它模块使用,
这是实现 Tree Shaking 技术的必要条件
8. 实践
与赋值语句类似,JavaScript 中的函数调用语句也可能产生副作用
Webpack 并不会对函数调用做 Tree Shaking 操作
添加 `/*#__PURE__*/` 备注,明确告诉 Webpack 该次函数调用并不会对上下文环境产生副作用
实践:始终使用 ESM
实践:避免无意义的赋值
实践:使用 `#pure` 标注纯函数调用
实践:禁止 Babel 转译模块导入导出语句
实践:优化导出值的粒度
实践:使用支持 Tree Shaking 的包: 使用 `lodash-es` 替代 `lodash`
实践:在异步模块中使用 Tree-Shaking
Tree-Shaking 并不能完美删除所有无效的模块导出,
需要我们在业务代码中遵循若干最佳实践规则,帮助 Tree-Shaking 更好地运行
7、 Sourcemap
核心价值:在优化构建和高效调试之间构建桥梁
Sourcemap 是一种高效的位置映射算法,它将产物到源码之间的位置关系表达为
`mappings` 分层设计与 VLQ 编码,
再通过 Chrome、Safari、VS Code、Sentry
等工具异地还原为接近开发状态的源码形式
[VLQ] “只存差值+压缩成整数”
是一种将整数数值转换为 Base64 的编码算法,
它先将任意大的整数转换为一系列六位字节码,再按 Base64 规则转换为一串可见字符。
在开发环境中,我们需要实时重新加载或热模块替换能力的server,相对完整的source map。
但是在生产环境中,我们更加关注更小的bundle(压缩输出), 更轻量的source map, 还有更优化的资源等
开发环境,基础构建需要重新加载,热更新功能,需要尽可能完整的source map
生产环境,基础构建需要压缩文件,文件指纹功能,需要尽可能小的source map
当 `devtool` 包含 `cheap` 时,生成的 Sourcemap 内容会抛弃**列**维度的信息
module : cheap-module-eval-source-map显示loader前的代码,
cheap-source-map 则是经过 babel-loader 编译处理的内容
- 对于开发环境,适合使用:
- `eval`:速度极快,但只能看到原始文件结构,看不到打包前的代码内容;
- `cheap-eval-source-map`:速度比较快,可以看到打包前的代码内容,但看不到 loader 处理之前的源码;
- `cheap-module-eval-source-map`:速度比较快,可以看到 loader 处理之前的源码,不过定位不到列级别;
- `eval-source-map`:初次编译较慢,但定位精度最高;
- 对于生产环境,则适合使用:
- `source-map`:信息最完整,但安全性最低,外部用户可轻易获取到压缩、混淆之前的源码,慎重使用;
- `hidden-source-map`:信息较完整,安全性较低,外部用户获取到 `.map` 文件地址时依然可以拿到源码;
- `nosources-source-map`:源码信息缺失,但安全性较高,需要配合 Sentry 等工具实现完整的 Sourcemap 映射
8、babel
ES6/ES7的代码转化成指定浏览器能支持的代码
babel-runtime 是一个包含 babel 模块化运行时助手的库。
babel-runtime 的主要作用就是
将这些可能被重用的代码抽取成单独的模块,以避免在每个文件中重复出现相同的代码。
它通过模块导入的方式引入这些功能,从而避免了对全局作用域的修改或污染
使用 babel-runtime 通常需要配合 babel-plugin-transform-runtime 插件一起使用
9、CommonJS 和 ES Module
1、CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
2、CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
3、CommonJs 是单个值导出,ES6 Module可以导出多个
4、CommonJs 是动态语法可以写在判断里,ES6 Module 静态语法只能写在顶层
5、CommonJs 的 this 是当前模块,ES6 Module的 this 是 undefined
ES Module :
1. 编译时加载:esm 是编译时加载,也就是只有所有import的模块都递归加载完成,才会开始执行
2. 确定的导入导出
3. 无副作用的模块导入
ESM 下模块之间的依赖关系是高度确定的,与运行状态无关,编译工具只需要对 ESM 模块做静态分析
ES6 Modules 不支持循环引用(编译时报错)
10 、webpack4内置的代码分割策略:
新的 chunk 是否被共享或者是来自 node_modules 的模块
新的 chunk 体积在压缩之前是否大于 30kb
(个人理解:文件压缩后会更小,如果只有几个页面用到,没必要拆出来,拆出来会多耗费一次http请求,直接打包在一起。
但是如果每个页面都用到这个,就可以打包合并成一个component-vendor.js的包)
按需加载 chunk 的并发请求数量小于等于 5 个
页面初始加载时的并发请求数量小于等于 3 个
作者:花裤衩
链接:https://juejin.cn/post/6844903652956585992
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
我们现在的策略是按照体积大小、共用率、更新频率重新划分我们的包,使其尽可能的利用浏览器缓存
其实优化就是一个博弈的过程,是让 a bundle 大一点还是 b? 是让首次加载快一点还是让 cache 的利用率高一点?
但有一点要切记,拆包的时候不要过分的追求颗粒化,什么都单独的打成一个 bundle,
不然你一个页面可能需要加载十几个.js文件,如果你还不是HTTP/2的情况下,请求的阻塞还是很明显的
(受限于浏览器并发请求数)。所以还是那句话资源的加载策略并没什么完全的方案,
都需要结合自己的项目找到最合适的拆包策略。
拆包策略:
基础类库 chunk-libs
UI 组件库 chunk-elementUI
自定义共用组件/函数 chunk-commons
低频组件 chunk-eachrts/chunk-xlsx等
业务代码 lazy-loading xxxx.js
11、Chunk
https://juejin.cn/post/6844903889393680392
Webpack的打包是从一个入口文件开始,也可以说是入口模块, 入口模块引用这其他模块,模块再引用模块。 Webpack通过引用关系逐个打包模块,这些module就形成了一个Chunk。
Chunk是过程中的代码块,Bundle是结果的代码块。
产生Chunk的三种途径 entry入口 异步加载模块 代码分割(code spliting)
Webpack 默认会将以下三种模块做分包处理:
- Initial Chunk:
entry
模块及相应子模块打包成 Initial Chunk; - Async Chunk:通过
import('./xx')
等语句导入的异步模块及 相应子模块组成的 Async Chunk; - Runtime Chunk:运行时代码抽离成 Runtime Chunk,可通过 [entry.runtime]配置项实现。
模块重复打包、资源冗余 & 低效缓存
Webpack 专门提供了 SplitChunksPlugin
插件,
用于实现更灵活、可配置的分包,提升应用性能
12、webpack插件
TerserWebpackPlugin,还有其他一些替代方案可以用来压缩和最小化 JavaScript 代码, 例如 UglifyJSWebpackPlugin 和 CleanCSSWebpackPlugin
之前压缩使用的UglifyjsWebpackPlugin,但其不支持ES6+语法,所以替换为TerserPlugin
webpack-obfuscator
- uglifyjs-webpack-plugin ( webpack4 不需要uglifyjs-webpack-plugin) 压缩 JavaScript 代码的 Webpack 插件 配置选项说明: test: 匹配需要压缩的文件。 exclude: 排除不需要压缩的文件或目录。 cache: 是否启用缓存,提高构建速度。 parallel: 是否启用并行压缩,提高压缩效率。 sourceMap: 是否生成 source map 文件。 uglifyOptions: UglifyJS 的具体配置选项,如压缩选项、输出选项等。
打包时去除打印信息
webpack4 不需要uglifyjs-webpack-plugin了 production 模式下,由于提供了splitChunks和minimize,所以基本零配置, 代码就会自动分割、压缩、优化,同时 webpack 也会自动帮你 Scope hoisting 和 Tree-shaking
module.exports = {
optimization: {
minimizer: [
new UglifyJsPlugin({
cache: true,
parallel: true,
}),
],
},
};
-
经过SideEffectsFlagPlugin处理后, 没有副作用且没有被使用的模块都会被打上sideEffectFree标记
// webpack.pord.config.js module.exports = { optimization: { sideEffects: true } };
-
terser-webpack-plugin来进行压缩
// webpack.pord.config.js module.exports = { optimization: { minimize: true } }
// js压缩 const TerserPlugin = require('terser-webpack-plugin'); module.exports = { optimization: { minimizer: [ new TerserPlugin({ cache: true, parallel: true, // 多进程并发运行 sourceMap: true, // Must be set to true if using source-maps in production terserOptions: { // github.com/webpack-con… } }), ] } }
多数情况下使用默认 Terser 配置即可 module.exports = { //... optimization: { minimize: true } }; 手动创建 [terser-webpack-plugin] 实例并传入压缩配置实现更精细的压缩功能, 比如去除打印,重复声明
-
开启gizp压缩 const CompressionWebpackPlugin = require('compression-webpack-plugin')//gzip压缩
new CompressionWebpackPlugin({
filename: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp('\\.(' + productionGzipExtensions.join('|') + ')$'),
threshold: 10240,
minRatio: 0.8,
})
5、 图片压缩【image-webpack-loader】使用 图像压缩是一种非常耗时的操作,建议只在生产环境下开启 6、 css-minimizer-webpack-plugin: new CssMinimizerPlugin(), css压缩需要使用插件CssMinimizerWebpack 7. speed-measure-webpack-plugin 这个插件,它能监控 webpack 每一步操作的耗时 8. html-loader 编译后产生的字符串, 会由 html-webpack-plugin 储存为 html 文件到输出目录 那些第三方资源的CDN,请注意先后顺序 通过 html-webpack-plugin注入到 index.html之中
9、[SpeedMeasureWebpackPlugin] 插件能够统计出各个 Loader、 插件的处理耗时,开发者可以根据这些数据分析出哪些类型的文件处理更耗时间 10. 使用 HtmlMinifierTerser 压缩 HTML React、Vue 等 MVVM 框架,这衍生出来的 一个副作用是原生 HTML 的开发需求越来越少,HTML 代码占比越来越低,所以大多数现代 Web 项目中其实并不需要考虑为 HTML 配置代码压缩工作流
十六、、 // 别名配置 Object.assign(config, { // 开发生产共同配置 resolve: { alias: { '@': path.resolve(__dirname, './src'), '@c': path.resolve(__dirname, './src/components'), '@p': path.resolve(__dirname, './src/pages') } } }),
config.output.filename = `[name].${Version}.${Timestamp}.js` //打包生成的文件
config.output.chunkFilename = `[name].${Version}.${Timestamp}.js`
//兼容性
// lib-flexible
postcss: {
plugins: [
//remUnit这个配置项的数值是多少呢??? 通常我们是根据设计图来定这个值,原因很简单,便于开发。
//假如设计图给的宽度是750,我们通常就会把remUnit设置为75,这样我们写样式时,可以直接按照设计图标注的宽高来1:1还原开发。
require('postcss-px2rem')({
remUnit: 37.5
})
]
}
13、webpack5特性-持久化缓存
1. [持久化缓存]
Webpack 5 最令人振奋的特性之一,它能够将首次构建的过程与结果数据持久化保存到本地文件系统,
在下次执行构建时跳过解析、链接、编译等一系列非常消耗性能的操作,
直接复用上次的 Module/ModuleGraph/Chunk 对象数据,迅速构建出最终产物。
cache: {
type: 'filesystem'
},
首次构建出的 Module、Chunk、ModuleGraph 等对象序列化后保存到硬盘中
后面再运行的时候,就可以跳过许多耗时的编译动作,直接复用缓存数据。
2. Webpack4:使用 `cache-loader`
修改配置,注意必须将 `cache-loader` 放在 `loader` 数组首位
`cache-loader` 只缓存了 Loader 执行结果,缓存范围与精度不如 Webpack5 内置
的缓存功能,所以性能效果相对较低
3. [hard-source-webpack-plugin]
也是一种实现缓存功能的第三方组件,
与 `cache-loader` 不同的是,它并不仅仅缓存了 Loader 运行结果,
还保存了 Webpack 构建过程中许多中间数据,包括:模块、模块关系、
模块 Resolve 结果、Chunks、Assets 等,效果几乎与 Webpack5 自带的 Cache 对齐
缓存路径:`node_module/.cache`
Webpack5 持久化缓存用法简单,且优化效果非常出色,确实是一个特别让人振奋的新功能,
甚至特定情况下能够让构建性能达到 Unbundle 方案的量级,
妥妥的 Webpack 性能优化利器!
而在 Webpack4 中,我们还可以借助下述组件实现缓存优化:
- `cache-loader`:针对 Loader 运行结果的通用缓存方案;
- `hard-source-webpack-plugin`:针对 Webpack 全生命周期的通用缓存方案;
- `babel-loader`:针对 Babel 工具的专用缓存能力;
- `eslint-loader`/`eslint-webpack-plugin`:针对 ESLint 的专用缓存方案;
- `stylelint-webpack-plugin`:针对 StyleLint 的专用缓存方案。
这些方案各有特色,但都无可置疑地能有效提升编译性能,建议你在尝试做性能优化时优先选用。
14、并行文件加载
- HappyPack 虽然确实能有效提升 Webpack 的打包构建速度,但它有一些明显的缺点:
- 作者已经明确表示不会继续维护,扩展性与稳定性缺乏保障, 随着 Webpack 本身的发展迭代,可以预见总有一天 HappyPack 无法完全兼容 Webpack;
- HappyPack 底层以自己的方式重新实现了加载器逻辑,源码与使用方法
都不如 Thread-loader 清爽简单,而且会导致一些意想不到的兼容性问题,
如
awesome-typescript-loader
; - HappyPack 主要作用于文件加载阶段,并不会影响后续的产物生成、合并、优化等功能, 性能收益有限。
- Thread-loader:
Thread-loader 放在
use
数组首位,确保最先运行
一些 Loader 无法与 Thread-loader 共同使用,大家需要仔细加以甄别、测试
- Parallel-Webpack: Thread-loader、HappyPack 这类组件所提供的并行能力都仅作用于文件加载过程, 对后续 AST 解析、依赖收集、打包、优化代码等过程均没有影响,理论收益还是比较有限的。 对此,社区还提供了另一种并行度更高, 以多个独立进程运行 Webpack 实例的方案 —— [Parallel-Webpack]
parallel-webpack 相对于 Thread-loader、HappyPack 有更高的并行度, 但进程实例之间并没有做任何形式的通讯, 这可能导致相同的工作在不同进程 —— 或者说不同 CPU 核上被重复执行
这种技术实现,对单 entry 的项目没有任何收益,只会徒增进程创建成本; 但特别适合 MPA 等多 entry 场景, 或者需要同时编译出 esm、umd、amd 等多种产物形态的类库场景
Webpack4 默认使用 [Uglify-js] 实现代码压缩,Webpack5 之后则升级为 [Terser]
- 对于 Webpack4 之前的项目,可以使用 HappyPack 实现并行文件加载;
- Webpack4 之后则建议使用 Thread-loader;
- 多实例并行构建场景建议使用 Parallel-Webpack 实现并行;
- 生产环境下还可配合
terser-webpack-plugin
的并行压缩功能,提升整体效率。
理论上,并行确实能够提升系统运行效率,但 Node 单线程架构下, 所谓的并行计算都只能依托与派生子进程执行, 而创建进程这个动作本身就有不小的消耗 —— 大约 600ms
对于小型项目,构建成本可能可能很低, 引入多进程技术反而导致整体成本增加, 因此建议大家按实际需求斟酌使用上述多进程方案
15.开发模式禁用产物优化
Webpack 提供了许多产物优化功能,例如:Tree-Shaking、SplitChunks、Minimizer 等, 这些能力能够有效减少最终产物的尺寸,提升生产环境下的运行性能,但这些优化在开发环境中意义不大,反而会增加构建器的负担(都是性能大户)。
因此,开发模式下建议关闭这一类优化功能,具体措施:
- 确保
mode='development'
或mode = 'none'
,关闭默认优化策略; optimization.minimize
保持默认值或false
,关闭代码压缩;optimization.concatenateModules
保持默认值或false
,关闭模块合并;optimization.splitChunks
保持默认值或false
,关闭代码分包;optimization.usedExports
保持默认值或false
,关闭 Tree-shaking 功能; 最终,建议开发环境配置如:
module.exports = { // ... mode: "development", optimization: { removeAvailableModules: false, removeEmptyChunks: false, splitChunks: false, minimize: false, concatenateModules: false, usedExports: false, }, };
最小化 watch
监控范围
跳过 TS 类型检查
优化 ESLint 性能
慎用 source-map
:
Webpack 提供了 devtool
选项,
可以配置 eval
、source-map
、cheap-source-map
等值,
不考虑其它因素的情况下,最佳实践:
- 开发环境使用 eval
,确保最佳编译速度;
- 生产环境使用 source-map
,获取最高质量。
设置 resolve
缩小搜索范围
Webpack5 中,resolve.extensions
默认值为 ['.js', '.json', '.wasm']
- 修改 `resolve.extensions` 配置项,减少匹配次数;
- 代码中尽量补齐文件后缀名;
- 设置 `resolve.enforceExtension = true` ,
强制要求开发者提供明确的模块后缀名,不过这种做法侵入性太强,不太推荐。
Webpack 这一逐层查找的逻辑大多数情况下实用性并不高,
开发者可以通过修改 `resolve.modules` 配置项,主动关闭逐层搜索功能
const path = require('path');
module.exports = {
//...
resolve: {
modules: [path.resolve(__dirname, 'node_modules')],
},
};
除了缓存、多进程构建这一类大杀器之外,还可以通过控制构建范围、能力等方式
尽可能减少各个环节的耗时,包括文中介绍的:
- 使用最新 Webpack、Node 版本;
- 约束 Loader 执行范围;
- 使用 `noParse` 跳过文件编译等。
如果下次再遇到性能问题,建议可以先试着分析哪些环节占用时长更多,
然后有针对性的实施各项优化。
16. 为什么要分包
Webpack 默认会将尽可能多的模块代码打包在一起,
优点是能减少最终页面的 HTTP 请求数,但缺点也很明显:
1. 页面初始代码包过大,影响首屏渲染性能;
2. 无法有效应用浏览器缓存,特别对于 NPM 包这类变动较少的代码,
业务代码哪怕改了一行都会导致 NPM 包缓存失效。
模块重复打包、资源冗余 & 低效缓存
为此,Webpack 提供了 `SplitChunksPlugin` 插件,
专门用于根据产物包的体积、引用次数等做分包优化,规避上述问题,特别适合生产环境使用。
17. SplitChunksPlugin
1. `optimization.splitChunks.cacheGroup` 概念,
用于对不同特点的资源做分组处理,并为这些分组设置更有针对性的分包规则
2. 提供一些开箱即用的分包特性:
- `node_modules` 资源会命中 `defaultVendors` 规则,并被单独打包;
- 只有包体超过 20kb 的 Chunk 才会被单独打包;
- 加载 Async Chunk 所需请求数不得超过 30;
- 加载 Initial Chunk 所需请求数不得超过 30
3. 设置分包范围:
- 字符串 `'all'` :对 Initial Chunk 与 Async Chunk 都生效,建议优先使用该值;
- 字符串 `'initial'` :只对 Initial Chunk 生效;
- 字符串 `'async'` :只对 Async Chunk 生效;
- 函数 `(chunk) => boolean` :该函数返回 `true` 时生效;
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'all',
},
},
}
设置为 `all` 效果最佳,此时 Initial Chunk、Async Chunk
都会被 `SplitChunksPlugin` 插件优化
4. 根据 Module 使用频率分包:
optimization: {
splitChunks: {
// 设定引用次数超过 2 的模块才进行分包
minChunks: 2
},
},
5. 限制分包数量
6. 限制分包体积
借助这些规则我们可以实现当包体过小时直接取消分包 —— 防止产物过"碎"
7. cacheGroups
缓存组的作用在于能为不同类型的资源设置更具适用性的分包规则,
一个典型场景是将所有
`node_modules` 下的模块统一打包到 `vendors` 产物,从而实现第三方库与业务代码的分离。
Webpack 提供了两个开箱即用的 `cacheGroups`,
分别命名为 `default` 与 `defaultVendors`,默认配置:
module.exports = {
//...
optimization: {
splitChunks: {
cacheGroups: {
default: {
idHint: "",
reuseExistingChunk: true,
minChunks: 2,
priority: -20
},
defaultVendors: {
idHint: "vendors",
reuseExistingChunk: true,
test: /[\\/]node_modules[\\/]/i,
priority: -10
}
},
},
},
};
- 将所有 `node_modules` 中的资源单独打包到 `vendors-xxx-xx.js` 命名的产物
- 对引用次数大于等于 2 的模块 —— 也就是被多个 Chunk 引用的模块,单独打包
`splitChunks` 规则比较复杂,大致上可以分类为:
- 规则类:如 `minSize/minChunks` 等,匹配这些条件的 Module 都会被单独分包;
- `cacheGroup`:可以理解为针对特定资源的次级规则集合。
分包策略的好坏直接影响应用的运行性能,
常用策略一是单独打包 `node_modules` 代码(习惯称为 `vendor`),
二是单独打包被频繁使用的模块
18. 代码压缩
`optimization.minimizer` 数组接入代码压缩插件,比较常用的插件有:
- `terser-webpack-plugin`:用于压缩 ES6 代码的插件;
- `css-minimizer-webpack-plugin`:用于压缩 CSS 代码的插件;
- `html-minifier-terser`:用于压缩 HTML 代码的插件。
mini-css-extract-plugin更优, 替代extract-text-webpack-plugin
- `mini-css-extract-plugin` 需要
与 `html-webpack-plugin` 同时使用,才能将产物路径以 `link` 标签方式插入到 html 中
// 需要使用 `mini-css-extract-plugin` 将 CSS 代码抽取为单独文件
// 才能命中 `css-minimizer-webpack-plugin` 默认的 `test` 规则
19、动态加载
- 使用动态加载,减少首屏资源加载量;
- 使用 `externals`、Tree-Shaking、Scope Hoisting 特性,减少应用体积;
- 正确使用 `[hash]` 占位符,优化 HTTP 资源缓存效率;
使用动态加载,减少首屏资源加载量; 多数情况下我们没必要为小模块使用动态加载能力 常见的用法是配合 SPA 的前端路由能力实现页面级别的动态加载
动态加载是 Webpack 内置能力之一,我们不需要做任何额外配置
就可以通过动态导入语句(`import`、`require.ensure`)轻易实现。
但请 注意,这一特性有时候反而会带来一些新的性能问题:
**一是过度使用会使产物变得过度细碎,产物文件过多,运行时 HTTP 通讯次数也会变多**,
在 HTTP 1.x 环境下这可能反而会降低网络性能,得不偿失;
**二是使用时 Webpack 需要在客户端注入一大段用于支持动态加载特性的 Runtime**:
20、雪碧图
雪碧图曾经是一种使用广泛的性能优化技术,但 HTTP2 实现 TCP 多路复用之后, 雪碧图的优化效果已经微乎其微 —— 甚至是反优化,可以预见随 HTTP2 普及率的提升, 未来雪碧图的必要性会越来越低,因此建议读者们了解作用与基本原理即可,不必深究。
- 开发环境需要使用 `webpack-dev-server` 实现 Hot Module Replacement;
- 测试环境需要带上完整的 Soucemap 内容,以帮助更好地定位问题;
- 生产环境需要尽可能打包出更快、更小、更好的应用代码,确保用户体验。
流程配置、性能优化类配置、日志类配置、开发效率类配置等,这里面较常用,需要着重学习的配置有:
entry
:声明项目入口文件,Webpack 会从这个文件开始递归找出所有文件依赖;output
:声明构建结果的存放位置;target
:用于配置编译产物的目标运行环境,支持web
、node
、electron
等值,不同值最终产物会有所差异;mode
:编译模式短语,支持development
、production
等值,Webpack 会根据该属性推断默认配置;optimization
:用于控制如何优化产物包体积,内置 Dead Code Elimination、Scope Hoisting、代码混淆、代码压缩等功能;module
:用于声明模块加载规则,例如针对什么类型的资源需要使用哪些 Loader 进行处理;plugin
:Webpack 插件列表。
21、Scope Hoisting “作用域提升”
Webpack 提供了 Scope Hoisting 功能,
用于 **将符合条件的多个模块合并到同一个函数空间** 中,从而减少产物体积,优化性能。
Scope Hoisting 底层基于 ES Module 方案的 [静态特性],
推断模块之间的依赖关系,并进一步判断模块与模块能否合并
实现原理:
Scope Hoisting 的实现原理其实很简单:分析出模块之间的依赖关系,
尽可能将打散的模块合并到一个函数中,前提是不能造成代码冗余。
因此只有那些被引用了一次的模块才能被合并。
由于 Scope Hoisting 需要分析出模块之间的依赖关系,
因此源码必须采用 ES6 模块化语句,不然它将无法生效。
失效情况:
1. 非 ESM 模块
2. 模块被多个 Chunk 引用
22. 性能优化
压缩、Tree-Shaking、Scope Hoisting 都在减少产物体积;
Code Splitting、外置依赖、[hash]
则有助于提升 HTTP 缓存效率;
动态加载则能够确保关键路径最小资源依赖。还可以监控产物体积
Webpack 的功能集非常庞大:模块打包、代码分割、按需加载、 Hot Module Replacement、文件监听、Tree-shaking、 Sourcemap、Module Federation、 Dev Server、DLL、多进程打包、Persistent Cache 等等 最最核心的功能依然是:静态模块打包能力
23. 内容处理阶段 (vue-loader)
插件处理完配置,webpack 运行起来之后,Vue SFC 文件会被多次传入不同的 Loader, 经历多次中间形态变换之后才产出最终的 js 结果,大致上可以分为如下步骤:
- 路径命中
/\.vue$/i
规则,调用vue-loader
生成中间结果 A; - 结果 A 命中
xx.vue?vue
规则,调用vue-loader
Pitch Loader 生成中间结果 B; - 结果 B 命中具体 Loader,直接调用 Loader 做处理
此时第一次执行 vue-loader ,执行如下逻辑:
- 调用
@vue/component-compiler-utils
包的parse函数,将SFC 文本解析为AST对象; - 遍历 AST 对象属性,转换为特殊的引用路径;
- 返回转换结果。
Pitch Loader 的逻辑比较简单,做的事情也只是转换 import 路径
我们可以将 vue-loader
的核心逻辑总结为:
- 首先给原始文件路径增加不同的参数,后续配合
resourceQuery
参数就可以分开处理这些内容, 这样的实现相比于一次性处理,逻辑更清晰简洁,更容易理解; - 经过 Normal Loader、Pitch Loader 两个阶段后,SFC 内容会
被转化为
import xxx from '!-babel-loader!vue-loader?xxx'
格式的 引用路径,以此复用用户配置。
-
Loader 主要负责将资源内容转译为 Webpack 能够理解、处理的标准 JavaScript 形式, 所以通常需要做 Loader 内通过
return
/this.callback
方式返回翻译结果; -
Loader Context 提供了许多实用接口,我们可以借助这些接口读取上下文信息, 或改变 Webpack 运行状态(相当于产生 Side Effect,例如通过
emitFile
接口); -
假若我们开发的 Loader 需要对外提供配置选项,建议使用
schema-utils
校验配置参数是否合法; -
假若 Loader 需要生成额外的资源文件,建议使用
loader-utils
拼接产物路径; -
执行时,Webpack 会按照
use
定义的顺序从前到后执行 Pitch Loader, 从后到前执行 Normal Loader,我们可以将一些预处理逻辑 放在 Pitch 中(如vue-loader
);
24. Runtime
Runtime 代码是指一些为了确保打包产物能正常运行,
而由 Webpack 注入的一系列基础框架代码
Webpack 动态生成的运行时代码,编译时,Webpack 会根据业务代码,
决定输出哪些支撑特性的运行时代码(基于 `Dependency` 子类)
例如:
- 需要 `__webpack_require__.f`、`__webpack_require__.r` 等
功能实现最起码的模块化支持;
- 如果用到动态加载特性,则需要写入 `__webpack_require__.e` 函数;
- 如果用到 Module Federation 特性,则需要写入 `__webpack_require__.o` 函数;
- `__webpack_modules__` 对象,包含了除入口外的所有模块,如示例中的 `a.js` 模块;
- `__webpack_module_cache__` 对象,用于存储被引用过的模块;
- `__webpack_require__` 函数,实现模块引用(require) 逻辑;
- `__webpack_require__.d` ,工具函数,实现将模块导出的内容附加的模块对象上;
- `__webpack_require__.o` ,工具函数,判断对象属性用;
- `__webpack_require__.r` ,工具函数,在 ESM 模式下声明 ESM 模块标识;
- 最后的 IIFE,对应 entry 模块即上述示例的 `index.js` ,用于启动整个应用。
它们协作构建起一个简单的模块化体系,从而实现 ES Module 规范所声明的模块化特性
上述函数、对象构成了 Webpack 运行时最基本的能力 —— 模块化
25. ModuleGraph、 ChunkGraph
「构建」阶段负责根据模块的引用关系构建 ModuleGraph;
「封装」阶段则负责根据 ModuleGraph 构建一系列 Chunk 对象,
并将 Chunk 之间的依赖关系(异步引用、Runtime)组织为
ChunkGraph —— Chunk 依赖关系图对象。
`ModuleGraph`:记录 Dependency Graph 信息的容器,
记录构建过程中涉及到的所有 `module`、`dependency` 对象,
以及这些对象互相之间的引用
「构建」阶段如何从 Entry 开始逐步递归读入、解析模块内容,
并最终构建出模块依赖关系图 —— ModuleGraph 对象。
「封装」阶段,根据 ModuleGraph 内容组织 Chunk,
并进一步构建出 ChunkGroup、ChunkGraph 依赖关系对象的主流程。
Chunk vs ChunkGroup vs ChunkGraph
`ChunkGraph`:最后,Webpack 会将 Chunk 之间、ChunkGroup
之间的依赖关系存储到 `compilation.chunkGraph` 对象中
Webpack5 内置的三种分包规则:Entry Chunk、Async Chunk 与 Runtime Chunk
Runtime Chunk:`entry.runtime` 不为空时,会将运行时模块单独组织成一个 Chunk。
26. 构建过程
compile 开始编译
make 从入口点分析模块及其依赖的模块,创建这些模块对象
build-module 构建模块
seal 封装构建结果
emit 把各个chunk输出到结果文件
- Webpack 构建过程可以简单划分为 Init、Make、Seal 三个阶段;
- Init 阶段负责初始化 Webpack 内部若干插件与状态,逻辑比较简单;
- Make 阶段解决资源读入问题,这个阶段会从 Entry —— 入口模块开始,递归读入、解析所有模块内容,并根据模块之间的依赖关系构建 ModuleGraph —— 模块关系图对象;
- Seal 阶段更复杂:
- 一方面,根据 ModuleGraph 构建 ChunkGraph;
- 另一方面,开始遍历 ChunkGraph,转译每一个模块代码;
- 最后,将所有模块与模块运行时依赖合并为最终输出的 Bundle —— 资产文件。
27. 手写webpack loader
@babel/parser 将源代码解析成 AST
@babel/traverse 对AST节点进行递归遍历,生成一个便于操作、转换的path对象
@babel/generator 将AST解码生成js代码
@babel/types通过该模块对具体的AST节点进行进行增、删、改、查
新建drop-console.js
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const t = require('@babel/types')
module.exports=function(source){
const ast = parser.parse(source,{ sourceType: 'module'})
traverse(ast,{
CallExpression(path){
if(t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.object, {name: "console"})){
path.remove()
}
}
})
const output = generator(ast, {}, source);
return output.code
}
如何使用:
const path = require('path')
module.exports = {
mode:'development',
entry:path.resolve(__dirname,'index.js'),
output:{
filename:'[name].[contenthash].js',
path:path.resolve(__dirname,'dist')
},
module:{
rules:[{
test:/\.js$/,
use:path.resolve(__dirname,'drop-console.js')
}
]
}
}
webpack4中已经集成了去除console功能,在minimizer中可配置 去除console
28. 手写webpack plugin
compiler和 compilation的区别在于
compiler代表了整个webpack从启动到关闭的生命周期,而compilation 只是代表了一次新的编译过程 compiler和compilation暴露出许多钩子,我们可以根据实际需求的场景进行自定义处理
在生成打包文件之前自动生成一个关于打包出文件的大小信息
新建一个webpack-firstPlugin.js
class firstPlugin{
constructor(options){
this.options = options
}
apply(compiler){
compiler.plugin('emit',(compilation,callback)=>{
let str = ''
for (let filename in compilation.assets){
str += 文件:${filename} 大小${compilation.assets[filename]['size']()}\n
}
// 通过compilation.assets可以获取打包后静态资源信息,同样也可以写入资源
compilation.assets['fileSize.md'] = {
source:function(){
return str
},
size:function(){
return str.length
}
}
callback()
})
}
}
module.exports = firstPlugin
如何使用 const path = require('path') const firstPlugin = require('webpack-firstPlugin.js') module.exports = { // 省略其他代码 plugins:[ new firstPlugin() ] }