代码拆分 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']
})
]
};
babel
的配置以及webpack
中entry
和output
的配置是实现代码分割的关键。
结果展示
通过npm run build
命令构建,生成的文件存放在dist
目录中,dist
目录如下:
├─dist
│ 1_chunk.js
│ split.html
│ split_bundle.js
其中1_chunk.js
为懒加载代码,当import
没加魔法注释时默认name
为数字,对应split/text.js
,split_bundle.js
为入口文件的输出文件,split.html
为split/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
需要注意的是,不要将字符串模板提取成一个变量,例如:import(code)
,webpack
在编译前不会去推断这个变量名code
到底代表什么。因此在 import()中必须至少包含导入模块位置的某些信息以方便调用。
添加魔法注释后的代码可以点击github
仓库的code-split-1下载运行。