一、前言
随着 Web 业务日益复杂化和多元化以及前端模块化的流行,前端也进入了工程化的领域,而 Webpack 是当前阶段最流行的构建打包工具(bundler),没有之一(Vite 正在崛起)。
二、工程化
提到构建打包工具,就不得不提前端工程化,工程化给构建工具提供理论指导,构建工具是工程化的实现。 工程化可以分为模块化、组件化、规范化、自动化。而 Webpack 提供了这些问题的解决方案。
三、Webpack
Webpack 是一个静态模块打包器(bundler) 在 Webpack 看来,前端的所有资源(js/ts/json/css/less/jpg/gif/mp4/...)都会作为静态模块处理,src/index.js 作为默认入口文件(Webpack 分析入口),分析依赖关系,生成代码块(chunks),然后编译打包成静态资源(bundle)。
将程序员写完的源代码加工生成具有良好兼容性,且可以让浏览器高效,稳定运行的的代码
Webpack 配置文件
Webpack 中,有五个核心属性,默认定义在webpack.config.js,它们分别是:
1、Entry
- 入口起点(入口点)指示 Webpack 应该使用该模块,来作为构建其内部依赖图(依赖关系图)的。进入入口起点后,Webpack 会发现有模块和库是入口起点(直接和间接)依赖的。
- 默认值是
./src/index.js,但您可以通过在 Webpack 配置中配置 entry 属性,来指定一个(或多个)不同的入口起点。例如:
module.exports = {
entry: './path/to/my/entry/file.js'
};
2、Output
- output 属性告诉 Webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件。主要输出文件的默认值是
./dist/main.js,其他生成文件默认放置在./dist文件夹中。 - 以通过在配置中指定一个 output 字段,来配置这些处理过程:
const path = require('path');
module.exports = {
entry: './path/to/my/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js'
}
};
在上面的示例中,我们通过 output.filename 和 output.path 属性,来告诉 webpack bundle 的名称,以及我们想要 bundle 生成(emit)到哪里。
tips: path 模块是一个 Node.js 核心模块,用于操作文件路径。
3、Loader
-
webpack 只能理解 JavaScript 和 JSON 文件,这是 Webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中。实际上,loader 只是一个普通的 funciton,接受匹配文件的字符串内容(借助资源模块完成),进行转换。
loader 例子:
/** * loader Function * @param {String} content 文件内容 */ module.exports = function(content){ return "{};" + content }在更高层面,在 webpack 的配置中,loader 有两个属性:
-
test属性,识别出哪些文件会被转换。 -
use属性,定义出在进行转换时,应该使用哪个 loader。
const path = require('path');
module.exports = {
output: {
filename: 'my-first-webpack.bundle.js'
},
module: {
rules: [
{ test: /\.txt$/, use: 'raw-loader' }
]
}
}
以上配置中,对一个单独的 module 对象定义了 rules 属性,里面包含两个必须属性:test和use。这告诉 Webpack 编译器(compiler)如下信息:
“嘿,webpack 编译器,当你碰到「在
require()/import语句中被解析为'.txt' 的路径」时,在你对它打包之前,先 use(使用)raw-loader转换一下。”
tips: 请记住,使用正则表达式匹配文件时,你不要为它添加引号。也就是说,/\.txt$/与'/\.txt$/'或 "/\.txt$/"不一样。前者指示 Webpack 匹配任何以 .txt 结尾的文件,后者指示 webpack 匹配具有绝对路径 '.txt' 的单个文件。
4、Plugin
loader 用于转换某些类型的模块,而 plugin 则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。
想要使用一个插件,你只需要 require() 它,然后把它添加到 plugins数组中。多数插件可以通过选项(option)自定义。你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用new操作符来创建一个插件实例。
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 安装
const webpack = require('webpack'); // 用于访问内置插件
module.exports = {
module: {
rules: [
{ test: /\.txt$/, use: 'raw-loader' }
]
},
plugins: [
new HtmlWebpackPlugin({template: './src/index.html'})
]
};
在上面的示例中,html-webpack-plugin 为应用程序生成一个 HTML 文件,并自动注入所有生成的bundle。
5、Mode
通过选择 development, production 或 none 之中的一个,来设置mode 参数,以启用 webpack内置在相应环境下的优化。默认值为production。或者从 CLI 参数中传递:webpack --mode=development
module.exports = {
mode: 'production'
};
| 选项 | 描述 |
|---|---|
development | 会将DefinePlugin中process.env.NODE_ENV的值设置为development。为模块和chunk启用有效的名。 |
production | 会将DefinePlugin中process.env.NODE_ENV的值设置为production。为模块和chunk启用确定性的混淆名称,FlagDependencyUsagePlugin,FlagIncludedChunksPlugin,ModuleConcatenationPlugin,NoEmitOnErrorsPlugin 和 TerserPlugin 。 |
none | 不使用任何默认优化选项 |
四、Webpack 构建性能
-
先使用 speed-measure-webpack-plugin 插件对项目打包耗时进行评估。
// webpack.config.js const SpeedMeasurePlugin = require("speed-measure-webpack-plugin"); const smp = new SpeedMeasurePlugin(); const webpackConfig = smp.wrap(webpackConfig); -
输出如下:
-
再根据分析出的报告进行针对性的优化。
根据代码执行环境,Webpack 性能优化分为三个部分,分别是通用环境、开发环境,生产环境。
通用环境
Dll
使用 DllPlugin 为更改不频繁的代码生成单独的编译结果。这可以提高应用程序的编译速度,尽管它增加了构建过程的复杂度。
Loader
将 loader 应用于最少数量的必要模块。而非如下:
module.exports = {
//...
module: {
rules: [
{
test: /.js$/,
loader: 'babel-loader',
},
],
},
};
通过使用 include 字段,仅将 loader 应用在实际需要将其转换的模块:
const path = require('path');
module.exports = {
//...
module: {
rules: [
{
test: /.js$/,
include: path.resolve(__dirname, 'src'),
loader: 'babel-loader',
},
],
},
};
解析
以下步骤可以提高解析速度:
- 减少
resolve.modules,resolve.extensions,resolve.mainFiles,resolve.descriptionFiles中条目数量,因为他们会增加文件系统调用的次数。 - 如果你不使用 symlinks(例如
npm link或者yarn link),可以设置resolve.symlinks: false。 - 如果你使用自定义 resolve plugin 规则,并且没有指定 context 上下文,可以设置
resolve.cacheWithContext: false。
worker 池(worker pool)
thread-loader 可以将非常消耗资源的 loader 分流给一个 worker pool。在耗时的 Loader 前开启多进程。
Warning:不要使用太多的 worker,因为 Node.js 的 runtime 和 loader 都有启动开销。最小化 worker 和 main process(主进程) 之间的模块传输。进程间通讯(IPC, inter process communication)是非常消耗资源的。
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /.js$/,
include: path.resolve('src'),
use: [
"thread-loader",
// 耗时的 loader (例如 babel-loader)
],
},
],
},
};
持久化缓存
在 webpack 配置中使用 cache 选项。使用 package.json 中的 "postinstall" 清除缓存目录。
oneOf
当规则匹配时,只使用第一个匹配规则,并跳出匹配数组。
webpack.config.js
module: {
rules: [{
oneOf: [{
resourceQuery: /raw/,
type: 'asset/source',
},
{
test: /.m?js$/,
use: [ ... ]
}]
}]
},
开发环境
增量编译
使用 webpack 的 watch mode(监听模式)。而不使用其他工具来 watch 文件和调用 webpack 。内置的 watch mode 会记录时间戳并将此信息传递给 compilation 以使缓存失效。
在某些配置环境中,watch mode 会回退到 poll mode(轮询模式)。监听许多文件会导致 CPU 大量负载。在这些情况下,可以使用 watchOptions.poll 来增加轮询的间隔时间。
在内存中编译
下面几个工具通过在内存中(而不是写入磁盘)编译和 serve 资源来提高性能:
webpack-dev-serverwebpack-hot-middlewarewebpack-dev-middleware
HMR(hot module replacement)模块热替换
HMR 功能会在应用程序运行过程中,替换、添加或删除模块,而无需重新加载整个页面。主要是通过以下几种方式,来显著加快开发速度:
- 保留在完全重新加载页面期间丢失的应用程序状态。
- 只更新变更内容,以节省宝贵的开发时间。
- 在源代码中 CSS/JS 产生修改时,会立刻在浏览器中进行更新,这几乎相当于在浏览器 devtools 直接更改样式。
Webpack 配置方式:
// webpack.config.js
module.exports = {
devServer: {
hot: true,
},
}
- Css 文件:可以使用 HMR 功能,因为 style-loader 内部实现了
- JS 文件:默认不能使用 HMR 功能,需要修改 js 代码,添加支持 HMR 功能的代码
- HTML 文件:默认不能使用 HRM 功能,同时会导致问题:html 文件不能热更新了
Devtool
需要注意的是不同的 devtool 设置,会导致性能差异。
"eval"具有最好的性能,但并不能帮助你转译代码。- 如果你能接受稍差一些的 map 质量,可以使用
cheap-source-map变体配置来提高性能 - 使用
eval-source-map变体配置进行增量编译。
Tip:在大多数情况下,最佳选择是
eval-cheap-module-source-map。
避免在生产环境下才会用到的工具
某些 utility, plugin 和 loader 都只用于生产环境。例如,在开发环境下使用 TerserPlugin 来 minify(压缩) 和 mangle(混淆破坏) 代码是没有意义的。通常在开发环境下,应该排除以下这些工具:
TerserPlugin[fullhash]/[chunkhash]/[contenthash]AggressiveSplittingPluginAggressiveMergingPluginModuleConcatenationPlugin
生产环境
Source-Map
一种提供源代码到构建后代码映射技术(如果构建后代码错了,通过映射可以追踪源代码错误) 建议配置为精简项,减少 CPU 压力。
// webpack.config.js
module.exports = {
devtool:'nosources-source-map'
...
}
可选项:[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
source-map:生成外部调试文件 提供错误代码准确信息 和 源代码的错误位置inline-source-map:以base64方式内联进打包文件,提供错误代码准确信息和源代码的错误位置hidden-source-map:提示错误代码错误原因,没有错误位置,不能追踪到源代码错误eval-source-map:每一个文件都生成对应的 source-map,都包裹在 eval 文件,提供错误代码准确信息和源代码的错误位置nosource-source-map:提示错误代码错误原因,但是没有任何源代码信息cheap-source-map:提示错误代码错误原因和源代码错误位置,但只能精确到行cheap-module-source-map:提示错误代码错误原因和源代码错误位置,module 会将loader 的 source-map 加入
Babel 缓存
通过使用 cacheDirectory 将转译的结果缓存到文件系统中。
// webpack.config.js
module: {
rules: [
{
test: /.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
cacheDirectory:true, // 缓存
cacheCompression:false // 取消缓存压缩
}
}
}
]
}
五、Webpack 优化静态资源
CssMinimizerPlugin
压缩 CSS 文件
// webpack.config.js
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
optimization: {
minimizer: [
// '...',
new CssMinimizerPlugin(),
],
},
};
TerserWebpackPlugin
压缩 JS 文件
// webpack.config.js
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
optimization: {
minimize: true,
minimizer: [
// '...',
new TerserPlugin()
],
},
};
Hash 优化
减少服务器部署文件的更新,合理利用浏览器缓存机制,提高静态资源加载速度。
hash: 每次 Webpack 打包构建时都会生成不同的 hash 值chunkhash:根据 chunk 生成的 hash 值,如果打包来源同一个chunk,那么 hash 值就一样contenthash:根据文件内容生产的 hash 值,不同文件 hash 值不同。如果文件未更改,则每次生成的 hash 值相同
Tree shaking
去除无用代码。
前提:
- 使用 ES2015 模块语法(即
import和export)。 - 确保没有编译器将您的 ES2015 模块语法转换为 CommonJS 的(顺带一提,这是现在常用的 @babel/preset-env 的默认行为,详细信息请参阅文档)。
- 在项目的
package.json文件中,添加"sideEffects"属性。 - 使用
mode为"production"的配置项以启用 tree shaking。