Skip to main content

代码拆分 Code Splitting

概述

对于一个大的项目来说,代码都被打包到同一个文件中是相当臃肿的。当其中的某些代码只有在特定场景中才会用到时,这种打包方式的缺点尤为明显。所以,在webpack中允许将代码分割成更小的chunk,只有当代码运行到需要他们的时候再进行加载。通过脚本懒加载,可以使得初始下载的代码更小,有利于减少首屏的渲染时间。

实现方式

实现懒加载JS脚本的方式主要有以下两种:

  • CommonJS: require.ensure()
require.ensure('./text', function (require) {
const result = require('./text');
});
  • ES6: 动态 import(原生未支持,需要 babel 转换)
import('./text').then(result => {});

上述两种方式都可以实现懒加载,但是ES6的动态import更符合我们开发时的编程习惯,所以本文将以动态import来实现代码分割功能。

相关代码

  • text.js(懒加载脚本)
const text = 'dynamic import';

export {text};
  • index.js
import React from 'react';
import ReactDOM from 'react-dom';

class Split extends React.Component {
constructor(props) {
super(props);
this.state = {
text: null
};
}
// 通过onClick事件触发懒加载
loadComponent() {
import('./text').then(result => {
this.setState({
text: result.text
});
});
}
render() {
const {text} = this.state;
return (
<div>
<p>{text ? text : null}</p>
<p onClick={() => this.loadComponent()}>Spilt Code Test</p>
</div>
);
}
}

ReactDOM.render(<Split />, document.getElementById('root'));

index.js对应demo的主页面,通过onClick事件调用回调函数loadComponent实现代码懒加载。

相关配置

  • .babelrc
{
"presets": [
["@babel/preset-env"],
"@babel/preset-react"
],

"plugins": [
"@babel/plugin-syntax-dynamic-import"
]
}

上述presets@babel/preset-env是为了解析ES6语法,@babel/preset-react是为了解析react JSX@babel/plugin-syntax-dynamic-import是为了支持动态import语法。

  • webpack 配置
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
entry: {
split: './src/split/index'
},
output: {
path: path.join(__dirname, 'dist'), // 输出目录
filename: '[name]_bundle.js', // 入口文件打包后的文件名
chunkFilename: '[name]_chunk.js' // 代码分离出块的文件名
},
mode: 'production',
module: {
rules: [
{
test: /.js$/,
use: 'babel-loader'
}
]
},
plugins: [
// 打包 html
new HtmlWebpackPlugin({
template: path.join(__dirname, `src/split/index.html`),
filename: 'split.html',
chunks: ['split']
})
]
};
tip

babel的配置以及webpackentryoutput的配置是实现代码分割的关键。

结果展示

通过npm run build命令构建,生成的文件存放在dist目录中,dist目录如下:

├─dist
│ 1_chunk.js
│ split.html
│ split_bundle.js

其中1_chunk.js为懒加载代码,当import没加魔法注释时默认name为数字,对应split/text.jssplit_bundle.js为入口文件的输出文件,split.htmlsplit/index.html打包后文件。

在浏览器中执行split.html文件,执行结果如下图:

通过上图Network可以看出,执行时并没有引入1_chunk.js文件。

当我们点击页面中的文字时,1_chunk.js脚本被加载,文字'dynamic import'显示在网页中。如下图所示:

项目中所有的代码和配置已经上传到github仓库的code-split中,可以下载运行一下。

魔法注释

通过在import()中添加魔法注释,可以对分割后的chunk进行命名等操作。本节我们将讲解两个常用的注释:/* webpackChunkName: "my-chunk-name" *//* webpackExclude: /\.json$/ */

webpackChunkName

通过之前的运行结果可以看出,被分割的chunk的文件名都是数字,当分割的文件较多时用数字对文件命名显然是反人类的。为了解决这一问题,我们可以使用魔法注释中的webpackChunkName来自定义分割出的chunk名。

  • 动态导入 npm 包

当动态导入的是npm包时,建议webpackChunkName的值设为包名。例如当我们动态导入lodash包时,我们不再需要使用import _ from 'lodash',其配置如下:

import(/* webpackChunkName: "lodash" */ 'lodash').then();

使用webpack打包后的文件名为:

vendors~lodash_chunk.js
  • 动态导入文件:

当动态导入的是文件时,建议webpackChunkName的值设为[request]。例如当我们导入text.js文件时,其配置如下:

import(/* webpackChunkName: "[request]" */ `./text.js`).then();

使用webpack打包后的文件名为:

text_chunk.js

webpackExclude

webpackExclude注释的值是一个正则表达式,符合该正则表达式的所有文件在代码分割时将被排除在外。

例如,demo 的目录如下所示:

src
├─code
│ regexp.json
│ test.js
│ text.js

└─split
index.html
index.js

当我们在spilt/index.js文件中动态导入code目录下的text.js文件时,如果spilt/index.js代码设置如下:

   loadComponent(fileName) {
const path = `${fileName}.js`;
import(/* webpackChunkName: "[request]" */ /* webpackExclude: /\.json$/ */`../code/${path}`).then((result) => {
if(fileName === 'test') {
this.setState({ test: result[fileName] })
}else{
this.setState({ text: result[fileName] })
}
})
}

因为webpack编译前不会分析${path},所以/code目录下的所有.js文件将分别打包成对应的chunk,而.json文件将会被忽略。此处可以通过传递不同的参数来懒加载不同的文件。

打包后的结果为:

├─dist
│ split_bundle.js
│ test_chunk.js
│ text_chunk.js
caution

需要注意的是,不要将字符串模板提取成一个变量,例如:import(code)webpack在编译前不会去推断这个变量名code到底代表什么。因此在 import()中必须至少包含导入模块位置的某些信息以方便调用。

添加魔法注释后的代码可以点击github仓库的code-split-1下载运行。

参考资料