JavaScript Modules Part 2: Module Bundling笔记
date
Feb 27, 2018
slug
module-bundling
status
Published
tags
JavaScript
summary
type
Post
前言
自己刚开始接触前端的时候,前端生态圈已处于一个蓬勃发展的阶段,各类打包工具层出不穷。Github 上的许多脚手架项目如 create-react-app 都支持零配置启动一个前端项目。工具的便捷能让开发者不再为复杂的打包工具的配置而烦恼,但是这并不代表开发者可以不去了解打包工具运作的原理。通过精读 JavaScript Modules Part 2: Module Bundling 这篇文章,我们将更加深入得了解前端项目的模块打包。这篇文章主要内容分以下几点:
- 为什么我们要将模块打包
- 现今流行的几种打包方案
- JavaScript 模块化的未来
笔记
什么是模块打包
模块打包是一个将一组模块(及其依赖)按照正确的顺序打包成一个文件(或多个文件)的过程。
为什么要打包
不同 JavaScript 模块通常以不同的
.js
文件存在,那么在 HTML 文件中需要引入多个 js 文件,导致浏览器在渲染页面时产生较多的 HTTP 请求。
要提高页面性能,我们往往需要将不同 js 文件串联起来,并打包成一个(或者多个)文件。
当然,打包工具所做的工作不仅仅是将不同模块串联起来那么简单。模块打包的不同方式
上一篇文章中提到
,现今在 JavaScript 生态圈中有多种模块化方案,如 CommonJS、AMD、ES6 Modules等,那么对应不同的模块化方案,也存在着不同的打包工具。这也意味着,打包工具的另一项工作就是需要将打包前的代码转换成
browser-friendly
的代码。CommonJS 模块的打包工具——Browserify
Browserify 是一个为浏览器编译 CommonJS 模块的工具。
Browserify 使用示例
假设我们在
main.js
中引入 myDependency 模块来计算一个数组的平均值。// main.js
var myDependency = require(‘myDependency’);
var myGrades = [93, 95, 88, 0, 91];
var myAverageGrade = myDependency.average(myGrades);
然后使用 Browserify 打包:
browserify main.js -o bundle.js
运作原理
通过为每一个
require
语句编译 abstract syntax tree (AST),来遍历整个项目的整个依赖关系,并根据依赖关系按顺序将各个模块打包成一个文件。构建 AMD 模块
在浏览器中,我们通常使用 Require.js 和 Curl来动态加载 AMD 模块。
由于 AMD 是异步加载模块的,所以理论上讲我们不需要将 AMD 模块打包成一个模块,浏览器只会逐步请求那些执行程序所必需的文件,而不是在用户第一次访问页面的时候一次性下载所有模块。
在开发环境中,AMD 应用不需要进行构建(打包)。
但是当应用发布至生产环境时,开发者们还是会使用构建工具来打包 AMD 模块并压缩代码来实现最佳性能,使用诸如 Require.js 优化器——r.js 之类的工具。
Webpack
Webpack 支持 CommonJS、AMD 及 ES6 Modules 等各种模块化方案来组织前端应用的依赖关系。另外,Webpack 还支持代码分割(code splitting),它能将代码分割为一个个按需加载的 chunk。
webpack 官网:https://webpack.js.org
ES6 modules
ES6 模块系统与 CommonJS、AMD 之间最大的区别在于 ES6 模块系统具有静态分析(static analysis)的特性——当你
import
一个模块时,import
在编译时就被解析了。基于这个特性,通过 Webpack 和 Rollup 等构建工具便能做到 Tree Shaking。Tree Shaking
tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES6 模块系统中的静态结构特性,例如 import 和 export。
下面是一个简单的例子。util.js 中定义并
export
了each
、filter
、map
和reduce
三个函数:// util.js
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;
}
然后首先在
main.js
引入util.js
中的所有函数:// main.js
import * as Utils from ‘./utils.js’;
仅使用
each
函数:// main.js
import * as Utils from ‘./utils.js’;
Utils.each([1, 2, 3], function(x) { console.log(x) });
经过 Tree shaking 处理之后,
main.js
等价于: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) });
HTTP/2 会使模块化构建过时吗?
使用 HTTP/1,每个 TCP 连接只允许发出一个请求。这就是为什么加载多个资源需要多个请求。而 HTTP/2 是多路复用的,这意味着多个请求和响应可以并行发生。因此,我们可以通过单个连接同时处理多个请求。
在 HTTP/2 的支持下,加载一堆模块似乎不再是一个巨大的性能问题。
但是,模块打包工具所做的不仅仅是打包,比如它通常会移除无用代码以节省空间等……若要追求极致性能,模块化构建还是很有必要的。
另外,我们离所有的网站都采用 HTTP/2 传输还有很长一段时间。短期内使用模块化构建还是很有必要的。
HTTP/2 与 HTTP/1 的详细对比可见:https://http2.github.io/faq/#what-are-the-key-differences-to-http1x