JS模块化编程

1. 背景

  JS的强大已经不用解释了,从Web RIA到Node服务器,到处都是JS的身影。然而由于出身的缘故,JS本身在大规模应用上存在着很多问题,比如模块化编程。本文以浏览器端模块化编程为内容,重点介绍AMD规范和CMD规范。在服务器端,NodeJS遵守的Common规范我们这里暂不讨论。

  对于计算机语言,模块化编程是必不可少的,对架构设计、代码复用起到至关重要的作用,工程中引入别人写好的库和模块能大大缩减开发周期。C/C++中,我们可以用include;Java中我们可以用import。可是在JS中,这些是不存在的,取而代之的是script标签。

  在浏览器端,我们可以通过script标签引入需要的库,然后加载自己的脚本,最后运行脚本。这样做似乎与include和import没什么区别,然而有几个重要的因素必须要考虑:JS是解释型语言,边加载边运行,后续脚本运行时,这些脚本所依赖的一切必须已经加载完毕;JS脚本加载时会阻塞浏览器,如果加载的JS很多很大,浏览器会卡住,带来很差的用户体验;通过调整script标签顺序可以修改JS模块之间的依赖,然而当模块很多时,这种做法就不可用了。

  鉴于以上,JS模块化编程规范应运而生,并出现了大量实现。这些实现,简单讲就是一个超底层的JS库,这个库的作用有两个:一是完成JS脚本的异步加载与执行;二是确保JS脚本按照用户设计的依赖关系加载,即一段脚本执行时,它的所有依赖已经加载完毕了。不同的规范,只是写法用法不同而已,最终目的都是一样的。另外,模块化编程与异步加载不分家,会让JS按需加载,大大提高页面响应速度;模块加载提供命名空间的效果,有效防止作用域污染,并有一定的容错能力。

  AMD规范和CMD规范是目前最为流行的两种模块化编程模式,符合AMD规范的库有RequireJS,符合CMD规范的库有SeaJS。

2. AMD(Asynchronous ModuleDefinition)规范

  AMD翻译过来叫“异步的模块定义”,接口非常简单,只有一个define,完整格式如下:

define(‘module-name’, [‘lib1’, ‘lib2’], function (lib1, lib2) {
         // todo
         return {};
});

第一个参数是模块的名称,可以省略;第二个是模块的依赖,也就是其他模块,如果省略,则此模块没有任何依赖;第三个参数是依赖加载完毕后的回调函数,回调函数的形参是依赖模块的输出,顺序与第二个数组参数一致,回调函数的返回值就是此模块的输出,可以作为其他模块的依赖形参。

2.1  纯数据模块

  这种模块没有任何依赖,不做任何操作,只是为了提供数据源,可以这样定义:

define({
    color: "black",
    size: "unisize"
});

2.2无依赖模块

  如果模块没有任何依赖,但进行了一些预处理,可以这样定义:

define(function() {
    // TODO
     return {
        color: "black",
        size: "unisize"
    }
});

2.3将模块定义为函数

  这种模块是最常见的,意为模块的输出是一个函数,模块的依赖是这个输出函数使用的外部变量,因此这种模块的输出是一个闭包。使用时也要格外小心,尤其是输出函数当做构造函数使用时。

define(["my/cart", "my/inventory"],
    function(cart, inventory){
        //return a function todefine "foo/title".
        //It gets or sets thewindow title.
        return function(title){
            return title ? (window.title= title) :
                  inventory.storeName + ' ' + cart.name;
        }
    }
);

2.4引入符合Common规范的模块

define(function(require){
var mod = require("./relative/name");
return mod;
});

2.5 JSONP模块

  JSONP可以跨域,应用非常广泛,使用JSONP模块时,callback必须设成’define’。

require(["http://example.com/api/data.json?callback=define"],
    function (data) {
        //The data object will be the APIresponse for the
        //JSONP data call.
        console.log(data);
    }
);

这种方法一般用于初始化,但错误处理比较复杂。

