天行健, 君子以自强不息
Sunny's Blog
Title

JavaScript模块化方式:AMD, CommonJS和ES Harmony

开头闲谝传,这个内容我老早就想总结一下了,因为这对于前端来讲算是一个比较资深的问题,如果没有经验估计是总结不好的。理解需要时间,所以我一直放着,直到最近我觉得自己这三个规范都用过了,别人写的总结差不多能看懂了,也能分出好与不好,所以自己总结一下子。以后备用。

下面正文。。。

1.模块化

1)你要知道什么是模块化,我个人理解的模块化是把复杂的系统按照不同功能划分为一组松耦合的模块(文件)的过程,这个是为了降低项目的管理和维护成本。说到这儿小聊一下组件化,组件化是将页面组件进行分割归类,这个是为了系统复用的需要,这个是指数性的提高系统时间的方法。模块一般考虑的是功能逻辑,组件考虑的是UI。组件比模块的粒度更细。比如requirejs是一个js模块化的解决方案,而nunjucks是UI组件化的解决方案。

2)如果谈模块化设计,一个模块应该具有至少3个特点:

a.模块是一个封闭的,这个封闭不光是形式上的封闭,更多考虑的是一种防止对全局变量造成污染的方法;

b.新模块开发可以依赖其他已有模块;

c.新模块开发后可以支持其他模块的开发。

在这篇文章中,我们会对三种模块化编码js的方式进行梳理:AMD,CommonJS以及下一代ES Harmony。

2.CommonJS

1)什么是CommonJS

CJS是一种为server-side定义的js模块化规范。

ADDY OSMANI在他的 Learning JavaScript Design Patterns 书中明确说到CommonJS is A Module Format Optimized For The Server.所以可以说CJS是一种为server-side定义的js模块化规范,具体来说,Node应用由模块组成,采用CommonJS模块规范。当然你也可以把这种规范用到浏览器端。

One evening at Joyent, when I mentioned being a bit frustrated some ludicrous request for a feature that I knew to be a terrible idea, he said to me, "Forget CommonJS. It's dead. We are server side JavaScript." - NPM creator Isaac Z. Schlueter quoting Node.js creator Ryan Dahl

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.

2)CommonJS规范如何定义使用

根据CommonJS规范,每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。下面用node代码来举例。

              // example.js
              var x = 5;
              var addX = function (value) {
                return value + x;
              };
              上面代码中,变量x和函数addX,是当前文件example.js私有的,其他文件不可见。
            

CommonJS规范规定,每个模块内部,module变量代表当前模块(仅在nodejs中,客户端js没有module)。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口(客户端js直接使用exports作为对外接口)。加载某个模块,其实是加载该模块的module.exports属性。

              var x = 5;
              var addX = function (value) {
                return value + x;
              };
              module.exports.x = x;
              module.exports.addX = addX;
              上面代码通过module.exports输出变量x和函数addX。
            

require方法用于加载模块。

              var example = require('./example.js');
              console.log(example.x); // 5
              console.log(example.addX(1)); // 6
            

3)CommonJS规范主要特点

a.模块同步加载: 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。Synchronous API makes it not suitable for certain uses (client-side)。

b.每个文件就是一个模块(One file per module)。

c.Browsers require a loader library or transpiling(比如webpack,Browserify: 浏览器不兼容CommonJS的根本原因,在于缺少四个Node.js环境的变量--module、exports、require、global。要能够提供这四个变量,浏览器就能加载 CommonJS 模块)。

d.所有代码都运行在模块作用域,不会污染全局作用域(Module definitions are encapsulated, helping us to avoid pollution of the global namespace)。

e.模块加载的顺序,按照其在代码中出现的顺序。

3.AMD

1)什么是AMD

AMD(异步模块定义)是根据客户端特点定义的js模块化规范。

ADDY OSMANI在他的 Learning JavaScript Design Patterns 书中明确说到AMD(Asynchronous Module Definition) is A Format For Writing Modular JavaScript In The Browser.明显可以看出AMD(异步模块定义)是根据客户端特点定义的js模块化规范。

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.

异步加载对于客户端来说很重要,如果你对这个概念不是很清楚,可以去看我写的一篇blog-- 对于js各种加载方式我的理解。而我们需要了解的是为什么AMD可以进行异步加载。详细解释如下:

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.

