JavaScript Module Systems Showdown: CommonJS vs AMD vs ES2015

原文链接:https://auth0.com/blog/javascript-module-systems-showdown/。作者:https://twitter.com/speyrott?lang=en

随着JavaScript开发越来越普遍,命名空间和依赖关系变得越来越难以处理。于是出现了以模块化的不同解决方案来处理这个问题。在这篇文章中,我们将探讨开发者目前使用的不同解决方案以及他们试图解决的问题。请看下文。


Different pieces of software are usually developed in isolation until some requirement needs to be satisfied by a previously existing piece of software. At the moment that other piece of software is brought into the project a dependency is created between it and the new piece of code. Since these pieces of software need to work together, it is of importance that no conflicts arise between them. This may sound trivial, but without some sort of encapsulation it is a matter of time before two modules conflict with each other. This is one of the reasons elements in C libraries usually carry a prefix:

简介:我们为什么需要Javascript模块化
如果你熟悉其他开发平台,你可能了解“封装”和“依赖”的概念。通常我们是孤立开发不同代码片段直到我们需要依赖之前已经存在的代码。当我们需要将其他软件片段引入项目时,它与新的代码段之间产生依赖关系。由于新旧两段代码要一起运行,所以他们之间不能产生冲突。这听起来可能不算什么,但是如果不经过封装,两个模块之间发生冲突只是时间问题。这是C库中元素通常带有前缀的原因之一:

#ifndef MYLIB_INIT_H
#define MYLIB_INIT_H

enum mylib_init_code {
    mylib_init_code_success,
    mylib_init_code_error
};

enum mylib_init_code mylib_init(void);

// (...)

#endif //MYLIB_INIT_H

When it comes to dependencies, in traditional client-side JavaScript development, they are implicit. In other words, it is the job of the developer to make sure dependencies are satisfied at the point any block of code is executed. Developers also need to make sure dependencies are satisfied in the right order (a requirement of certain libraries).
The following example is part of Backbone.js's examples. Scripts are manually loaded in the correct order:

封装对于预防冲突和提高代码的可维护性有很重要的作用。
谈到依赖关系,在传统的Javascript开发中,往往都认为是隐式的。换句话说,开发人员只要确保在执行任何代码块的时候确保依赖关系得到满足。
开发人员还需要确保依赖性以正确的顺序(某些库的要求)得到满足。
以下示例是Backbone.js示例的一部分。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Backbone.js Todos</title>
        <link rel="stylesheet" href="todos.css"/>
    </head>

    <body>
        <script src="../../test/vendor/json2.js"></script>
        <script src="../../test/vendor/jquery.js"></script>
        <script src="../../test/vendor/underscore.js"></script>
        <script src="../../backbone.js"></script>
        <script src="../backbone.localStorage.js"></script>
        <script src="todos.js"></script>
    </body>

    <!-- (...) -->

</html>

As JavaScript development gets more and more complex, dependency management can get cumbersome. Refactoring is also impaired: where should newer dependencies be put to maintain proper order of the load chain?

JavaScript module systems attempt to deal with these problems and others. They were born out of necessity to accommodate the ever growing JavaScript landscape. Let's see what the different solutions bring to the table.

随着JavaScript开发变得越来越复杂,依赖关系管理变得越来越麻烦。重构也面临挑战:我们应该采用哪些新的依赖关系来维护负载链的正确顺序?

JavaScript模块化系统准备处理上述问题。他们应景而生,以适应不断增长的JavaScript场景。让我们看看被提上台面的不同解决方案。


An Ad-Hoc Solution: The Revealing Module Pattern

一个特色解决方案:揭示模块模式

Most module systems are relatively recent. Before they were available, a particular programming pattern started getting used in more and more JavaScript code: the revealing module pattern.
大多数的模块化系统的概念都是最近被提出的,在这些模块化系统还未被投入使用之前,有一种特殊的设计模式:揭示模块模式。被广泛运用于Javascript代码当中。

