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 中自动帮我们引入了这两个文件

在这里插入图片描述

问题:

查看打包生成的 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.js

optimization: {
  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 中添加
<button id="btn">点我</button>

// 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

在这里插入图片描述
可以看到,打包文件多生成了一个叫 0.js 的文件,并且 html 中并没有引入该文件,点击按钮再看看会发生什么
在这里插入图片描述
生成了一个 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 跟着变化,那么之前的缓存将会失效
在这里插入图片描述

我们可以使用上面提到的 runtimeChunk 将引用的代码抽离到一个单独的文件中。

optimization: {
  runtimeChunk: {
    name: entrypoint => `runtime-${entrypoint.name}`
  }
}

执行打包,随意修改 other.js 中的内容,再次打包,两次生成的文件如下

在这里插入图片描述

可以看到,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并执行了函数打印输出结果

在这里插入图片描述

问题:

这样做可能会让用户的交互长时间没有响应的。原因就是待到交互时才进行模块的加载,可能时间会比较长。由此,我们引入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已经预先获取了

加载完成之后,浏览器又会去自动加载
在这里插入图片描述

preload

preload和prefetch在用法上相差不大,效果上的差别如下(引自官方文档):

  • preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
  • preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
  • preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
    浏览器支持程度不同。
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值