AMD不是简单的用define/require来代替CommonJS的export/require。关键在于它引入了define中模块依赖参数,这个特点是说该模块中的函数需要在它依赖的模块加载完成之后再运行。这使得模块编译器(比如RequireJS)可以知道项目中不同模块间的一个依赖关系图。没有依赖关系的模块就可以并行下载。

2)AMD规范如何定义使用

Understanding AMD: define()

                // A module_id (myModule) is used here for demonstration purposes only
                define( "myModule",
                    ["foo", "bar"],
                    // module definition function
                    // dependencies (foo and bar) are mapped to function parameters
                    function ( foo, bar ) {
                        // return a value that defines the module export
                        // (i.e the functionality we want to expose for consumption)

                        // create your module here
                        var myModule = {
                            doStuff: function () {
                                console.log( "Yay! Stuff" );
                            }
                        };

                    return myModule;
                });

                // An alternative version could be..
                define( "myModule",

                    ["math", "graph"],

                    function ( math, graph ) {

                        // Note that this is a slightly different pattern
                        // With AMD, it's possible to define modules in a few
                        // different ways due to it's flexibility with
                        // certain aspects of the syntax
                        return {
                            plot: function( x, y ){
                                return graph.drawPie( math.randomGrid( x, y ) );
                            }
                        };
                });
            

Understanding AMD: require()

                // Consider "foo" and "bar" are two external modules
                // In this example, the "exports" from the two modules
                // loaded are passed as function arguments to the
                // callback (foo and bar) so that they can similarly be accessed

                require(["foo", "bar"], function ( foo, bar ) {
                    // rest of your code here
                    foo.doSomething();
                });
            

Dynamically-loaded Dependencies

                define(function ( require ) {
                    var isReady = false, foobar;

                    // note the inline require within our module definition
                    require(["foo", "bar"], function ( foo, bar ) {
                        isReady = true;
                        foobar = foo() + bar();
                    });

                    // we can still return a module
                    return {
                        isReady: isReady,
                        foobar: foobar
                    };
                });
            

RequireJS是AMD规范下的实现,RequireJS要求每个模块均放在独立的文件之中。require方法调用模块。

                define([ 'module1', 'module2' ], function(m1, m2){
                    ...
                });

                require( ['foo', 'bar'], function( foo, bar ){
                    foo.func();
                    bar.func();
                } );
            

3)AMD规范主要特点

a.模块可以异步加载,The main difference between AMD and CommonJS lies in its support for asynchronous module loading。

b.可动态加载模块(Dynamically-loaded Dependencies)。

c.Modules can be split in multiple files if necessary。

d.Browsers require a loader library or transpiling(比如webpack预编译,RequireJS在线编译)。

e.所有代码都运行在模块作用域,不会污染全局作用域(Module definitions are encapsulated, helping us to avoid pollution of the global namespace)。

4.ES Harmony

1)什么是ES Harmony

ES Harmony指得是Modules Of The Future,你可以叫它ES6 Modules规范或者更细一些叫ES2015 Modules和ES2017 Modules规范。具体我会再写一篇blog来说明一下ECMAScript各种版本之间的关系。

ES6 Modules规范才是我们最应该学习和掌握的Modules规范,这是前端未来的趋势。

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.

2)ES Harmony规范如何定义使用

import declarations bind a modules exports as local variables and may be renamed to avoid name collisions/conflicts.

export declarations declare that a local-binding of a module is externally visible such that other modules may read the exports but can't modify them. Interestingly, modules may export child modules but can't export modules that have been defined elsewhere. We can also rename exports so their external name differs from their local names.

                //------ 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
            

3)ES Harmony规范主要特点

a.Synchronous and asynchronous loading supported(The result is syntactically pleasing and compatible with both synchronous and asynchronous modes of operation).

b.Syntactically simple.

c.Support for static analysis tools.Still not supported everywhere(这里我们用babel来降级使用).

5.参考

除了上文中引用到的,涉及到的其他文章有:

ADDY OSMANI--Writing Modular JavaScript With AMD, CommonJS, ES Harmony

Sebastián Peyrott--JavaScript Module Systems Showdown: CommonJS vs AMD vs ES2015

Sunny Sun--闭包计数器,AMD,CMD

地势坤,君子以厚德载物