1. 代码分离的方法

代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间

常用的代码分离方法有三种:

  • 入口起点:使用entry配置手动地分离代码。缺点:如果项目有多个入口,那么这些多个入口共享的文件会分别在每个包里面重复打包,所以需要第二种防止重复的分离方法
  • 防止重复:使用Entry dependencies或者SplitChunksPlugin去重和分离 chunk
  • 动态导入:通过模块的内联函数调用来分离代码

2. 入口起点(静态导入)

这是迄今为止最简单直观的分离代码的方式。不过,这种方式手动配置较多,并有一些隐患,我们之后将会解决这些问题。先来看看如何从 main bundle 中分离 another module(另一个模块):

在 src/js 文件中新建 anthor-module.js

  1. // 使用 lodash 之前需安装依赖 npm i lodash
  2. import _ from 'lodash'
  3. console.log(_.join(['Another', 'module', 'loaded!'], ' '))

配置entry使之有多个入口

  1. const path = require('path')
  2. module.exports = {
  3. entry: {
  4. index: './src/js/index.js',
  5. anthor: './src/js/antoer-module.js'
  6. },
  7. output: {
  8. filename: 'js/[name].bundle.js',
  9. path: path.resolve(__dirname, './dist'),
  10. clean: true
  11. }
  12. }

执行npx webpack并观察控制台:
image.png
我们发现lodash.js被打包到了 anthor.bundle.js 中,而且打包后的 dist/js 中有两个出口文件(因为有两个入口文件),dist/index.html 中也引入了两个 js 文件
image.png
image.png
接着我们在 index.js 文件中也引入并使用lodash

  1. // 导入 js 文件
  2. import _ from 'lodash'
  3. import hello from './hello.js'
  4. // 加载 js 格式文件
  5. hello()
  6. console.log(_.join(['index', 'module', 'loaded!'], ' '))

附:hello.js

  1. function hello() {
  2. return new Promise((resolve, reject) => {
  3. setTimeout(() => {
  4. resolve('Hello Webpack5 !!!')
  5. }, 1000)
  6. })
  7. }
  8. async function getString() {
  9. const str = await hello()
  10. console.log(str)
  11. }
  12. export default getString

执行npx webpack并观察控制台:
image.png

所以产生了一些问题:

  1. 如果入口 chunk 之间包含一些重复的模块,那些重复模块都会被引入到各个 bundle 中,造成重复引用
  2. 这种方法不够灵活,并且不能动态地将核心应用程序逻辑中的代码拆分出来

3. 防止重复(静态导入)

现在我们需要将多个 bundle.js 文件中引入的重复模块给单独抽离出来,有两种方法可以实现在多个 chunk 之间共享模块:

① 配置 dependOn option

  1. entry: {
  2. index: {
  3. import: './src/js/index.js',
  4. dependOn: 'shared'
  5. },
  6. anthor: {
  7. import: './src/js/another-module.js',
  8. dependOn: 'shared'
  9. },
  10. shared: 'lodash'
  11. },

执行npx webpack并观察控制台:
image.png
以及打包后 dist 中的情况(js文件夹下和 index.html):
image.png
image.png

② SplitChunksPlugin

SplitChunksPlugin插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk

  1. entry: {
  2. index: './src/js/index.js',
  3. anthor: './src/js/another-module.js'
  4. },
  5. // 配置 optimization.splitChunks
  6. optimization: {
  7. splitChunks: {
  8. chunks: 'all'
  9. }
  10. }

执行npx webpack并观察控制台:
image.png
发现lodash已经被抽离出来了,接着观察打包后的 dist 文件夹:
image.png
image.png

4. 动态导入

当涉及到动态代码拆分时,webpack 提供了两个类似的技术:

  1. 比较推荐选择的方式,是使用符合 ECMAScript 提案 的import()语法来实现动态导入
  2. 是 webpack 的遗留功能,使用 webpack 特定的require.ensure

看第一种方式:
在 js 文件夹下创建 async-module.js ,并在 index.js 文件中引入

  1. function getComponent() {
  2. return import ('lodash').then(({ default: _ }) => {
  3. const divEle = document.createElement('div')
  4. divEle.innerHTML = _.join(['Hello', 'webpack5'], ' ')
  5. return divEle
  6. })
  7. }
  8. getComponent().then( divEle => {
  9. document.body.appendChild(divEle)
  10. })
  1. import '../js/async-module.js'
  1. module.exports = {
  2. entry: './src/js/index.js',
  3. optimization: {}
  4. }