var myRevealingModule = (function () {
    var privateVar = "Ben Cherry",
        publicVar = "Hey there!";

    function privateFunction() {
        console.log( "Name:" + privateVar );
    }

    function publicSetName( strName ) {
        privateVar = strName;
    }

    function publicGetName() {
        privateFunction();
    }

    // Reveal public pointers to
    // private functions and properties
    return {
        setName: publicSetName,
        greeting: publicVar,
        getName: publicGetName
    };
})();

myRevealingModule.setName( "Paul Kinlan" );

这是 Addy Osmani's JavaScript Design Patterns 这本书上的例子。

JavaScript scopes (at least up to the appearance of let in ES2015) work at the function level. In other words, whatever binding is declared inside a function cannot escape its scope. It is for this reason the revealing module pattern relies on functions to encapsulate private contents (as many other JavaScript patterns).

Javascript的作用域(在ES2015的let语法之前)都是在函数层的。换言之,任何在函数内部声明的变量都不能脱离它的作用域。正是由于这个原因,揭示模块模式与其他JavaScript设计模式一样依赖于封装私有变量。

In the example above, public symbols are exposed in the returned dictionary. All other declarations are protected by the function scope enclosing them. It is not necessary to use var and an immediate call to the function enclosing the private scope; a named function can be used for modules as well.
在上面的例子当中,公有方法暴露在返回的API当中。其他的函数变量声明都被包裹在函数作用域的一个闭包之中。这时就没有必要使用var配合立即调用封闭私有范围的函数(IIFE); 一个命名函数也可以用于实现模块化。
This pattern has been in use for quite some time in JavaScript projects and deals fairly nicely with the encapsulation matter. It does not do much about the dependencies issue. Proper module systems attempt to deal with this problem as well. Another limitation lies in the fact that including other modules cannot be done in the same source (unless using eval).
这种模式在JavaScript项目中已经使用了相当一段时间,也不错的处理了封装。它对依赖性问题没也有太多的影响。但实际中的另一个局限性在于,如果不使用eval的话,别的模块会有同源限制。

优点

  • 简单到可以在任何平台任何语言中使用
  • 可以在单个文件中定义多个模块。

缺点

No way to programmatically import modules (except by using eval).
Dependencies need to be handled manually.
Asynchronous loading of modules is not possible.
Circular dependencies can be troublesome.
Hard to analyze for static code analyzers.

  • 没有办法程序化导入模块(除了使用eval)
  • 依赖关系需要手动处理
  • 无法实现模块的异步加载
  • 处理循环依赖很棘手
  • 很难解析静态代码解析器

CommonJS

CommonJS is a project that aims to define a series of specifications to help in the development of server-side JavaScript applications. One of the areas the CommonJS team attempts to address are modules. Node.js developers originally intended to follow the CommonJS specification but later decided against it. When it comes to modules, Node.js's implementation is very influenced by it:
CommonJS是一个旨在定义一系列规范以帮助开发服务器端JavaScript应用程序的项目。CommonJS团队尝试解决的领域之一是模块化。Node.js开发人员最初打算遵循CommonJS规范,但后来否决了。因为当涉及到模块时,Node.js的实现受到很大的影响:

// In circle.js
const PI = Math.PI;

exports.area = (r) => PI * r * r;

exports.circumference = (r) => 2 * PI * r;

// In some file
const circle = require('./circle.js');
console.log( `The area of a circle of radius 4 is ${circle.area(4)}`);

There are abstractions on top of Node.js's module system in the form of libraries that bridge the gap between Node.js's modules and CommonJS. For the purposes of this post, we will only show the basic features which are mostly the same.

