Module bundling

是的这又是一篇搬运文章,谁让我放荡不羁爱打野呢。作者博客地址Preethi Kasireddy

之前有聊过一篇关于Modules的文章,主要讲什么是模块以及为什么需要模块。这篇主要讲怎么捆绑(打包)模块也就是模块使用的深一步探讨。

什么是模块捆绑?

从高层次上讲,模块捆绑只是将一组模块(及其依赖项)按正确顺序拼接到单个文件(或一组文件)中的过程。

为什么需要模块捆绑?

如果项目中使用的模块过多,那么你就需要一个一个的使用script 标签引入到html中去,然后html运行的时候会一个一个的去加载你引入的模块。

像上图那样 one by one 如果用的模块一多,页面的延迟就会过久,用户体验不好。

为了解决这个问题,我们把所有文件捆绑,或“拼接”到一个文件(有时也是一组文件)中,正是为了减少请求数

另一个加速构建操作的常用方法是,“缩减”捆绑后的代码。缩减,是把源代码中不需要的字符(如空格、评论、换行符等等)移除,从而减小了代码的总体积,却不改变其功能。

数据更少,浏览器处理的时间就更短,比如经常见到的”xxxmin.js”,相比完整版,缩减版小了好多。

捆绑模块有哪些不同的方式?

如果你用的是一种标准模块模式(在 第一部分 讨论过)来定义模块,比如IIFE、全局导入等,其实就是直接拼接和缩减几堆纯 JavaScript 代码,通常则不用借助工具。

但如果你用的是非原生模块系统,浏览器不能像 CommonJS、AMD、甚至原生 ES6 模块格式那样解析,你就需要用专门工具先转化成排列正确、浏览器可识别的代码。这正是 Browserify、RequireJS、Webpack 和其他模块捆绑工具,或模块加载工具粉墨登场的时候。

下面就来过一遍常用的模块捆绑方法:

Bundling CommonJS

第一部分所知,CommonJS同步加载模块,它对于浏览器是不适用的。 解决此问题其中一个方法是用Browserify。 Browserify是一个为浏览器编译CommonJS模块的工具。

例如,假设您有一个main.js文件,该文件导入一个模块来计算数字数组的平均值:

1
2
3
var myDependency = require('myDependency');
var myGrades = [93,95,88,0,91];
var myAverageGrade = myDependency.average(myGrades);

因此,在这种情况下,我们只有一个依赖项(myDependency)。 使用以下命令,Browserify将递归所有必需的模块(从main.js开始)并把它们捆绑到一个名为bundle.js的文件中:

1
browserify main.js -o bundle.js

Browserify通过跳入每个require 调用的AST解析来遍历项目的整个依赖关系图来实现此目的。 一旦确定了依存关系的结构,便会将它们按正确的顺序捆绑到一个文件中。当需要使用时,则将“bundle.js” 文件的<script>标记插入到html中,以确保所有源代码都在一个HTTP请求中下载。

同样,如果您有多个具有多个依赖项的文件,则只需告诉Browserify您的条目文件是什么,然后静静等待Browserify进行文件捆绑。

最后,可以使用Minify-JS之类的工具来缩小捆绑代码。

Bundling AMD

如果是AMD,则需要使用诸如RequireJS或Curl之类的AMD加载器。 模块加载器(相对于捆绑器)动态加载程序需要的模块。

AMD与CommonJS的主要区别之一是它加载模块的方式为异步。 所以对于AMD,从技术上讲,实际上不需要构建步骤,因为是异步加载模块的,因此无需将模块捆绑到一个文件中。这意味着运行程序时仅会逐步下载执行该命令所必需的那些文件,而不是在用户首次访问该页面时立即下载所有文件。

但是,实际上,随着时间的推移,随着产品复杂性的增加,随着程序依赖的模块越来越多, 大多数情况下仍然使用构建工具来捆绑和缩小其AMD模块,以实现最佳性能,例如使用诸如RequireJS优化器,r.js之类的工具。

总的来说,AMD与CommonJS之间的区别在于:AMD是异步加载,CommonJS时同步加载。

有关CommonJS与AMD的有趣讨论,请查看Tom Dale博客中的这篇文章。

ES6 modules

接下来谈谈ES6模块,它在某些方面可以减少将来对捆绑器的需求。

当前的JS模块格式(CommonJS,AMD)和ES6模块之间最重要的区别是ES6模块在设计时考虑了静态分析。 这意味着在导入模块时,将在编译时(即在脚本开始执行之前)解决导入问题。 这使我们可以在运行程序之前删除其他模块未使用的导出。 删除未使用的导出可以节省大量空间,从而减轻浏览器的压力。

一个常见的问题是:与使用UglifyJS之类的代码来缩小代码时所产生的死代码消除有什么不同?这得分情况 。

有时,清除死代码可能在UglifyJS和ES6模块之间完全相同,而有时则不行。

