Webpack5完整教程|webpack(八)代码分离

我们的 js 都是打包输出到一个文件中的,当内容越来越多的时候,会导致单个文件体积十分巨大,所以我们就需要对代码进行分割,将一个巨大的文件,分割成多个中小型文件,然后可以按需加载或并行加载这些文件
代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的bundle,以及控制资源加载优先级,如果使用合理,会极大缩减加载时间。
常用的代码分离方法有三种:
  • 入口起点:使用 entry 配置手动地分离代码。
  • 动态导入:通过模块的内联函数调用来分离代码
  • 防止重复:使用 SplitChunksPlugin 去重和分离 chunk。
一、多入口 npm init 初始化一个项目,新建 src/index.js、src/other.js、index.html,创建 webpack.config.js,写入如下配置,并在 package.json 中添加开发模式打包的脚本
module.export = { entry: { index: "./src/index.js", other: "./src/other.js" }, output: { filename: "[name].js" }, plugins: [new HTMLWebpackPlugin({ template: "index.html" })] }// package.json "script": { "build": "webpack --mode development" }

执行 npm run build,会发现输出了两个 js 文件,并且 html 中自动帮我们引入了这两个文件
Webpack5完整教程|webpack(八)代码分离
文章图片

问题: 查看打包生成的 index.js 跟 other.js 文件,会发现它们有大量重复的内容
这些都是 webpack 生成的 runtime 代码,由于这两个文件同时在 index.html 里面引入,因此 runtime 代码被引入了两次,我们可以添加如下配置将 runtime 代码分离出来
runtime的配置:
module.export = { optimization: { runtimeChunk: "single" // 它其实是下面这种写法的简写 /* runtimeChunk: { name: "runtime" } */ } }

打包后会发现多了一个叫 runtime.js 的文件,index.js 与 other.js 中重复的 runtime 代码都被抽离到了这个文件中,index.html 也同时引入了这三个文件
对于单页面,应避免使用多入口,可以使用单入口多文件,像下面这样
entry: { index: ["./src/index.js", "./src/other.js"] }

二、 防止重复 (1)配置 entry 提取公用依赖
webpack.config.js
module.exports = { entry: { index: { import: './src/index.js', // 启动时需加载的模块 dependOn: 'shared', // 当前入口所依赖的入口 }, another: { import: './src/other.js', dependOn: 'shared', },shared: 'lodash' // 当上面两个模块有lodash这个模块时,就提取出来并命名为shared chunk }, output: { filename: '[name].bundle.js', // 对应多个出口文件名 path: path.resolve(__dirname, './dist'), }, }

执行webpack命令,可以看到打包结果
已经提取出来shared.bundle.js,即为提取打包了lodash公用模块
index.bundle.js other.bundle.js体积也变小
查看dist/index.html可以看到三个文件都被加载了
(2)SplitChunksPlugin
SplitChunksPlugin 能自动的帮助我们做公共模块的抽离
webpack 中 splitChunks 的默认配置
//webpack.config.jsoptimization: { splitChunks: { chunks: 'async', // 动态导入的模块其依赖会根据规则分离 minSize: 30000, // 文件体积要大于 30k minChunks: 1, // 文件至少被 1 个chunk 引用 maxAsyncRequests: 5, // 动态导入文件最大并发请求数为 5 maxInitialRequests: 3, // 入口文件最大并发请求数为 3 automaticNameDelimiter: '~', // 文件名中的分隔符 name: true, // 自动命名 cacheGroups: { vendors: { // 分离第三方库 test: /[\\/]node_modules[\\/]/, priority: -10 // 权重 }, default: { // 分离公共的文件 minChunks: 2, // 文件至少被 2 个 chunk 引用 priority: -20, reuseExistingChunk: true // 复用存在的 chunk } } } }

chunks 该参数有四种取值
  • async:动态导入的文件其静态依赖会根据规则分离
  • initial:入口文件的静态依赖会根据规则分离
  • all:所有的都会根据规则分离
  • chunk => Boolean:返回 true 表示根据规则分离,false 则不分离
更多配置查看: SplitChunksPlugin
三、 动态导入 当涉及到动态代码拆分时,webpack 提供了两个类似的技术。
  • 第一种,也是推荐选择的方式是,使用符合 ECMAScript 提案 的 import() 语法 来实现动态导入。
  • 第二种,则是 webpack 的遗留功能,使用 webpack 特定的 require.ensure 。
这里让我们尝试使用第一种方式,使用 import() 来进行动态导入
我们分别在 index.html、other.js、index.js 中添加如下代码
// index.html body 中添加 // other.js console.log("我被加载了")// index.js let btn = document.querySelector("#btn"); btn.addEventListener("click", () => { // import() 返回的是一个 promise,在 then() 方法中执行导入后的操作,也可以使用 async/await import("./other").then(res => { console.log(res); }); });

打包后在浏览器中打开 index.html
Webpack5完整教程|webpack(八)代码分离
文章图片

可以看到,打包文件多生成了一个叫 0.js 的文件,并且 html 中并没有引入该文件,点击按钮再看看会发生什么
Webpack5完整教程|webpack(八)代码分离
文章图片

生成了一个 script 标签,并加载了 0.js 文件,这就是动态导入。对于这个文件名称,不是很直观,我们使用魔法注释修改一下
import(/* webpackChunkName: "other" */ "./other").then(res => { console.log(res); });

最终打包生成的文件名就叫 other.js,如果希望加上 hash 值,可以在配置文件里添加一个参数
output: { filename: "[name]-[chunkhash:10].js", // 入口文件打包生成的文件名 chunkFilename: "[name]-[chunkhash:10].js" // 动态模块打包生成的文件名,name 默认为数字,如果使用了魔法注释则为魔法注释的名字 }

这样一来,又引发了一个新的问题,从下图可以看出,虽然 other.js 的内容没有被直接打包进 main.js,但是 main.js 中保存着 other.js 的文件名,当 other.js 内容发生变化时,其文件名也会变化,导致 main.js 跟着变化,那么之前的缓存将会失效
Webpack5完整教程|webpack(八)代码分离
文章图片

我们可以使用上面提到的 runtimeChunk 将引用的代码抽离到一个单独的文件中。
optimization: { runtimeChunk: { name: entrypoint => `runtime-${entrypoint.name}` } }

执行打包,随意修改 other.js 中的内容,再次打包,两次生成的文件如下
【Webpack5完整教程|webpack(八)代码分离】Webpack5完整教程|webpack(八)代码分离
文章图片

可以看到,main.js 的 hash 值并没有变化,而抽离出来的文件用户也访问不到,因此不会影响缓存
四、懒加载
懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。
math.js
export function add (x, y) { return x + y }export function reduce (x, y) { return x - y }

src/index.js
const button = document.createElement('button') button.textContent = '点击执行加法运算' button.addEventListener('click', () => { // 魔法注释 webpackChunkName 修改懒加载打包文件名 // 即使不使用 webpackChunkName,webpack 5 也会自动在 development 模式下分配有意义的文件名。 import(/* webpackChunkName: 'math' */ './math.js').then(({ add }) => { console.log(add(4, 5)) }) }) document.body.appendChild(button)

效果:
点击按钮后才加载math.bundle.js并执行了函数打印输出结果
Webpack5完整教程|webpack(八)代码分离
文章图片

问题: 这样做可能会让用户的交互长时间没有响应的。原因就是待到交互时才进行模块的加载,可能时间会比较长。由此,我们引入prefetch,即预取。
五、预获取、预加载 Webpack v4.6.0+ 增加了对预获取和预加载的支持。
在声明 import 时,使用下面这些内置指令,可以让 webpack 输出 “resource hint(资源提示)”,来告知浏览器:
  • prefetch(预获取):将来某些导航下可能需要的资源(当页面所有内容都加载完毕后,在网络空闲的时候,加载资源)
  • preload(预加载):当前导航下可能需要资源
prefetch src/index.js
const button = document.createElement('button') button.textContent = '点击执行加法运算' button.addEventListener('click', () => { // webpackPrefetch: true 在动态引入时开始预获取 import(/* webpackChunkName: 'math', webpackPrefetch: true */ './math.js').then(({ add }) => { console.log(add(4, 5)) }) }) document.body.appendChild(button)

效果
可以看到math.bundle.js已经预先获取了
加载完成之后,浏览器又会去自动加载
Webpack5完整教程|webpack(八)代码分离
文章图片

preload preload和prefetch在用法上相差不大,效果上的差别如下(引自官方文档):
  • preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
  • preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
  • preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
    浏览器支持程度不同。

    推荐阅读