3. CMD规范

  CMD(Common ModuleDefinition)规范,跟AMD相比,CMD更像CommonJS。至于CMD和AMD哪个好哪个坏,完全没有定论的,主要看个人喜好。CMD的主接口也叫define,其参数可以是string、object、function。define的用法也可以像AMD一样,带id、依赖、回调,但这样就不属于CMD规范了。基本格式如下:

define(function (require, exports, module) {
         // todo
});

3.1 require参数

  第一个形参require是一个方法,用来加载外部模块,用法跟2.4类似:

define(function (require, exports) {
    // 获取模块 a 的接口
    var a = require('./a');
    // 调用模块 a 的方法
    a.doSomething();
});

  但是这个require是同步模式加载,即如果模块没加载完毕,require后面的语句是不会执行的。AMD规范中都是异步加载,在回调function中使用require,其实相当于告诉构建器要在依赖队列中加入相关模块。

  require同样有异步加载功能,异步加载后,执行响应的回调:

define(function(require, exports, module) {
  // 异步加载一个模块,在加载完成时,执行回调
  require.async('./b',function(b) {
    b.doSomething();
  });
  // 异步加载多个模块,在加载完成时,执行回调
  require.async(['./c','./d'], function(c, d) {
    c.doSomething();
    d.doSomething();
  });
});

3.2 exports参数

  exports是一个对象,是模块的对外接口,即AMD规范中的return部分,当然,也可以像AMD规范一样,利用return返回接口。两种方式都可以,非常灵活。

define(function(require, exports) {
  // 对外提供 foo 属性
  exports.foo = 'bar';
  // 对外提供 doSomething 方法
  exports.doSomething =function() {};
});
define(function(require) {
  // 通过 return 直接提供接口
  return {
    foo: 'bar',
    doSomething: function() {}
  };
});

值得注意的是,exports形参是不能重新定义的,因为重新定义后,原引用就断掉了,下面的做法起不到输出模块的作用:

define(function(require, exports) {
  // 错误用法!!!
 exports = {
   foo: 'bar',
   doSomething: function() {}
  };
});

3.3 module参数

  module 是一个对象,上面存储了与当前模块相关联的一些属性和方法。

  module.id为模块标识,module.uri为模块绝对路径,module.dependencies为模块的依赖数组,module.exports为模块的输出引用。也就是说,3.2节最后的例子中,如果这样写,就是可以工作的:

define(function(require, exports, module) {
  // 正确写法
  module.exports = {
    foo: 'bar',
    doSomething: function() {}
  };
});

同时,对module.exports的赋值必须是同步的,放在settimeout里面是不行的。

4.总结

  可以看出,模块化编程为我们提供了良好的环境,在这个环境中,可以进行更明确的分工合作,让JS开发更专业化。同时,AMD规范和CMD规范都提供了异步加载,让Web前端应用更加高效,用户体验更好。

  至于这两种规范哪个好一些,这个完全看个人编码习惯了,适合你的,就是好的。不过总体来看,CMD有相当一部分兼容了AMD,其最大的区别还是对依赖的引用上。

  AMD主要通过回调函数的形参对依赖进行引用,有多少个依赖,就有多少个形参,当然如果依赖很多也可以不写那么多形参,通过arguments引用,或者在回调函数内部require也是一样的。这里需要说明的是,AMD规范中,尤其是RequireJS中,如果通过var a = require(‘module-name’);这种形式引入依赖,那么module-name必须要在依赖数组中存在,否则会报错。

  CMD规范的依赖引用主要是在回调函数中直接require了,并且这种require是同步的,也提供了异步接口,这样做,势必要影响运行期性能。但是想AMD那种预加载依赖的方式,势必要影响加载时间。这两种影响哪个能容忍,哪个不能容忍,应该具体情况具体分析,视应用而定。

5.参考

CMD规范定义:

https://github.com/seajs/seajs/issues/242

Javascript的AMD:

http://www.cnblogs.com/happyPawpaw/archive/2012/05/31/2528864.html

RequireJS中文网:

http://www.requirejs.cn/

Sea.js文档:

http://seajs.org/docs/#docs

你可能感兴趣的:(JavaScript)