执行npx webpack并观察控制台:
image.png
发现lodash已经被抽离出来了,接着观察打包后的 dist 文件夹:
image.png
image.png
注:用import()语法实现动态导入并不会在打包后的 index.html 中引入

静态导入很好用啊,为什么要用动态导入呢?动态导入的应用场景在哪?应用动态导入又有什么好处呢?

5. 懒加载

动态导入的应用场景之一
懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后(如:点击按钮),立即引用或即将引用另外一些新的代码块(不操作就不会引入)。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载

在 js 文件夹下创建一个math.js文件,在主页面中通过点击按钮调用其中的函数:

  1. export const add = (a, b) => {
  2. return a + b
  3. }
  4. export const minus = (a, b) => {
  5. return a - b
  6. }

index.js 中实现懒加载

  1. // 懒加载 js 模块
  2. const div1 = document.createElement('div')
  3. const btn1 = document.createElement('button')
  4. btn1.innerHTML = '点击执行加法运算'
  5. div1.appendChild(btn1)
  6. document.body.appendChild(div1)
  7. btn1.addEventListener('click', () => {
  8. import ('../js/math.js').then(({add}) => {
  9. console.log(add(1, 2))
  10. })
  11. })

执行npx webpack后观察 dist:
发现 math.js 已经被单独的抽离出来了,导出的文件名不好看,所以我们可以自定义导出文件的文件名,只需要在懒加载时追加一句魔法注释/* webpackChunkName: '自定义文件名' */
image.png

  1. // 修改一下代码
  2. btn1.addEventListener('click', () => {
  3. import(/* webpackChunkName: 'math' */ '../js/math.js').then(({ add }) => {
  4. console.log(add(1, 2))
  5. })
  6. })

image.png
再进入浏览器中观察模块的加载情况:
发现初始化时并没有加载模块,当执行操作时才加载模块,实现了资源的按需加载
懒加载.gif

6. 预获取/预加载模块

动态导入的应用场景之一
Webpack v4.6.0+ 增加了对预获取和预加载的支持
在声明 import 时,使用下面这些内置指令,可以让 webpack 输出 “resource hint(资源提示)”,来告知浏览器:

prefetch(预获取):将来某些导航下可能需要的资源 preload(预加载):当前导航下可能需要资源

沿用第五点懒加载中的代码,仅需追加一下懒加载时的魔法注释(webpackPrefetch: truewebpackPreload: true),就可以实现预获取/预加载

  1. // 修改一下代码
  2. btn1.addEventListener('click', () => {
  3. import(/* webpackChunkName: 'math', webpackPrefetch: true */ '../js/math.js')
  4. .then(({ add }) => {
  5. console.log(add(1, 2))
  6. })
  7. })

执行npx webpack后 dist 没有发生改变,再进入浏览器查看资源加载的情况:

  1. 预获取

预加载.gif
发现页面加载的时候还没有执行点击按钮操作 math.bundle.js 已经加载了,当我们进行操作时,它又加载了一次。这么做有什么意义呢?乍一看还不如懒加载,其实它的工作原理是这样的:它在浏览器的中引入了一个,这是告诉浏览器当页面资源加载完了之后,在网络空闲的时候将<link rel="prefetch" href=" ... ">标签中路径下的文件加载下来,在进行操作的时候可以快速使用,是一种比懒加载还要优秀的模式
image.png

  1. 预加载

效果与懒加载类似:加载页面的时候不加载,只有当操作的时候再加载资源,也不会在浏览器的标签上添加标签
预加载.gif
与 prefetch 指令相比,preload 指令有许多不同之处:

  • preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。所以当页面需要请求的资源过多或者网路速度过慢,preload chunk 加载方式会分流导致页面加载速度慢,用户体验差
  • preload chunk 具有中等优先级,并立即下载;prefetch chunk 在浏览器闲置时下载
  • preload chunk 会在父 chunk 中立即请求,用于当下时刻;prefetch chunk 会用于未来的某个时刻
  • 浏览器支持程度不同