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 中定义并exporteachfiltermapreduce三个函数:
// 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

© Sytone 2021 - 2024