yanni4night

利用 Rollup 和 ECMAScript 2015 Modules 实现打包和 Tree-Shaking

yanni4night · 2016-08-11翻译 · 1089阅读 原文链接

利用 Rollup 和 ECMAScript 2015 Modules 实现打包和 Tree-Shaking

2016年6月12日

#ecmascript2015 #javascript

Browserify 和 Webpack 都是优秀的工具,但另一名新秀已经崭露头角:Rollup,所谓的“下一代 JavaScript 模块打包器”。它的思路是使用 ECMAScript 2015 modules 编写你的应用,然后 Rollup 会高效地把它们打包成一个文件。

Rollup,下一代 JavaScript 模块打包器

Rollup 有意思的是它不会对生成的文件增加任何额外的开销。没有用于注册和加载模块的包装函数。通过这种方式,打成的包总是会比 Browserify 或 Webpack 的小。

Rollup 并不会将模块用函数包装起来,而是先计算出整个应用的模块依赖图谱,按照被引入的拓扑图进行排序,最后按照此顺序暴露出被引用的成员。你可以把这个流程认为是按照正确的顺序拼接模块。

使用 Browserify 和 Webpack 打包 CommonJS 模块

在知道如果使用 Rollup 打包模块之前,让我们先看一下 Browserify 和 Webpack 打成的包。我们将要使用两个简单的模块作为例子。在 math.js 内部,我们定义和导出一个简单的 square 函数:

module.exports = {
    square: square
};

function square(x) {
    return x * x;
}

index.js 内,我们把 math.js 导入为 math,然后调用它的 square 方法。

var math = require("./math");

var squared = math.square(7);
console.log(squared);

这是 Browserify 打成的包:

(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
var math = require("./math");

var squared = math.square(7);
console.log(squared);

},{"./math":2}],2:[function(require,module,exports){
module.exports = {
    square: square
};

function square(x) {
    return x * x;
}

},{}]},{},[1]);

这是 Webpack 打成的包:

/******/ (function(modules) { // webpackBootstrap
/******/    // The module cache
/******/    var installedModules = {};

/******/    // The require function
/******/    function __webpack_require__(moduleId) {

/******/        // Check if module is in cache
/******/        if(installedModules[moduleId])
/******/            return installedModules[moduleId].exports;

/******/        // Create a new module (and put it into the cache)
/******/        var module = installedModules[moduleId] = {
/******/            exports: {},
/******/            id: moduleId,
/******/            loaded: false
/******/        };

/******/        // Execute the module function
/******/        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

/******/        // Flag the module as loaded
/******/        module.loaded = true;

/******/        // Return the exports of the module
/******/        return module.exports;
/******/    }

/******/    // expose the modules object (__webpack_modules__)
/******/    __webpack_require__.m = modules;

/******/    // expose the module cache
/******/    __webpack_require__.c = installedModules;

/******/    // __webpack_public_path__
/******/    __webpack_require__.p = "";

/******/    // Load entry module and return exports
/******/    return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {

    var math = __webpack_require__(1);

    var squared = math.square(7);
    console.log(squared);

/***/ },
/* 1 */
/***/ function(module, exports) {

    module.exports = {
        square: square
    };

    function square(x) {
        return x * x;
    }

/***/ }
/******/ ]);

有许多样板代码。虽然压缩后会小很多,但实话说,这部分代码仍存在一定的开销。我们看看 Rollup 是怎么做的。

用 Rollup 打包 ECMAScript 2015 Modules

由于 Rollup 需要 ECMAScript 2015 modules 的格式,我们必须稍稍改一下我们的应用。这是更新后的 math.js 模块,使用了 export 关键字:

export function square(x) {
    return x * x;
}

这是更新后的 index.js 模块,使用了 import 声明导入了 square 方法:

import { square } from "./math";

var squared = square(7);
console.log(squared);

好了,到了见证奇迹的时刻。这是 Rollup 打成的包:

function square(x) {
    return x * x;
}

var squared = square(7);
console.log(squared);

这比上面两个包的体积小多了。注意 Rollup 做了什么:square 方法被内联到 index.js 模块中,所有 importexport 声明都不见了。简单直白。

注意这不是简单地把模块源代码合并到一起。Rollup 会解析模块,自动对冲突的变量名进行重命名,内联的部分不会破坏你的代码。

ECMAScript 2015 Modules 的静态结构

让我们花点时间想想 Rollup 是如何判断一个模块的哪些成员应该被导入或导出的。

ECMAScript 2015 modules 是完全的静态结构。导入和导出声明都必须放在模块的最顶层 —— 也就是说,它们不能被其它语句嵌套。更重要的是,这种限制阻止了用 if 语句有条件地加载模块的做法。

if (Math.random() < 0.5) {
    import foo from "bar"; // 不允许!
}

另外,导入和导出声明不能包含任何动态部分。模块描述符必须是硬编码的字符串,代表模块名或者文件路径。变量和运行时计算的表达式都是非法的:

var moduleName = Math.random() < 0.5 ? "foo" : "bar";
import * as module from moduleName; // 不允许!

上面两个特性保证了 Rollup 能够以静态的方式分析整个应用的依赖图谱,因为所有的导入和导出在编译期就知道了。

使用 Tree-Shaking 消除无用代码

假设 math.js 模块是由别人编写的。虽然基本上你不会使用其 100% 的功能,Browserify 和 Webpack 都会把整个源代码导入到包中。你只需要一瓣香蕉,但你却得到了拿着香蕉的大猩猩和整个雨林。

Rollup 与之不同。它推广了一种叫做 tree-shaking 的技术,即从最终的包中移除无用的代码。应用中仅仅被使用到的部分,和这部分的递归依赖,各自会被装载进 Rollup 生成的包中。

我们通过简单扩展 math.js 模块来解释它。现在导出两个方法,squarecube,都依赖于一个(未导出的)方法 pow

function pow(a, b) {
    return Math.pow(a, b);
}

export function square(x) {
    return pow(x, 2);
}

export function cube(x) {
    return pow(x, 3);
}

index.js 内,我们仍然仅导入 square 方法:

import { square } from "./math";

var squared = square(7);
console.log(squared);

这是 Rollup 生成的包:

function pow(a, b) {
    return Math.pow(a, b);
}

function square(x) {
    return pow(x, 2);
}

var squared = square(7);
console.log(squared);

square 方法被导入了是因为我们直接导入和调用它了,pow 随之被导入了是因为 square 在内部调用了 pow。然而, cube 没有被导入,因为我们没有导入它。我们“摇晃了依赖树”,就是这个意思。

我认为 tree-shaking 未来会非常有前景。移除无用代码能显著地减少包的体积,对 JavaScript 在 Web 上的应用来说特别有益。仅仅使用了 lodash 提供的一小部分函数?没问题,导入就行了!

相关文章