在Node.js的模块系统之上,以库的形式提供了抽象,缩小了Node.js模块和CommonJS之间的差距。在下文中,我们将只展示他们之间基本相同的基本功能。
In both Node's and CommonJS's modules there are essentially two elements to interact with the module system: require and exports. require is a function that can be used to import symbols from another module to the current scope. The parameter passed to require is the id of the module. In Node's implementation, it is the name of the module inside the node_modules directory (or, if it is not inside that directory, the path to it). exports is a special object: anything put in it will get exported as a public element. Names for fields are preserved. A peculiar difference between Node and CommonJS arises in the form of the module.exports object. In Node, module.exports is the real special object that gets exported, while exports is just a variable that gets bound by default to module.exports. CommonJS, on the other hand, has no module.exports object. The practical implication is that in Node it is not possible to export a fully pre-constructed object without going through module.exports:

Node和CommonJS的模块自身都有两个方法和模块系统进行交互:requireexportsrequire是一种用于将对象从别的模块引入当前作用域的方法。传递给require的参数是模块的ID。在node中,它是node_modules目录中的模块的名称(如果它不在该目录中,那么就是它的路径)。exports导出的是一个特殊的对象:它传入的参数将被导出为一个公共元素。文件名称将会被保留。Node和CommonJS的特殊区别之处在于module.exports导出对象的形式。在Node中,module.exports是导出的真正的对象,而exports只是module.exports的一个引用。然而,CommonJS没有module.exports 导出对象。实际上,在Node中,必须经过module.exports才能导出完全预构建的对象:

// 不能正常运行,exports是引用 module.exports的值,exports在module.exports 被改变后,失效。
// modules.exports.
exports =  (width) => {
  return {
    area: () => width * width
  };
}

// This works as expected.
module.exports = (width) => {
  return {
    area: () => width * width
  };
}

CommonJS modules were designed with server development in mind. Naturally, the API is synchronous. In other words, modules are loaded at the moment and in the order they are required inside a source file.
CommonJS模块的实际考虑到了服务器端的开发。当然这个API不是异步的。换句话说,源文件中的模块是当按顺序按需加载的。

优点

Simple: a developer can grasp the concept without looking at the docs.
Dependency management is integrated: modules require other modules and get loaded in the needed order.
require can be called anywhere: modules can be loaded programmatically.
Circular dependencies are supported.

  • 简单: 开发者不用看文档就能理解这个概念。
  • 集成依赖管理:模块之间依赖,并按需要加载。
  • require可以在任何地方调用:恶意程序化加载模块
  • 支持依赖循环

缺点

Synchronous API makes it not suitable for certain uses (client-side).
One file per module.
Browsers require a loader library or transpiling.
No constructor function for modules (Node supports this though).
Hard to analyze for static code analyzers.

  • 同步API使其不适合某些场景(浏览器客户端)
  • 一个模块就是一个文件
  • 浏览器需要依赖库或者编译
  • 没有模块的构造函数(Node已经支持)
  • 很难解析静态代码解析器

实现

We have already talked about one implementation (in partial form): Node.js.
我们之前说到一种实现(部分形式):Node.js。
For the client there are currently two popular options: webpack and browserify. Browserify was explicitly developed to parse Node-like module definitions (many Node packages work out-of-the-box with it!) and bundle your code plus the code from those modules in a single file that carries all dependencies. Webpack on the other hand was developed to handle creating complex pipelines of source transformations before publishing. This includes bundling together CommonJS modules.

在客户端现在流行两种选择:webpack 以及 browserify。Browserify是一种类似Node化的模块管理工具(许多Node包和他配合都是开箱机用),也就是让服务器端的CommonJS格式的模块可以运行在浏览器端。并将您的代码合并入别的模块的代码并加载到拥有所有依赖项的单个文件中。而Webpack是在发布之前用复杂管线来处理源文件。这包括将CommonJS模块打包在一起。

异步模块定义 (AMD)

AMD was born out of a group of developers that were displeased with the direction adopted by CommonJS. In fact, AMD was split from CommonJS early in its development. The main difference between AMD and CommonJS lies in its support for asynchronous module loading.

AMD出自一组对CommonJS的发展方向感到不满的开发小组。其实AMD很早就从CommonJS的开发中脱颖而出,AMD相比CommonJS的主要优势就是它支持模块异步加载。

