前言
继上一篇 #99 之后,我们今天来看看如何实现 webpack 的代码切割(code-splitting)功能,最后实现的代码版本请参考这里。至于什么是 code-splitting ,为什么要使用它,请直接参考官方文档。
目标
一般说来,code-splitting 有两种含义:
- 将第三方类库单独打包成 vendor.js ,以提高缓存命中率。(这一点我们不作考虑)
- 将项目本身的代码分成多个 js 文件,分别进行加载。(我们只研究这一点)
换句话说,我们的目标是:将原先集中到一个 output.js 中的代码,切割成若干个 js 文件,然后分别进行加载。 也就是说:原先只加载 output.js ,现在把代码分割到3个文件中,先加载 output.js ,然后 output.js 又会自动加载 1.output.js 和 2.output.js 。
切割点的选择
既然要将一份代码切割成若干份代码,总得有个切割点的标志吧,从哪儿开始切呢?
答案:webpack 使用require.ensure
作为切割点。
然而,我用 nodeJS 也挺长时间了,怎么不知道还有require.ensure
这种用法?而事实上 nodeJS 也是不支持的,这个问题我在CommonJS 的标准中找到了答案:虽然 CommonJS 通俗地讲是一个同步模块加载规范,但是其中是包含异步加载相关内容的。只不过这条内容只停留在 PROPOSAL (建议)阶段,并未最终进入标准,所以 nodeJS 没有实现它也就不奇怪了。只不过 webpack 恰好利用了这个作为代码的切割点。
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个文件:output.js ,1.output.js ,2.output.js
识别与处理切割点
程序如何识别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 模块最终解析出来的数据结构如下图所示:
module 与 chunk
我在刚刚使用 webpack 的时候,是分不清这两个概念的。现在我可以说:“在上面的例子中,有3个 chunk,分别对应 output.js、1.output.js 、2.output.js;有7个 module,分别是 example 和 a ~ f。
所以,module 和 chunk 之间的关系是:1个 chunk 可以包含若干个 module。
观察上面的例子,得出以下结论:
- chunk0(也就是主 chunk,也就是 output.js)应该包含 example 本身和 a、b 三个模块。
- chunk1(1.output.js)是从 chunk0 中切割出来的,所以 chunk0 是 chunk1 的 parent。
- 本来 chunk1 应该是包含模块 c、b 和 d 的,但是由于 b 已经被其 parent-chunk(也就是 chunk1)包含,所以,必须将 b 从 chunk1 中移除,这样方能避免代码的冗余。
- chunk2(2.output.js)是从 chunk0 中切割出来的,所以 chunk0 也是 chunk2 的 parent。
- chunk2 包含 e 和 f 两个模块。
好了,下面进入重头戏。
构建 chunks
在对各个模块进行解析之后,我们能大概得到以下这样结构的 depTree。
下面我们要做的就是:如何从8个 module 中构建出3个 chunk 出来。 这里的代码较长,我就不贴出来了,想看的到这里的 buildDep.js 。
其中要重点注意是:前文说到,为了避免代码的冗余,需要将模块 b 从 chunk1 中移除,具体发挥作用的就是函数removeParentsModules
,本质上无非就是改变一下标志位。最终生成的chunks的结构如下:
拼接 output.js
经历重重难关,我们终于来到了最后一步:如何根据构建出来的 chunks 拼接出若干个 output.js 呢?
此处的拼接与上一篇最后提到的拼接大同小异,主要不同点有以下2个:
- 模板的不同。原先是一个 output.js 的时候,用的模板是 templateSingle 。现在是多个 chunks 了,所以要使用模板 templateAsync。其中不同点主要是 templateAsync 会发起 jsonp 的请求,以加载后续的 x.output.js,此处就不加多阐述了。仔细 debug 生成的 output.js 应该就能看懂这一点。
- 模块名字替换为模块 id 的算法有所改进。原先我直接使用正则进行匹配替换,但是如果存在重复的模块名的话,比如此例子中 example.js 出现了2次模块 b,那么简单的匹配就会出现错乱。因为 repalces 是从后往前匹配,而正则本身是从前往后匹配的。webpack 原作者提供了一种非常巧妙的方式,具体的代码可以参考这里。
后话
其实关于 webpack 的代码切割还有很多值得研究的地方。比如本文我们实现的例子仅仅是将1个文件切割成3个,并未就其加载时机进行控制。比如说,如何支持在单页面应用切换 router 的时候再加载特定的 x.output.js?
——– EOF ———–
看完两件小事
如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:
- 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
- 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程
本文来源于网络,其版权属原作者所有,如有侵权,请与小编联系,谢谢!
转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com
标题:webpack源码学习系列之二:code-splitting(代码切割)
链接:https://www.javascriptc.com/1350.html
原文链接:https://github.com/youngwind/blog/issues/100