webpack源码学习系列之二:code-splitting(代码切割)

前言

上一篇以后,咱们今天来看看如何实现 webpack 的代码切割(code-splitting)功能,最后实现的代码版本请参考这里。至于什么是 code-splitting ,为何要使用它,请直接参考官方文档前端

目标

通常说来,code-splitting 有两种含义:node

  1. 将第三方类库单独打包成 vendor.js ,以提升缓存命中率。(这一点咱们不做考虑)
  2. 将项目自己的代码分红多个 js 文件,分别进行加载。(咱们只研究这一点)

换句话说,咱们的目标是:将原先集中到一个 output.js 中的代码,切割成若干个 js 文件,而后分别进行加载。 也就是说:原先只加载 output.js ,如今把代码分割到3个文件中,先加载 output.js ,而后 output.js 又会自动加载 1.output.js 和 2.output.js 。jquery

切割点的选择

既然要将一份代码切割成若干份代码,总得有个切割点的标志吧,从哪儿开始切呢?
答案:webpack 使用require.ensure做为切割点。webpack

然而,我用 nodeJS 也挺长时间了,怎么不知道还有require.ensure这种用法?而事实上 nodeJS 也是不支持的,这个问题我在CommonJS 的标准中找到了答案:虽然 CommonJS 通俗地讲是一个同步模块加载规范,可是其中是包含异步加载相关内容的。只不过这条内容只停留在 PROPOSAL (建议)阶段,并未最终进入标准,因此 nodeJS 没有实现它也就不奇怪了。只不过 webpack 刚好利用了这个做为代码的切割点。git

ok,如今咱们已经明白了为何要选择require.ensure做为切割点了。接下来的问题是:如何根据切割点对代码进行切割? 下面举个例子。github

例子

// 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.jsweb

识别与处理切割点

程序如何识别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 模块最终解析出来的数据结构以下图所示:
express

image

module 与 chunk

我在刚刚使用 webpack 的时候,是分不清这两个概念的。如今我能够说:“在上面的例子中,有3个 chunk,分别对应 output.js、1.output.js 、2.output.js;有7个 module,分别是 example 和 a ~ f。json

因此,module 和 chunk 之间的关系是:1个 chunk 能够包含若干个 module。
观察上面的例子,得出如下结论:

  1. chunk0(也就是主 chunk,也就是 output.js)应该包含 example 自己和 a、b 三个模块。
  2. chunk1(1.output.js)是从 chunk0 中切割出来的,因此 chunk0 是 chunk1 的 parent。
  3. 原本 chunk1 应该是包含模块 c、b 和 d 的,可是因为 c 已经被其 parent-chunk(也就是 chunk1)包含,因此,必须将 c 从 chunk1 中移除,这样方能避免代码的冗余。
  4. chunk2(2.output.js)是从 chunk0 中切割出来的,因此 chunk0 也是 chunk2 的 parent。
  5. chunk2 包含 e 和 f 两个模块。

好了,下面进入重头戏。

构建 chunks

在对各个模块进行解析以后,咱们能大概获得如下这样结构的 depTree。

image

下面咱们要作的就是:如何从8个 module 中构建出3个 chunk 出来。 这里的代码较长,我就不贴出来了,想看的到这里的 buildDep.js

其中要重点注意是:前文说到,为了不代码的冗余,须要将模块 c 从 chunk1 中移除,具体发挥做用的就是函数removeParentsModules,本质上无非就是改变一下标志位。最终生成的chunks的结构以下:

image

拼接 output.js

经历重重难关,咱们终于来到了最后一步:如何根据构建出来的 chunks 拼接出若干个 output.js 呢?
此处的拼接与上一篇最后提到的拼接大同小异,主要不一样点有如下2个:

  1. 模板的不一样。原先是一个 output.js 的时候,用的模板是 templateSingle 。如今是多个 chunks 了,因此要使用模板 templateAsync。其中不一样点主要是 templateAsync 会发起 jsonp 的请求,以加载后续的 x.output.js,此处就不加多阐述了。仔细 debug 生成的 output.js 应该就能看懂这一点。
  2. 模块名字替换为模块 id 的算法有所改进。原先我直接使用正则进行匹配替换,可是若是存在重复的模块名的话,好比此例子中 example.js 出现了2次模块 b,那么简单的匹配就会出现错乱。由于 repalces 是从后往前匹配,而正则自己是从前日后匹配的。webpack 原做者提供了一种很是巧妙的方式,具体的代码能够参考这里

后话

其实关于 webpack 的代码切割还有不少值得研究的地方。好比本文咱们实现的例子仅仅是将1个文件切割成3个,并未就其加载时机进行控制。好比说,如何支持在单页面应用切换 router 的时候再加载特定的 x.output.js?

注:更多系列文章请移步个人博客

-------- EOF -----------


本文对你有帮助?欢迎扫码加入前端学习小组微信群:

相关文章
相关标签/搜索