//Calling define with a dependency array and a factory function
define(['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

// Or:
define(function (require) {
    var dep1 = require('dep1'),
        dep2 = require('dep2');

    return function () {};
});

Asynchronous loading is made possible by using JavaScript's traditional closure idiom: a function is called when the requested modules are finished loading. Module definitions and importing a module is carried by the same function: when a module is defined its dependencies are made explicit. An AMD loader can therefore have a complete picture of the dependency graph for a given project at runtime. Libraries that do not depend on each other for loading can thus be loaded at the same time. This is particularly important for browsers, where startup times are essential to a good user experience.

使用Javascrpt的传统语法 闭包可以实现异步加载:函数在请求模块加载完成时调用。模块的定义和导入都是由一个函数来完成的:当模块定义后,它的依赖也被明确。AMD加载器因此可以得到完整的依赖项的加载时间,这对于浏览器端来说特别重要,因为启动时间对于用户体验至关重要。

优点

Asynchronous loading (better startup times).
Circular dependencies are supported.
Compatibility for require and exports.
Dependency management fully Asynchronous loading (better startup times).
Circular dependencies are supported.
Compatibility for require and exports.
Dependency management fully integrated.
Modules can be split in multiple files if necessary.
Constructor functions are supported.
Plugin support (custom loading steps).
.

  • 异步加载(良好的首屏时间)
  • 支持循环依赖
  • 兼容requireexports
  • 完全集成依赖管理
  • 如果需要模块可以拆分为多个文件
  • 支持构造函数
  • 支持插件(自定义加载先后)

缺点

Slightly more complex syntactically.
Loader libraries are required unless transpiled.
Hard to analyze for static code analyzers.

  • 异步让代码看起来更复杂
  • 如果不编译,就要引入加载库
  • 很难解析静态代码解析器

实现

目前最受欢迎的AMD是require.js and Dojo.

Require.js for JavaScript Modules
Require.js for JavaScript Modules

Using require.js is pretty straightforward: include the library in your HTML file and use the data-main attribute to tell require.js which module should be loaded first. Dojo has a similar setup.
使用requirejs更为简单:在你的HTML文件中引入require文件,然后使用该data-main属性来告诉require.js应该首先加载哪个模块。
Dojo 也类似.

ES2015 模块化

Fortunately, the ECMA team behind the standardization of JavaScript decided to tackle the issue of modules. The result can be seen in the latest release of the JavaScript standard: ECMAScript 2015 (previously known as ECMAScript 6). The result is syntactically pleasing and compatible with both synchronous and asynchronous modes of operation.
喜大普奔,ECMA幕后团队已经决定解决模块化的问题。我们可以再最新的JavaScript标准中看到:ECMAScript 2015 (以前叫ES6).
ECMAScript 2015在语法上兼容了同步和异步两种模式

//------ lib.js ------
export const sqrt = Math.sqrt;
export function square(x) {
    return x * x;
}
export function diag(x, y) {
    return sqrt(square(x) + square(y));
}

//------ main.js ------
import { square, diag } from 'lib';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5

The import directive can be used to bring modules into the namespace. This directive, in contrast with require and define is not dynamic (i.e. it cannot be called at any place). The export directive, on the other hand, can be used to explicitly make elements public.

The static nature of the import and export directive allows static analyzers to build a full tree of dependencies without running code. ES2015 does not support dynamic loading of modules, but a draft specification does:

import指令可以将模块引入命名空间。但requiredefine这两个指令却不是动态的(不能随便在任何地方调用)。export指令是将模块暴露为公有。
静态特性importexport指令允许静态解析器没有在不运行的代码情况下建立依赖关系的一个完整的树。ES2015不支持动态加载模块,但草案规范如下:

System.import('some_module')
      .then(some_module => {
          // Use some_module
      })
      .catch(error => {
          // ...
      });

In truth, ES2015 only specifies the syntax for static module loaders. In practice, ES2015 implementations are not required to do anything after parsing these directives. Module loaders such as System.js are still required. A draft specification for browser module loading is available.

实际上,ES2015 只指定静态模块解析器的语法。实际上,解析这些指令后,ES2015不需要执行任何操作。仍然需要loader,如System.js。浏览器模块加载草案规范可用

This solution, by virtue of being integrated in the language, lets runtimes pick the best loading strategy for modules. In other words, when asynchronous loading gives benefits, it can be used by the runtime.
ES6这种解决方案应该是模块化的最佳解决策略,因为是集成在语言中的。当文件异步加载时,加载时间就会大大缩短。

更新(2017年2月):现在有一个动态加载模块规范
这是ECMAScript标准未来版本的提案。

优点

ynchronous and asynchronous loading supported.
Syntactically simple.
Support for static analysis tools.
Integrated in the language (eventually supported everywhere, no need for libraries).
Circular dependencies supported.

  • 支持同步异步加载
  • 支持静态解析器
  • 集成在语言本身(不用引入库)
  • 支持循环依赖

缺点

Still not supported everywhere.

  • 仍未被所有浏览器支持

实现

不幸的是还没有一个Javascript平台在他们的当前版本支持ES2015模块。这意味着在Firefox, Chrome or Node.js都不支持。好在有很多编译工具如polyfill 。ES2015的预编译工具Babel 也能完美处理module
Unfortunately none of the major JavaScript runtimes support ES2015 modules in their current stable branches. This means no support in Firefox, Chrome or Node.js. Fortunately many transpilers do support modules and a polyfill is also available. Currently, the ES2015 preset for Babel can handle modules with no trouble。

Babel for JavaScript Modules

The All-in-One Solution: System.js
You may find yourself trying to move away from legacy code using one module system. Or you may want to make sure whatever happens, the solution you picked will still work. Enter System.js: a universal module loader that supports CommonJS, AMD and ES2015 modules. It can work in tandem with transpilers such as Babel or Traceur and can support Node and IE8+ environments. Using it is a matter of loading System.js in your code and then pointing it to your base URL:

一体化解决方案:System.js

    <script src="system.js"></script>
    <script>
      // set our baseURL reference path
      System.config({
        baseURL: '/app',
        // or 'traceur' or 'typescript'
        transpiler: 'babel',
        // or traceurOptions or typescriptOptions
        babelOptions: {

        }
      });

      // loads /app/main.js
      System.import('main.js');
    </script>

总结

Building modules and handling dependencies was cumbersome in the past. Newer solutions, in the form of libraries or ES2015 modules, have taken most of the pain away. If you are looking at starting a new module or project, ES2015 is the right way to go. It will always be supported and current support using transpilers and polyfills is excellent. On the other hand, if you prefer to stick to plain ES5 code, the usual split between AMD for the client and CommonJS/Node for the server remains the usual choice. Don't forget to leave us your thoughts in the comments section below. Hack on!
构建模块化和处理依赖关系在过去是一件很麻烦的事。现在有了外部库以及ES2015模块,解决了很多问题。如果你正要开始一个新项目,ES2015绝对是最好的方法。它肯定不会不被支持,而且现在的预编译工具做的很棒。如果你坚持使用ES5写代码,则客户端的AMD和服务器的CommonJS / Node之间仍然是常用的选择。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容

  • 十几年前的一个午后,阳光明媚! 在大桥边,遇到一位算命先生,白须飘飘,颇有些仙风道骨。 他拦住我,说要给我看相。 ...
    书香天使阅读 178评论 0 1
  • 小魔静真名是不是张静静呀? 名字签的好潇洒
    胡尼克阅读 191评论 2 2
  • 日精进 郑州 坚持原创分享第75天 2017年9月11日 星期一 晴 处理好你的人际关系 人,要么和他人建立联系;...
    小莲蓬儿阅读 108评论 0 0