08. 代码分离

184 阅读5分钟

相关代码

Webpack 能够把代码分离到不同的 bundle(打包分离出来的文件) 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大优化加载时间。

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

  • 入口起点:使用 entry 配置项手动地分离代码;
  • 防止重复:使用 Entry dependencies 或者 SplitChunksPlugin 去重和分离 chunk;
  • 动态导入:通过模块的内联函数调用来分离代码。

一、入口起点

入口起点:使用 entry 配置项配置多个入口手动地分离代码,这样就会对多个入口文件分别打包,在 dist 目录生成多个对应的 bundle,从而实现代码分离。

这种方式存在的问题:

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

在 src 目录下创建 another-module.js:

import _ from 'lodash' 
console.log(_.join(['Another', 'module', 'loaded!'], ' '))

这个模块依赖了 lodash,需要安装 lodash:

npm install lodash --save-dev

webpack.config.js:

module.exports = {
    entry: {
        index: './src/index.js',
        another: './src/another-module.js'
    },
    output: {
        filename: '[name].bundle.js',    // 多个入口,不能只配置一个出口文件
    },
}

打包后的结果:

image.png

上图中,只有两行代码的 another-module.js 打包后的体积居然有 554k,这是因为 another-module.js 中引入了 lodash,打包的时候 lodash.js 也被打包进去了。

再来修改一下 index.js,在 index.js 中也引入 lodash:

import helloWorld from './hello-world.js'
import _ from 'lodash' 

helloWorld()
console.log(_.join(['index', 'module', 'loaded!'], ' '))

打包后的结果:

image.png

上图中,对比修改前与修改后的 index.js 打包后的 index.bundle.js,体积明显增大了很多,这是因为引入了 lodash,第二次打包把 lodash 也打包进去了。

二、防止重复

防止重复:使用 Entry dependencies 或者 SplitChunksPlugin 去重和分离 chunk,将公共的文件抽离成单独的 chunk。

1. 入口依赖(Entry dependencies

module.exports = {
    mode: 'development',
    entry: {
        index: { 
            import: './src/index.js', 
            dependOn: 'shared'
        },
        another: { 
            import: './src/another-module.js', 
            dependOn: 'shared'
        },
        shared: 'lodash'
    },
    output: {
        filename: '[name].bundle.js',    // 多个入口,不能只配置一个出口文件
    },
}

image.png

从上图可以看出,index.bundle.js 与 another.bundle.js 共享的模块 lodash.js 被打包到一个单独的文件 shared.bundle.js 中。

2. SplitChunksPlugin

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

module.exports = {
    entry: {
        index: './src/index.js',
        another: './src/another-module.js'
    },
    output: {
        filename: '[name].bundle.js',    // 多个入口,不能只配置一个出口文件
    },
    optimization: { 
        splitChunks: { 
            chunks: 'all', 
        }, 
    }
}

image.png

使用 optimization.splitChunks 配置选项之后,index.bundle.js 和 another.bundle.js 中已经移除了重复的依赖模块,且 lodash 被分离到单独的 chunk。

三、动态导入

动态导入:通过模块的内联函数调用来分离代码。

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

  • 使用 import() 语法来实现动态导入(推荐)
  • 使用 Webpack 特定的 require.ensure(遗留功能)

1. import() 的基本使用

在 async-module.js 中使用 import() 语法 引入 lodash:

function getComponent() {
    return import('lodash')    // 使用 import 动态引入 lodash
      .then(({default: _}) => {
        const element = document.createElement('div')
  
        element.innerHTML = _.join(['Hello', 'webpack'], ' ')
  
        return element
      })
  }
  
  getComponent().then((element) => {
    document.body.appendChild(element)
  })

入口文件 index.js:

import helloWorld from './hello-world.js'
import _ from 'lodash'        // index.js 中引入了 lodash(第一处)
import './async-module.js'    // async-module.js 中引入了 lodash(第二处)

helloWorld()
console.log(_.join(['index', 'module', 'loaded!'], ' '))

webpack.config.js:

module.exports = {
    entry: {
        index: './src/index.js',
        another: './src/another-module.js'    // another-module.js 中引入了 lodash(第三处)
    },
    output: {
        filename: '[name].bundle.js',    // 多个入口,不能只配置一个出口文件
    },
    optimization: { 
        splitChunks: { 
            chunks: 'all', 
        }, 
    }
}

以上代码,有 3 个地方都引入了 lodash,但只打包出了一个 lodash 文件:

image.png

2. import() 的应用:懒加载

懒加载或者按需加载,是一种很好的优化方式。这种方式,实际上是先把代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,再引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。

入口文件 index.js:

const button = document.createElement('button')
button.textContent = '点击执行加法运算'
button.addEventListener('click', () => {
    import(/* webpackChunkName: 'math' */'./math.js').then(({ add }) => {
        console.log(add(4, 5))
    })
})
document.body.appendChild(button)

以上代码:

  • 其中的注释,称为 Webpack 魔法注释:webpackChunkName: 'math' 告诉 Webpack 打包生成的文件名为 math;
  • 由于使用了 import() 动态导入,所以会在 dist 目录中生成一个单独的文件 math.bundle.js;
  • 第一次加载完页面时,dist 目录中的 math.bundle.js 不会被加载,只有当点击按钮后,才会加载 math.bundle.js 文件。

3. import() 的应用:预获取/预加载模块

Webpack v4.6.0+ 增加了对预获取和预加载的支持。

在声明 import 时,使用下面这些内置指令,可以让 Webpack 输出 "resource hint(资源提示)",来告知浏览器:

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

3.1 预获取(prefetch)

入口文件 index.js:

const button = document.createElement('button')
button.textContent = '点击执行加法运算'
button.addEventListener('click', () => {
    import(/* webpackChunkName: 'math', webpackPrefetch: true */'./math.js').then(({ add }) => {
        console.log(add(4, 5))
    })
})
document.body.appendChild(button)

魔法注释中的 webpackPrefetch: true 告诉 Webpack 执行预获取,这会生成 <link rel="prefetch" href="math.js"> 并追加到页面头部,指示浏览器在闲置时加载 math.js 文件。

如图,当页面加载完成,浏览器空闲时就会加载 math.bundle.js:

image.png

3.2 预加载(preload)

入口文件 index.js:

const button2 = document.createElement('button') 
button2.textContent = '点击执行字符串打印' 
button2.addEventListener('click', () => { 
  import(/* webpackChunkName: 'print', webpackPreload: true */ './print.js').then(({ print }) => { 
    print(4, 5) 
  }) 
})
document.body.appendChild(button2)

魔法注释中的 webpackPreload: true 告诉 Webpack 执行预加载。

以上代码,虽然给 print.js 设置了预加载,但也是要等到点击了按钮才会预加载,不会提前加载。

如下图,没有点击按钮时,浏览器不会加载 print.bundle.js:

image.png

如果需要提前加载,可以改成下面这样(直接 import):

import(/* webpackChunkName: 'print', webpackPreload: true */ './print.js').then(({ print }) => {
    print(4, 5)
})

加载页面时,print.bundle.js 就预加载了:

image.png

3.3 预获取(prefetch)与预加载(preload)的区别

  • 加载时机:preload chunk 会在父 chunk 加载时,以并行方式立即加载;prefetch chunk 会在父 chunk 加载结束后浏览器闲置时加载。
  • 使用时机:preload chunk 会在父 chunk 中立即请求,用于当下时刻;prefetch chunk 会用于未来的某个时刻。
  • 浏览器支持程度不同。