webpack作为前端代码的打包工具,本身不会影响前端代码在生产环境下的代码执行过程,不过前端代码通常作为静态资源传输,尽可能减小前端代码的体积可以加快浏览器请求前端代码的速度,提高单页面应用切换及首评渲染的速度,而webpack作为代码的打包工具,则可以通过配置尽可能减少代码大小,实现性能优化。
1.代码分包
代码分包本质上并不是减少代码体积,但是代码分包可以将本来一次返回到浏览器的前端静态资源,可以分开按需返回,我们所学习vue和react的懒加载,本质上就是webpack在进行代码分包。而通过原生的webpack配置可以自定义代码分包策略。
可以在webpack的配置中添加entry配置,来定义多个打包入口,webpack就会以配置的入口为开始,分别进行打包,而由于是两个入口,打包出来的也是两个包。我们还可以通过output配置项来配置输出文件名的命名规则和输出路径。
多个入口打包文件大概率会依赖相同的其他文件,为了防止这些被两个入口文件都依赖的文件被重复打包,我们还要在每个入口文件处配置相应策略。我们可以通过配置optimization.splitChunks来定义切割代码的策略,然后配置依赖包和公用文件进行单独切割,这样就可以防止重复打包。
示例代码如下:
// entry: 定义项目的入口点,可以是一个或多个
entry: {
index: './src/js/index.js', // 第一个入口点,指向 index.js
about: './src/js/about.js', // 第二个入口点,指向 about.js
},
// output: 定义打包后文件的输出配置
output: {
filename: '[name].[contenthash].js', // 输出的文件名使用 [name] 代替入口名称,并添加内容哈希值用于缓存控制
path: path.resolve(__dirname, 'dist'), // 输出文件的目标路径为项目根目录下的 dist 文件夹
},
// optimization: 优化配置,用于设置代码分割等优化手段
optimization: {
// splitChunks: 代码分割配置,用于将共享模块提取到单独的文件中
splitChunks: {
chunks: 'all', // 对所有类型的代码块(包括同步和异步)进行分割
cacheGroups: { // cacheGroups 定义不同的缓存组,用于对不同类型的代码块进行分组
// vendor 组用于处理第三方库的分割
vendor: {
test: /[\\/]node_modules[\\/]/, // 匹配 node_modules 目录中的所有模块
name: 'vendors', // 提取出来的代码块命名为 vendors
chunks: 'all', // 对所有类型的代码块进行分割
enforce: true, // 强制将符合条件的模块分配到这个组,即使默认情况下不会被分割
},
// common 组用于处理多个入口文件之间共享的自定义模块
common: {
name: 'common', // 提取出来的代码块命名为 common
minChunks: 2, // 至少有两个入口文件共享该模块时,才会被提取到 common 组
chunks: 'all', // 对所有类型的代码块进行分割
priority: -10, // 优先级设置为 -10,低于 vendor 组
reuseExistingChunk: true, // 允许复用已经存在的块,而不是重新创建
},
},
},
}
可以在代码中通过import函数引入文件,以此方法引入的文件会被单独打包为一个文件,Vue的懒加载就是通过import实现的,而React的lazy方法,底层实现也是通过import函数,实例代码如下:
import('./index.js').then((res) => {
console.log(res.default());
});
import函数的返回值为promise,promise的res值为模块对象,如果引入的是模块,我们则可以通过promise的res.的方式获取模块导出的值,其中默认值取res.default,其他的直接使用变量名字作为res的属性获取。 import函数生成的单独的文件名,依旧受output配置项控制
刚才我们通过optimization.splitChunks来实现公用代码从原代码中切割为一个单独的文件,本质上这也是一个分包操作,所以单独通过optimization.splitChunks对代码进行合理切割,也可以实现代码打包的分包操作。
刚才我们通过配置cacheGroups中的comon属性,来实现对公用代码的切割,实际上我们还可以通过配置maxSize和minSize,来将打包后的代码分割成若干个符合大小规则的包,示例如下:
optimization: {
// `chunkIds: 'deterministic'` 确保在内容不变的情况下,构建之间的 chunk ID 保持一致,利于长期缓存。
chunkIds: 'deterministic',
// `runtimeChunk` 配置用于提取 Webpack 运行时代码到单独的文件中,以优化缓存。
runtimeChunk: {
name: "runtime" // 指定提取出的运行时代码文件名为 "runtime.js"。
},
// `splitChunks` 配置项控制代码拆分,以减少重复代码,优化加载性能。
splitChunks: {
chunks: "all", // 对所有类型的代码块(同步和异步)进行拆分。
maxSize: 20000, // 设置单个代码块的最大大小为 20000 字节(20KB),超过此大小将尝试进一步拆分。
minSize: 1000, // 设置单个代码块的最小大小为 1000 字节,小于此大小的代码块不会被拆分。
// `cacheGroups` 定义了如何将代码拆分成不同的组,每个组可以生成一个单独的文件。
cacheGroups: {
// `utils` 组专门处理路径中包含 `/utils/` 的模块,将它们拆分到独立文件中。
utils: {
test: /utils/, // 匹配所有路径中包含 `/utils/` 的模块。
filename: "[id]_utils.js" // 指定输出文件名为 "[id]_utils.js",其中 `[id]` 为 Webpack 分配的唯一 ID。
},
// `vendors` 组用于处理 `node_modules` 中的第三方库,将它们拆分到独立文件中。
vendors: {
test: /[\\/]node_modules[\\/]/, // 匹配所有位于 `node_modules` 目录中的模块。
filename: "[id]_vendors.js" // 指定输出文件名为 "[id]_vendors.js",其中 `[id]` 为 Webpack 分配的唯一 ID。
chunks: 'all', // 对所有类型的代码块进行分割
enforce: true, // 强制将符合条件的模块分配到这个组,即使默认情况下不会被分割
}
}
},
// `minimizer` 配置用于指定用于代码压缩的插件。
minimizer: [
// 使用 `TerserPlugin` 压缩输出的 JavaScript 文件,减少文件大小。
new TerserPlugin({
extractComments: false // 设置为 `false` 表示不将注释提取到单独的文件中,而是直接从输出中移除。
})
]
}
Chunks默认值为async,表示只有异步引入才被单独打包,import函数就是异步的一种,而all表示异步同步全都按照规则进行单独打包。 cacheGroup定义打包规则时,node_modules需要在两侧加上斜杠,防止某些文件名带着node_modules引起歧义,而window电脑既可以斜杠也可以反斜杠,并且反斜杠有特殊含义需要转义,所以最终写法如代码所示。 filename配置可自定义打包文件名字,可使用name和id变量,使用[ ]包裹。name变量是默认生成的,而id可以自定义通过chunkIds配置定义,可以配置如下内容
- natural:自然数,按照打包顺序
- id:分别为1234等等,
- named:默认生成的名字和name变量值相同
- deterministic:固定数
其中named和deterministic内容不变,当触发重新打包时,如果目标文件内容不变的话,则可以不用重新打包,提升打包速度。通常在开发环境我们选取named,因为可以看到打包的路径,而在打包上线时我们选取deterministic。
Webpack打包时,CSS文件打包进JS代码中,以JS逻辑的形式发挥作用,如果我们想对css文件进行分包操作,则需要,则需要使用MiniCssExtractPlugin 插件。
首先通过npm下载
npm install --save-dev mini-css-extract-plugin
然后进行如下配置开启CSS分包:
module: {
rules: [
{
test: /\.css$/, // 匹配所有的 CSS 文件
use: [
MiniCssExtractPlugin.loader, // 提取 CSS 到单独的文件
'css-loader', // 解析 CSS 文件
],
}
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css', // 输出的 CSS 文件名及其内容哈希值
}),
],
optimization: {
splitChunks: {
chunks: 'all', // 对所有类型的块进行分割
cacheGroups: {
styles: {
name: 'styles', // 把所有 CSS 分割到一个独立的文件中
test: /\.css$/, // 只匹配 CSS 文件
chunks: 'all',
enforce: true, // 强制生成这个块
},
},
},
},
其中该插件提供的loader处理CSS文件,会将css文件处理为单个文件,而不是嵌入到JS文件中。而插件配置则是将loader处理好的多个文件在打包成一个CSS文件。插件配置则是定义输出文件名字。
当受屏渲染完成时,我们应该在浏览器空闲时,下载其他资源,以最大程度化保证用户体验,这时便用到了预获取和预加载。
预获取和预加载的使用是通过魔法注释,其中预获取是该文件要等浏览器空闲时下载,同一时间只建立一个网络连接。而预获取则是开启一个新的网络连接,和主页面一同加载。
预获取通常用于两个页面之间的依赖关系,如路由,同一时间不需要展示两个页面,所以不急于快速加载完下一个页面,只需要在浏览器空闲时获取即可。 而预加载则是用于同一个页面的依赖关系,一个页面中可能很快就需要访问这部分依赖的静态资源,这种情况下不需要等待浏览器空闲,而是立即开启一个新的网络线程,和主页面并行加载。
代码示例如下:
//预获取
import(/* webpackPrefetch: true */ '路径')
//预加载
import(/* webpackPreload: true */ '路径')
2. CDN内容分发网络
CDN英文全称Content Delivery Network,中文翻译即为内容分发网络。其通过代码中引入CDN服务器中保存的依赖包,大大减小前端代码的体积。当使用了CDN后,前端页面加载流程如下:
- 当代码执行到CDN引入包的逻辑时,会向目标的URL发送请求
- 客户端向目标CDN服务器建立TCP连接。
- 客户端向目标CDN服务器发起对该资源的访问请求。
- 如果该CDN服务器对应的节点已缓存该资源,则会将数据直接返回给用户
- 如果该CDN服务器对应的节点未缓存该资源,则节点向源站发起对该资源的请求。获取资源后将资源缓存至节点。
在这个过程中,所有CDN服务器组成CDN内容分发网络,用户获取数据时如果本地CDN服务器有则直接返回数据,如果没有则请求源站,并将该数据缓存。
在代码中通过script标签的src属性,引入CDN服务器的依赖资源,代码如下:
当打包项目时,我们需要将一部分依赖包进行打包,一部分依赖包需要依赖CDN时,我们则需要依赖webpack的externals配置,来将依赖CND的依赖包进行排除。具体配置如下:
// 排除某些包不需要进行打包
externals: {
// key属性名:排除的框架的名称
// value值:从CDN地址请求下来的js中提供对应的名称
react: "React",
axios: "axios"
}
3. 摇树
简单来说,摇树就是在打包时去除无用代码,webpack内置了四种摇树工具,而摇树工具对无用代码的定义决定了其具体实现的功能。
webpack打包器是其内置的打包功能,其本身自带一定的摇树功能,其可以将未引用的模块,未被其他文件引入的导出值,未被执行的代码块以及未被修改过的变量定义为无用代码,通过摇树,在打包过程将其删除。
terser是一款处理js的工具,其本身具备对js代码的解析(parse),压缩(compress),以及绞肉机(mangler),绞肉机的意思就是去除无用代码,所以terser也可以实现摇树功能。
webpack将其集成为自己的TerserPlugin插件,可以通过配置optimization.minizer中配置TerserPlugin插件中的terserOptions.compress.unused为true,开启其摇树功能。
useexports是webpack的一款内置插件,他可以对在另一个文件对这个文件进行引入,但不对引入的值进行任何使用相关的代码进行摇树。假设下面的代码块中有a和b两个文件,其中b文件不对引入值进行任何使用,这种情况下useExports就会将b文件不打包进代码中,示例如下:
//a.js文件========================================================
function foo(a){
console.log(a)
}
export foo
//b.js文件==========================================================
import {foo} from "./a.js"
//只导入,不进行任何使用
useExports只需在webpack配置文件中将optimization中的usedExports配置改为true即可,具体如下:
optimization:{
usedExports: true
}
usedExports本身并不具备删除代码的功能它只能识别哪些导出是未使用的并将这些未使用的导出值制表作为webpack的配置传递给terser,最终还是terser进行删除,所以一些文献可能说terser具有删除导出未使用的功能,原因就是这个。
当b文件引入a的整个模块,但不使用a模块的导出值,但是a文件中定义了全局变量q,所以为随着b文件对于a文件的引入,b文件也会对全局代码产生影响,此时则需要对b文件进行打包,否则全局变量q将会消失。示例如下
//a.js===========================================================================
export const foo = (a)=>{
console.log(a)
}
window.q = "全局副作用"
//b.js=========================================================================
import "./a.js"
可是Webpack并不能检测到这种情况,也就是说即使a文件中并没有这种修改全局的代码,Webpack因为无法检测,也不敢将b文件将其删除,此时则需要sideEffect进行摇树功能。
sideEffect本身并不是摇树工具,而是一种标记配置。我们可以将没有副作用的模块,在package.json中进行如下配置:
{
"name": "my_project",
"version": "1.0.0",
"sideEffects": false
}
其中name是当前项目名,version是版本,而sideEffect设置为false则是表明,当前项目的所有模块均没有副作用,webpack可以进行摇树。sedeEffect也可以是文件路径的数组,表示只有当前数组中的文件,才具有副作用。
4. 代码压缩
在我们使用webpack进行打包时,我们会发现打包后的代码变成了一团乱麻,其中不仅换行符和空格没了,而且变量名也变了,这就js代码压缩后的结果。通过减少换行符和空格,以及减小变量名等等方式,使文件体积减小。
这个功能依赖于Webpack内部集成的TerserWebpackPlugin插件,也就是我们上述说的对代码进行摇树的插件。Terser工具包含了对js代码的解析(parse),压缩(compress),绞肉机(mangle),其中压缩就是对JS代码进行压缩,减小文件体积。开启配置如下:
// 优化配置
optimization: {
// 是否开启压缩(生产模式下默认开启)
minimize: true,
// 相关配置
minimizer: [
// js压缩插件terser
new TerserPlugin({
// 是否将注释单独打包到一个文件中
extractComments: false,
// terser中对js代码的压缩和绞肉相关配置
terserOptions: {
// 压缩配置
compress: {
// 改变函数形参名字,这就是为什么我们的参数变成一个字母
arguments: true,
// 是否开始tree shaking(摇树,这就是开启terser的摇树功能
unused: true,
},
// 绞肉机配置
mangle: {
// 项目变量是否改变
toplevel: false,
// 是否保持函数名字
keep_fnames: true,
}
},
}),
],
},
我们可以通过Webpack集成的CSSMinimizerPlugin插件对CSS文件进行压缩,其底层通过CssNano工具实现,开启配置如下:
optimization: {
// 是否开启(生产模式下默认开启)
minimize: true,
// 相关配置
minimizer: [
new CSSMinimizerPlugin({
// 是否开启并发执行(默认开启)
// parallel: true
})
]
},
无论是js压缩还是css压缩,webpack都会默认进行压缩,只有当我们需要自定义配置时,才进行配置修改。 parallel是开启并发打包,默认开启。 这两个压缩插件是webpack内置默认使用的插件,无需在插件配置内。只需要在optimization配置内修改相关配置即可。
与前两者不一样,HTML压缩不会默认执行,而是需要使用特定插件,也就是HtmlWebpackPlugin插件,并且这个插件需要卸载plugin配置内显示使用,开启配置如下:
plugins: [
new HtmlWebpackPlugin({
template: './index.html',
cache: true,
minify: isProduction ? {
// 移除注释
removeComments: true,
// 移除属性
removeEmptyAttributes: true,
// 移除默认属性
removeRedundantAttributes: true,
// 折叠空白字符
collapseWhitespace: true,
// 压缩内联的CSS
minifyCSS: true,
// 压缩HTML文件内的JS代码
minifyJS: {
mangle: {
toplevel: true
}
}
} : false
}),
]