使ES6模块与众不同的是消除死代码的不同方法,称为tree shaken。 tree shaken本质上是消除死代码的反向操作。 它仅包含捆绑软件需要运行的代码,而不排除捆绑软件不需要的代码。 让我们看一个例子:

假设我们有一个utils.js文件,其中包含以下函数,我们使用ES6语法导出每个函数::

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
export function each(collection, iterator) {
if (Array.isArray(collection)) {
for (var i = 0; i < collection.length; i++) {
iterator(collection[i], i, collection);
}
} else {
for (var key in collection) {
iterator(collection[key], key, collection);
}
}
}


export function filter(collection, test) {
var filtered = [];
each(collection, function(item) {
if (test(item)) {
filtered.push(item);
}
});
return filtered;
}


export function map(collection, iterator) {
var mapped = [];
each(collection, function(value, key, collection) {
mapped.push(iterator(value));
});
return mapped;
}


export function reduce(collection, iterator, accumulator) {
var startingValueMissing = accumulator === undefined;


each(collection, function(item) {
if(startingValueMissing) {
accumulator = item;
startingValueMissing = false;
} else {
accumulator = iterator(accumulator, item);
}
});


return accumulator;
}

接下来,假设我们不知道要在程序中使用哪些utils函数,因此我们继续将所有模块导入main.js中,如下所示:

1
import * as Utils from ‘./utils.js’;

然后我们最终只使用each函数:

1
2
3
4
import * as Utils from ‘./utils.js’;


Utils.each([1, 2, 3], function(x) { console.log(x) });

模块加载后,main.js文件的“tree shaken”版本将如下所示::

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function each(collection, iterator) {
if (Array.isArray(collection)) {
for (var i = 0; i < collection.length; i++) {
iterator(collection[i], i, collection);
}
} else {
for (var key in collection) {
iterator(collection[key], key, collection);
}
}
};


each([1, 2, 3], function(x) { console.log(x) });

所以你会发现除了 each函数其他函数都没有了.

Meanwhile, if we decide to use the filter function instead of the each function, we wind up looking at something like this:

1
2
3
4
import * as Utils from ‘./utils.js’;


Utils.filter([1, 2, 3], function(x) { return x === 2 });

The tree shaken version looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function each(collection, iterator) {
if (Array.isArray(collection)) {
for (var i = 0; i < collection.length; i++) {
iterator(collection[i], i, collection);
}
} else {
for (var key in collection) {
iterator(collection[key], key, collection);
}
}
};


function filter(collection, test) {
var filtered = [];
each(collection, function(item) {
if (test(item)) {
filtered.push(item);
}
});
return filtered;
};


filter([1, 2, 3], function(x) { return x === 2 });

注意这次是如何同时包含filtereach的。 这是filter使用each,因此我们需要导出这两个函数模块才能正常工作.

有兴趣可以试试Rollup.js live demo and editor。看看它对Tree-Shaking的应用。

Webpack

就打包器而言,Webpack是新手。 它的设计与您使用的模块系统无关,允许开发人员酌情使用CommonJS,AMD或ES6。

您可能想知道为什么当我们已经有其他捆绑器(如Browserify和RequireJS)完成工作并做得很好时,为什么我们需要Webpack。 其中一个原因是,Webpack提供了一些有用的功能,例如“代码拆分”-一种将您的代码库拆分为“块”(按需加载)的方法。

例如,如果您的Web应用程序仅在某些情况下需要使用代码块,则将整个代码库放入单个大型捆绑文件中可能没有效率。 在这种情况下,您可以使用代码拆分将代码提取为可按需加载的捆绑块,从而避免了在大多数用户只需要应用程序核心的情况下使用较大的前期有效负载的麻烦。

代码拆分只是Webpack提供的众多引人注目的功能之一,并且Internet上对于Webpack还是Browserify更好是有很多意见。 以下是一些我认为有助于解决问题的更高级的讨论::

小结

这篇文章比较老了,但是它对我来说是在于扫盲,毕竟概念性的东西是不过时的。

  • CommonJs主要针对服务端,AMD主要针对浏览器端。(顺便提一下,针对服务器端和针对浏览器端有什么本质的区别呢?服务器端一般采用同步加载文件,也就是说需要某个模块,服务器端便停下来,等待它加载再执行。而浏览器端要保证效率,需要采用异步加载,这就需要一个预处理,提前将所需要的模块文件并行加载好。)

  • AMD是预加载,在并行加载js文件同时,还会解析执行该模块(因为还需要执行,所以在加载某个模块前,这个模块的依赖模块需要先加载完成),即不能懒加载。不过因为AMD并行解析,加载快速,所以同一时间可以解析多个文件。当然也因为并行加载,异步处理,加载顺序不一定,所以不注意的情况下可能会造成程序会出现一些问题。

  • ES6 module的静态分析,配合tree sharking特性,做到用那些导哪些,也不用像AMD那样需要用define包装函数,当然这篇文章比较老了啊,现在其实ES6 module已经是普及了。