1、简介
随着前端业务复杂度的增加,模块化成为一个大的趋势。而在ES6还未被浏览器所支持的情况下,commonjs作为ES6中标准模块加载方案,在客服端中的支持情况并不好,现在在客服端中有2中模块化的解决方案,CMD和AMD,他们的代表分别为seajs和requirejs。这篇文章主要介绍我对commonjs、AMD以及CMD的理解。
2、commonJS
commonjs的目标是制定一个js模块化的标准,它的目标制定一个可以同时在客服端和服务端运行的模块。这些模块拥有自己独立的作用域,也可以向顶层曝露出自己的api也就是module.exports。在ES6中common被制定为标准。但是在ES6还未被浏览器完美支持的情况下,commonjs规范之能在服务端发挥它的作用。比如在nodejs和webpack等中。而我们服务端的异步加载模块主要有2种加载方案,CMD和AMD,这两种规范的典型是seajs和rjs(requirejs)。这两种方案虽然都是加载模块的解决方案,但是还是有一些的差别。
我们先来了解下commonjs规范。
此规范指出了如何编写可以在同类模块系统中所共用的模块,这类模块系统可以同时在客户端和服务端,以安全的或者不安全的方式已经被实现了或者通过语法扩展可以被未来的系统所支持。这些模块需要提供*作用域的私有性,并提供从其他模块导入单例对象到自身并且可以导出自身API的能力。含蓄的说,这个规范定义了如果一个模块系统要支持共用模块,那么它需要提供的最少的功能特性。
模块上下文
- 在一个模块中,存在一个*的变量"require",它是一个函数。
- 这个"require"函数接收一个模块标识符。
- "require"返回外部模块所输出的API。
- 如果出现依赖闭环(dependency cycle),那么外部模块在被它的传递依赖(transitive dependencies)所require的时候可能并没有执行完成;在这种情况下,"require"返回的对象必须至少包含此外部模块在调用require函数(会进入当前模块执行环境)之前就已经准备完毕的输出。
- 如果请求的模块不能返回,那么"require"必须抛出一个错误。
- 在一个模块中,会存在一个名为"exports"的*变量,它是一个对象,模块可以在执行的时候把自身的API加入到其中。
- 模块必须使用"exports"对象来做为输出的唯一表示。
如果不理解请移步CommonJS Modules/1.0 规范 & AMD 规范中文版 这里面会有具体的例子。
从模块的上下文可以可以看出,这只是制定了一种模块的方式,在模块内部用require函数去获取我们所需要的API,用exports抛出当前模块的API。这其实也就是我们node的模块方式,没错,node就是commonjs的实践者,commonjs只是一种规范,而nodejs就是实践这个规范。同样的,ES6中,也执行了commonjs规范,但是介于我们浏览器还不能完美的支持ES6,所以我们也不能用这种很cool的方式。(注解:当然babel是一个完美的工具,把ES6转成目前支持的es5。但是最终在浏览器中执行的代码还是es5。所以我们直接编写的ES6代码,只能通过webpack等打包工具,把模块之间的依赖通过打包工具来实现。)
既然无法在浏览器中使用ES6,那么我们如和在浏览器中去执行模块依赖加载的呢?这里有2中方案去实现浏览器下模块和依赖加载,CMD和AMD,这两种方案的典型是seajs和requirejs。下面我们先分析AMD
3、AMD规范
AMD是为了弥补commonjs规范在浏览器中目前无法支持ES6的一种解决方案。异步模块定义规范(AMD)制定了定义模块的规则,这样模块和模块的依赖可以被异步加载。这和浏览器的异步加载模块的环境刚好适应(浏览器同步加载模块会导致性能、可用性、调试和跨域访问等问题)。
这个规范只定义define函数。
define(id?,dependencies?,factory);
其中id和dependencies不是必须的。
模块的格式:
模块名用来唯一标识定义中模块,它们同样在依赖数组中使用。AMD的模块名规范是CommonJS模块名规范的超集。引用如下:
- 模块名是由一个或多个单词以正斜杠为分隔符拼接成的字符串
- 单词须为驼峰形式,或者".",".."
- 模块名不允许文件扩展名的形式,如".js"
- 模块名可以为 "相对的" 或 "*的"。如果首字符为"."或".."则为"相对的"模块名
- *的模块名从根命名空间的概念模块解析
- 相对的模块名从 "require" 书写和调用的模块解析
上文引用的CommonJS模块id属性常被用于JavaScript模块。
相对模块名解析示例:
- 如果模块
"a/b/c"
请求"../d"
, 则解析为"a/d"
- 如果模块
"a/b/c"
请求"./e"
, 则解析为"a/b/e"
如果AMD的实现支持加载器插件(Loader-Plugins),则"!"符号用于分隔加载器插件模块名和插件资源名。由于插件资源名可以非常*地命名,大多数字符都允许在插件资源名使用。
define.amd (Object)用来标识有amd模块加载器的存在
例子:
// 创建一个名为"alpha"的模块,使用了require,exports,和名为"beta"的模块: define("alpha", ["require", "exports", "beta"], function (require, exports, beta) { exports.verb = function() { return beta.verb(); //Or: return require("beta").verb(); } }); //一个返回对象的匿名模块: define(["alpha"], function (alpha) { return { verb: function(){ return alpha.verb() + 2; } }; }); //一个没有依赖性的模块可以直接定义对象: define({ add: function(x, y){ return x + y; } });
// 一个使用了简单CommonJS转换的模块定义: define(function (require, exports, module) { var a = require('a'), b = require('b'); exports.action = function () {}; });
4、AMD规范的实践者requirejs
requirejs是AMD规范的实践者,RequireJS 是一个JavaScript模块加载器。它非常适合在浏览器中使用,但它也可以用在其他脚本环境, 就像 Rhino and Node. 使用RequireJS加载模块化脚本将提高代码的加载速度和质量。
在使用requirejs时,可以查看官方文档
requirejs非常简单,我们只需要定义在页面加载的时候,引入requirejs并且,把mainjs指定在data-main中,在mainjs中引入我们的requirejs.config和我们需要用到的页面js,requirejs会根据我们的模块去加载相应的依赖,然后执行代码。
// 页面引入 <script data-main="main" src="./amdjs/require.js"></script> // 模块,这里使用AMD定义模块的方式,例如,定义一个模块module1 define('module1', ['zepto'], function($) { console.log('this is module1') }) //mainjs内容 require.config({ baseUrl: 'amdjs/modules', paths: { main: 'amdjs/main' zepto: 'http://zeptojs.com/zepto.min' }, shim: { }, waitSeconds: 15 }); // 你的模块 requirejs(['module1'],function($) { console.log('load success!') })
config文件里面有许多参数,这里我把常用的解释下,具体的请查看requirejs文档。
baseUrl:所有模块的查找根路径。
paths :path映射那些不直接放置于baseUrl下的模块名。设置path时起始位置是相对于baseUrl的,除非该path设置以"/"开头或含有URL协议(如http:)。
shim: 为那些没有使用define()来声明依赖关系、设置模块的"浏览器全局变量注入"型脚本做依赖和导出配置。shim配置仅设置了代码的依赖关系,想要实际加载shim指定的或涉及的模块,仍然需要一个常规的require/define调用。设置shim本身不会触发代码的加载。
deps: 指定要加载的一个依赖数组。当将require设置为一个config object在加载require.js之前使用时很有用。一旦require.js被定义,这些依赖就已加载。使用deps就像调用require([]),但它在loader处理配置完毕之后就立即生效。它并不阻塞其他的require()调用,它仅是指定某些模块作为config块的一部分而异步加载的手段而已。
5、CMD规范
cmd是commonjs另外的一种模块加载方案,这个规范本身偏向于commonjs的规范。他以一个文件就是一个模块和ES6中标准的commonjs规范类似。
它定义了以个define(factory)函数。define
接受 factory
参数,factory
可以是一个函数,也可以是一个对象或字符串。
如果factory为函数时,它有三个参数:require,exports,module。
define.cmd是一个cmd模块加载器的标识
require方法:同步加载模块
define(function(require, exports) { // 获取模块 a 的接口 var a = require('./a'); // 调用模块 a 的方法 a.doSomething(); });
require.async:用来在模块的内部异步加载模块,并且完成后执行指定回掉。
define(function(require, exports, module) { // 异步加载一个模块,在加载完成时,执行回调 require.async('./b', function(b) { b.doSomething(); }); // 异步加载多个模块,在加载完成时,执行回调 require.async(['./c', './d'], function(c, d) { c.doSomething(); d.doSomething(); }); });
require.resolve:使用模块系统内部的路径解析机制来解析并返回模块路径。该函数不会加载模块,只返回解析后的绝对路径。
define(function(require, exports) { console.log(require.resolve('./b')); // ==> http://example.com/path/to/b.js });
exports:
是一个对象,用来向外提供模块接口。
define(function(require) { // 通过 return 直接提供接口 return { foo: 'bar', doSomething: function() {} }; });
module.exports:模块暴露的出口
define(function(require, exports, module) { // 正确写法 module.exports = { foo: 'bar', doSomething: function() {} }; });
6、CMD规范的实践者seaJS
seajs和requirejs的加载方式类似,在页面引入seajs文件后,加载seajs.config,并且之后加载mainjs。详细信息请查看
加载seajs
// 引入seajs <script src="../sea-modules/seajs/seajs/2.2.0/sea.js"></script> // 配置文件 <script> // Set configuration seajs.config({ base: "../sea-modules/", alias: { "jquery": "jquery/jquery/1.10.1/jquery.js" } }); // 入口 // For development if (location.href.indexOf("?dev") > 0) { seajs.use("../static/hello/src/main"); } // For production else { seajs.use("examples/hello/1.0.0/main"); } </script>
API:
seajs.config: 配置信息
seajs.config({ // 设置路径,方便跨目录调用 paths: { 'arale': 'https://a.alipayobjects.com/arale', 'jquery': 'https://a.alipayobjects.com/jquery' }, // 设置别名,方便调用 alias: { 'class': 'arale/class/1.0.0/class', 'jquery': 'jquery/jquery/1.10.1/jquery' } });
更多的详细配置选项请参考页面:seajs config详细
seajs.use:页面加载一个或者多个模块
// 加载一个模块 seajs.use('./a'); // 加载一个模块,在加载完成时,执行回调 seajs.use('./a', function(a) { a.doSomething(); }); // 加载多个模块,在加载完成时,执行回调 seajs.use(['./a', './b'], function(a, b) { a.doSomething(); b.doSomething(); });
这里值得一提的是seajs.use的用法,它替代了AMD中require(【】,factory)的用法。
define:用来定义模块。
define(function(require, exports, module) { // 模块代码 });
require: 用来获取指定模块的接口。
define(function(require) { // 获取模块 a 的接口 var a = require('./a'); // 调用模块 a 的方法 a.doSomething(); });
require.async:用来在模块内部异步加载一个或多个模块。
define(function(require) { // 异步加载一个模块,在加载完成时,执行回调 require.async('./b', function(b) { b.doSomething(); }); // 异步加载多个模块,在加载完成时,执行回调 require.async(['./c', './d'], function(c, d) { c.doSomething(); d.doSomething(); }); });
exports: 用来在模块内部对外提供接口
define(function(require, exports) { // 对外提供 foo 属性 exports.foo = 'bar'; // 对外提供 doSomething 方法 exports.doSomething = function() {}; });
module.exports:与 exports
类似,用来在模块内部对外提供接口
define(function(require, exports, module) { // 对外提供接口 module.exports = { name: 'a', doSomething: function() {}; }; });
seajs API具体的用法,请参考seajs API
7、CMD和AMD之间的差异
1)AMD(异步加载模块),CMD(通用模块),AMD是需要通过异步加载的形式把依赖加载进来,然而CMD在require依赖的时候,可以通过同步的形式(require),也可以通过异步的形式(require.async)。当然AMD也可以通过特殊的写法支持CMD,但是不推崇。
2)CMD 推崇依赖就近,AMD 推崇依赖前置。在AMD中,我们需要把依赖前置在依赖数组中。而在cmd中,我们只需要在使用这个模块前,把依赖的模块require进来。
3)设计理念不一样,在 SeaJS 里,API 的设计理念是:
- 保持简单,职责单一。
- 遵守规范,但不拘泥。
- 适度灵活
requirejs中,require的用法多样,比如:
require('a') -- gets exports of module a
require(['a']) -- fetch module a according to module name scheme
require(['a.js']) -- fetch a.js directly relative to current page
require({...}) -- set loader config
4)聚焦点有差异
seajs专注于浏览器环境下的模块加载,而requirejs集成了在node环境以及Rhino 环境下的代码,这导致requirejs比seajs更大。