JavaScript自己不是一种模块化语言,设计者在创造JavaScript之初应该也没有想到这么一个脚本语言的做用领域会愈来愈大。之前一个页面的JS代码再多也不会多到哪儿去,而如今随着愈来愈多的JavaScript库和框架的出现,Single-page App的流行以及Node.js的迅猛发展,若是咱们还不对本身的JS代码进行一些模块化的组织的话,开发过程会愈来愈困难,运行性能也会愈来愈低。所以,了解JS模块化编程是很是重要的。javascript
什么是模块?我认为将不一样功能的函数放在一块儿,组成一个能实现某种或某些特定功能的总体就是一个模块,所以这样:php
function add(a, b) { return a + b; } function divide(a, b) { return a / b; }
如此简单的两个函数就能够组成一个模块,这个模块能够进行一些数学运算。html
固然没有人会这么写模块。仅仅是从“型”上来看,两个函数分散在全局环境中,这也看不出模块的特色。模块存在于全局变量中,应该提供一个命名空间,成为模块内容的入口。那么咱们能够将函数包裹在一个对象中:前端
var math = { add: function(a, b) { return a + b; }, divide: function(a, b) { return a / b; } }
这样看起来彷佛有模块的“型”了。可是这样还不完善,math
中的全部成员都是对外暴露的,若是其中有一些变量不但愿被修改的话那就有风险了。为了防止世界被破坏,为了维护私有变量不被修改,咱们可使用闭包。java
var math = (function() { var _flag = 0; return { add: function(a, b) { return a + b; }, divide: function(a, b) { return a / b; } }; })();
外部代码只能访问返回的add
和divide
方法,内部的_flag
变量是不能访问的。关于建立对象的一些方法的解释,能够参考个人另外一篇博文,里面有较详细的解释。node
利用自执行函数的特色,咱们还能够很方便地为模块添加方法:jquery
var math = (function(module) { module.subtract = function(a, b) { return a - b; } })(math);
模块在全局变量中的名称可能会与其余的模块产生冲突,例如$
符号,虽然使用方便,但多个模块可能都会用它做为本身的简写,例如jQuery。咱们能够在模块的组织代码中用$
做为形参,将模块的全名变量做为参数传入,可起到防冲突的效果。git
var math = (function($) { // 这里的$指的就是Math })(math);
模块的构建思想即是经过这样的方式逐渐演化而来,下面将经过介绍一些JS模块化编程的标准来展现如何组织,管理和编写模块。github
AMD
与 CMD
在JavaScript模块化编程的世界中,有两个规范不得不提,它们分别是AMD和CMD。如今的JS库或框架,凡是模块化的,通常都是遵循了这两个规范其中之一。编程
CommonJS
在说AMD以前,先要提一下CommonJS。CommonJS是为了弥补JavaScript标准库过少的缺点而产生的,因为JS没有模块机制(ES6引入了模块系统,但浏览器全面支持估计还有好几年),CommonJS就帮助JS实现模块的功能。如今很热门的Node.js就是CommonJS规范的一个实现。
CommonJS在模块中定义方法要借助一个全局变量exports
,它用来生成当前模块的API:
/* math module */ exports.add = function(a, b) { return a + b; };
要加载模块就要使用CommonJS的一个全局方法require()
。加载以前实现的math
模块像这样:
var math = require('math');
加载后math
变量就是这个模块对象的一个引用,要调用模块中的方法就像调用普通对象的方法同样了:
var math = require('math'); math.add(1, 3);
总之,CommonJS就是一个模块加载器,能够方便地对JavaScript代码进行模块化管理。但它也有缺点,它在设计之初并无彻底为浏览器环境考虑,浏览器环境的特色是全部的资源,不考虑本地缓存的因素,都须要从服务器端加载,加载的速度取决于网络速度,而CommonJS的模块加载过程是同步阻塞的。也就是说若是math
模块体积很大,网速又很差的时候,整个程序便会中止,等待模块加载完成。
随着浏览器端JS资源的体积愈来愈庞大,阻塞给体验带来的不良影响也愈来愈严重,终于从,在CommonJS社区中有了不一样的声音,AMD
规范诞生了。
AMD
它的特色即是异步加载,模块的加载不会影响其余代码的运行。全部依赖于某个模块的代码所有移到模块加载语句的回调函数中去。AMD的require()
语句接受两个参数:
// require([module], callback) require(['math'], function(math) { math.add(1, 3); });
在回调函数中,能够经过math
变量引用模块。
AMD规范也规定了模块的定义规则,使用define()
函数。
define(id?, dependencies?, factory);
它接受三个参数:
id
这是一个可选参数,至关于模块的名字,加载器可经过id名加载对应的模块。若是没有提供id,加载器会将模块文件名做为默认id。
dependencies
可选,接受一个数组参数,传入当前对象依赖的对象id。
factory
回调函数,在依赖模块加载完成后会调用,它的参数是全部依赖模块的引用。回调函数的返回值就是当前对象的导出值。
用AMD规范实现一个简单的模块能够这样:
define('foo', ['math'], function(math) { return { increase: function(x) { return math.add(x, 1); } }; });
若是省去id和dependencies参数的话,就是一个彻底的匿名模块。factory的参数将为默认值require
,exports
和module
加载器将彻底经过文件路径的方式加载模块,同时若是有依赖模块的话可经过require
方法加载。
define(function(require, exports, module) { var math = require('math'); exports.increase = function(x) { return math(x, 1); }; });
AMD规范也容许对加载进行一些配置,配置选项不是必须的,但灵活更改配置,会给开发带来一些方便。
baseUrl 以字符串形式规定根目录的路径,之后在加载模块时都会以该路径为标准。在浏览器中,工做目录的路径就是运行脚本的网页所在的路径。
{ baseUrl: './foo/bar' }
path 能够指定需加载模块的路径,模块名与路径以键-值对的方式写在对象中。若是一个模块有多个可选地址,能够将这些地址写在一个数组中。
{ path: { 'foo': './bar' } }
关于模块路径的设置项还有packages,map。
shim
对于某些没有按照AMD规范编写的模块,好比jQuery,来讲,要使它们能被加载器加载,须要用shim
方法为其配置一些属性。在main
模块中,用require.config()
方法:
require.config({ shim: { 'jquery': { exports: '$' }, 'foo': { deps: [ 'bar', 'jquery' ], exports: 'foo' } } });
以后再用加载器加载就能够了。
目前实现了AMD规范的库有不少,比较有名的是Require.js。
CMD在不少地方和AMD有类似之处,在这里我只说二者的不一样点。
首先,CMD规范和CommonJS规范是兼容的,相比AMD,它简单不少。遵循CMD规范的模块,能够在Node.js中运行。
define
与AMD规范不一样的是CMD规范中不使用id
和deps
参数,只保留factory
。其中:
1.factory
接收对象/字符串时,代表模块的接口就是对象/字符串。
define({ 'foo': 'bar' }); define('My name is classicemi.');
define.cmd
其值为一个空对象,用于判断页面中是否有CMD模块加载器。
if (typeof define === 'function' && define.cmd) { // 使用CMD模块加载器编写代码 }
require
此函数一样用于获取模块接口。如需异步加载模块,使用require.async
方法。
define(function(require, exports, module) { require.async('math', function(math) { math.add(1, 2); }); });
咱们能够发现,require(id)
的写法和CommonJS同样是以同步方式加载模块。要像AMD规范同样异步加载模块则使用define.async
方法。
exports
此方法用于模块对外提供接口。
define(function(require, exports, module) { // 对外提供foo属性 exports.foo = 'bar'; // 对外提供add方法 exports.add = function(a, b) { return a + b; } });
提供接口的另外一个方法是直接return包含接口键值对的对象:
define(function(require, exports, module) { return { foo: 'bar', add: function(a, b) { return a + b; } } });
可是注意,不能用exports输出接口对象:
define(function(require, exports, module) { exports = { foo: 'bar', add: function(a, b) { return a + b; } } });
这样写是错误的!
替代方式是这样写:
define(function(require, exports, module) { module.exports = { foo: 'bar', add: function(a, b) { return a + b; } } });
以前错误的缘由是在factory
内部,exports
其实是module.exports
的一个引用,直接给exports
赋值是不会改变module.exports
的值的。
在module对象上,除了有上面提到的exports
之外,还有一些别的属性和方法。
module.id
模块的标识。
define('math', [], function(require, exports, module) { // module.id 的值为 math });
module.uri
模块的绝对路径,由模块系统解析获得。
define(function(require, exports, module) { console.log(module.uri); // http://xxx.com/path/ });
module.dependencies
值为一个数组,返回本模块的依赖。
以前在说AMD规范的时候提到了Require.js。它是AMD规范的表明性产品。另外一个Sea.js在前端界也是赫赫有名了,CMD规范实际上就是它的产出。它们之间的区别也很能表现AMD和CMD规范之间的区别。
AMD的依赖须要前置书写
define(['foo', 'bar'], function(foo, bar) { foo.add(1, 2); bar.subtract(3, 4); });
CMD的依赖就近书写便可,不须要提早声明:
同步式:
define(function(require, exports, module) { var foo = require('foo'); foo.add(1, 2); ... var bar = require('bar'); bar.subtract(3, 4); });
异步式:
define(function(require, exports, module) { ... require.async('math', function(math) { math.add(1, 2); }); ... });
虽然AMD也能够用和CMD类似的方法,但不是官方推荐的。
以前在介绍CMD的API时,咱们能够发现其API职责专注,例如同步加载和异步加载的API都分为require
和require.async
,而AMD的API比较多功能。
总而言之,引用玉伯的总结:
1. Require.js同时适用于浏览器端和服务器环境的模块加载。Sea.js则专一于浏览器端的模块加载实现。经过Node扩展也能够运行于Node环境中。
2. Require.js -> AMD,Sea.js -> CMD。
3. RequireJS 在尝试让第三方类库修改自身来支持 RequireJS,目前只有少数社区采纳。Sea.js 不强推,采用自主封装的方式来“海纳百川”,目前已有较成熟的封装策略。
4. Sea.js的调试工具比较完备,Require.js调试比较不方便。
5. RequireJS 采起的是在源码中预留接口的形式,插件类型比较单一。Sea.js 采起的是通用事件机制,插件类型更丰富。
怎么看都像是在自诩啊= =,固然它有这个资格