接上一篇文章:「 面试三板斧 」之 代码分割(上)
中, 我们分析了配置 splitChunks
不同值后的行为。
本篇我们将深入分析代码分割背后的运行机制。
今天的主要内容包括:
代码分割的思路及方法
切割点的选择
代码分割实例分析
识别与处理切割点
构建 chunks
拼接 output.js
相关资料推荐
文中部分内容参考了网上一些优秀实践以及相关的分析。引用部分将会在文中标注
, 文末也将给出具体链接。
代码分割是
webpack 中最引人注目的特性之一。
此特性能够把代码分离到不同的 bundle 中,然后可以按需加载
或并行加载
这些文件。
代码分割
可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。
且异步 chunk 为懒加载的——执行到 require.ensure 时才拉取并执行。
大体思路如下:
require.ensure
标识新 chunk依赖收集
时,单独标识异步依赖jsonp
函数,由 webpackJsonp
函数包裹执行 require.ensure 的回调
。通常,有三种常用的代码分离方法:
entry
配置手动地分离代码。CommonsChunkPlugin
去重和分离 chunk。内联函数调用
来分离代码。前2种是通过配置实现,第3种是通过代码动态分析实现。
动态导入也有2种写法:
import()
方法require.ensure
方法。「1」 一般说来,code-splitting 有两种含义
:
换句话说,我们的目标是:
将原先集中到一个 output.js 中的代码,切割成若干个 js 文件,然后分别进行加载。
也就是说:原先只加载 output.js。
现在把代码分割到 3 个文件中,先加载 output.js ,然后 output.js 又会自动加载 1.output.js 和 2.output.js 。
现在问题来了:
如何确定分割点
呢?
既然要将一份代码切割成若干份代码,总得有个切割点的标志吧,从哪儿开始切呢?
答案:require.ensure
。
webpack 使用 require.ensure 作为切割点。
分割点表示代码在此处被分割成两个独立的文件。
具体的方式有两种:
require.ensure
amd的动态require
require.ensure(["module-a", "module-b"], function(require) {
var a = require("module-a");
// ...
});
require(["module-a", "module-b"], function(a, b) {
// ...
});
上面的例子中,module-a
和 module-b
就会被分割到独立的文件中去,而不会和入口文件打包在同一个文件中。
然而, 我用 nodeJS 也挺长时间了,怎么不知道还有require.ensure
这种用法?
事实上 nodeJS 也是不支持的,这个问题我在 CommonJS 标准
中找到了答案:
虽然 CommonJS 通俗地讲是一个同步模块加载规范,但是其中是包含异步加载相关内容的。
只不过这条内容只停留在 PROPOSAL(建议)阶段,并未最终进入标准。
所以 nodeJS 没有实现它也就不奇怪了。
只不过 webpack 恰好利用了这个作为代码的切割点。
草案链接:http://wiki.commonjs.org/wiki/Modules/Async/A
ok,现在我们已经明白了为什么要选择require.ensure
作为切割点了。
接下来的问题是:
如何根据切割点对代码进行切割?
下面举个例子。
// example.js
var a = require("a");
var b = require("b");
a();
require.ensure(["c"], function(require) {
require("b")();
var d = require("d");
var c = require('c');
c();
d();
});
require.ensure(['e'], function (require) {
require('f')();
});
假设这个 example.js 就是项目的主入口文件。
模块 a ~ f 是简简单单的模块(既没有进一步的依赖,也不包含require.ensure)。
那么,这里一共有2个切割点,这份代码将被切割为3部分。
也就说,到时候会产生3个文件:
程序如何识别 require.ensure 呢?
答案自然是继续使用强大的 esprima
。
关键代码如下:
// parse.js
if (expression.callee && expression.callee.type === 'MemberExpression'
&& expression.callee.object.type === 'Identifier' && expression.callee.object.name === 'require'
&& expression.callee.property.type === 'Identifier' && expression.callee.property.name === 'ensure'
&& expression.arguments && expression.arguments.length >= 1) {
// 处理require.ensure的依赖参数部分
let param = parseStringArray(expression.arguments[0])
let newModule = {
requires: [],
namesRange: expression.arguments[0].range
};
param.forEach(module => {
newModule.requires.push({
name: module
});
});
module.asyncs = module.asyncs || [];
module.asyncs.push(newModule);
module = newModule;
// 处理require.ensure的函数体部分
if(expression.arguments.length > 1) {
walkExpression(module, expression.arguments[1]);
}
}
观察上面的代码可以看出,识别出require.ensure
之后,会将其存储到 asyncs 数组中,且继续遍历其中所包含的其他依赖。
举个例子,example.js 模块最终解析出来的数据结构如下图所示:
我在刚刚使用 webpack 的时候,是分不清这两个概念的。
现在我可以说:“在上面的例子中,有3个 chunk,分别对应:
有7个 module,分别是: example 和 a ~ f
。
所以,module 和 chunk 之间的关系是:
1个 chunk 可以包含若干个 module
。
观察上面的例子,得出以下结论:
好了,下面进入重头戏。
在对各个模块进行解析之后,我们能大概得到以下这样结构的 depTree
。
下面我们要做的就是:如何从8个 module 中构建出3个 chunk 出来。
这里的代码较长,我就不贴出来了,想看的到这里的 buildDep.js, 地址:https://github.com/youngwind/fake-webpack/commit/d6589263f90752ef8222749208694df654b631e3#diff-92c5d90abece48343aa1cdb71978f37b
其中要重点注意是:
前文说到,为了避免代码的冗余,需要将模块 b 从 chunk1 中移除,具体发挥作用的就是函数removeParentsModules,本质上无非就是改变一下标志位。
最终生成的chunks的结构如下:
经历重重难关,我们终于来到了最后一步:
如何根据构建出来的 chunks 拼接出若干个 output.js 呢?
此处的拼接与上一篇最后提到的拼接大同小异,主要不同点有以下2个:
其实关于 webpack 的代码切割还有很多值得研究的地方。
本文我们实现的例子仅仅是将1个文件切割成3个,并未就其加载时机进行控制。
webapck 打包之后生成了很多代码,中间也有不少细节, 有很多相关的分析文章写的不错, 在此推荐两篇:
篇幅有限,就不过多展开,感兴趣的朋友可以看看。
下篇文章,就是轻松愉快的项目打包优化实践环节
, 敬请期待~
以上就是本文全部内容, 希望对大家有所启发。