JavaScript模块化编程的发展
所谓模块,可以简单的理解为一个js文件。比如说,一个 JqueryJS 就是一个模块。有了模块,我们便可以很方便的引用并使用别人编写或封装好的代码,需要什么功能,就引入什么模块。
引入模块的形式可以分为很多种,最原始的,要从 script标签 说起。
一、最原始的方式 script标签
页面引用模块最常见的方式,当然是使用 script
标签。如,引入一个 Jquery:
<script src="jquery.js"></script> |
但这种方式的引入,很容易导致一些问题。最明显的是,模块里的变量相互污染,可能会导致一些模块名会被覆盖,或不能生效。
开发者必须了解模块的依赖关系,并手动调整模块加载的顺序。
另外,这种引入形式,大模块会加载很慢,主程序会被阻塞,导致浏览器出现 “假死” 的情况。
基于这些问题,便出现了模块加载的概念。
二、CommonJS
在之前看来,前端根本并没有模块加载的概念,直到2009年 Nodejs 项目的创建。因为该项目将JavaScript应用于服务端,而它的模块系统是参照 CommonJS 写的,这便代表着 “JavaScript模块化编程” 的开始。
其实,CommonJS 的原名是 ServerJS。之所以更名是因为 ServerJS 社区的人觉得 Nodejs 在基于此规范上有着不错的实践,它们希望把这种规范推广到客户端,因此,将 ServerJS 改为 CommonJS。
在 CommonJS 中,它提供了一个全局的方法 require(),用于加载模块。比如,我们需要数学方法,则得先这样引入,再使用它:
var math = require('math'); |
很快,当很多前端人员看到这种模式之后,他们想把它引入到客户端(浏览器端)。并且,希望在不改动代码的情况,同时能够被 服务端 和 客户端 正常引用。
然而,他们忽视了一个重要的问题。即上面的代码在 服务端 运行是完全没问题的,毕竟 math 模块就在本地磁盘,可以说根本无需下载,因此,加载起来非常快。
但运行在浏览器端的js文件,其中引入的模块是在远程服务器,如果模块很大,或者网速很慢,极有可能导致下载的时候过长,出现浏览器假死的情况。
也就是说,服务端可以使用”同步加载”(synchronous),但客户端或许要采用”异步加载”(asynchronous)的方式。
于是,针对客户端的这种状况,开发人员提出了 AMD 的概念。
三、AMD 和 RequireJS
AMD,是 Asynchronous Module Definition 的简写,即 异步模块定义。
那么,何为 异步模块定义。它表示当前模块的加载,不影响主线程代码的运行,所有依赖该模块的代码,都放在一个回调函数中。
最常见的语法是这样:
require([mod], callback); |
那么前面的代码,就可以这样表示:
require(['math'], function(math) { |
上面的代码中,加载 math 模块的同时,会继续执行 renderData
,直到 math 模块加载完毕,math.add(2, 3)
才会运行。
目前,基于这种模块加载规范的库主流的有两个,require.js 和 curl.js。
RequireJS
RequireJS 主要包含 require、define 两个函数。其中,require 是引用模块,而 define 是定义模块。
由于该加载器是基于 AMD,所以,你必须通过 define
来定义模块,若当前模块不依赖其他模块,你可以这样定义:
|
如果主代码依赖第三方库,则需要这样:
|
而使用 require 引入模块,语法也类似:
require(['main'], function(main) { |
需要注意的是,使用 RequireJS 中 require 方法引入的模块,必须是基于 AMD 规范编写的。
更多用法和详细介绍可参见 RequireJS 官网
四、CMD 和 SeaJS
CMD,为 Common Module Definition 的全称,即 通用模块定义。说到 CMD,避免不了提及它的倡导者玉伯和其对应的成品 SeaJS。
接上面,基于 AMD 的 RequireJS 出来之后,在当时,RequireJS 受到广大开发人员的追捧,一时非常火热,国内很多大公司也在使用。
但玉伯团队后来在使用 RequireJS 的过程中,遇到了很多坑,尝试不断给 RequireJS 提建议,但似乎都被对方拒绝。在深感无望之后,开始萌发了造轮子的想法(其中缘由详见 前端模块化开发那点历史)。
于是,就推出了 CMD 的概念,而基于 CMD 这个模块编码规范,便实现了 SeaJS 模块加载器。
SeaJS
与 CommonJS 语法类似,SeaJS 也是使用 define
来定义模块:
define(factory); |
其中,参数 factory 可以是一个函数,也可以是一个对象或字符串,参数类型不同,输出的模块也不同。
当 factory 为对象、字符串时,表示模块的接口就是该对象、字符串。
当 factory 为函数时,表示是模块的构造方法。
而 require
则是用于获取模块,该方法接受 模块标识 作为唯一参数:
define(function(require, exports) { |
需要注意的是,SeaJS 中 require 方法的使用场景,必须是在基于 CMD 规范所定义的模块中。
更多用法和详细介绍可参见 SeaJS 官网
五、AMD 和 CMD 的区别
AMD 和 CMD 都是用于加载模块的规范,这里引用玉伯的话,说下 AMD 与 CMD 区别:
- 推广理念有差异 RequireJS 在尝试让第三方类库修改自身来支持 RequireJS,目前只有少数社区采纳。Sea.js 不强推,采用自主封装的方式来“海纳百川”,目前已有较成熟的封装策略。
- 执行差异 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。
- 书写差异 CMD 推崇 依赖就近,而 AMD 推崇依赖前置。即 AMD 可在需要使用模块的位置前一行,才引入模块,而 CMD 则必须在参数里先写好所有依赖模块。
用代码说明下:
// CMD |
// AMD |
六、UMD 统一规范的使用
其实,目前主流的模块加载规范,只有 CommonJS 和 AMD。
如果你希望自己写的插件或工具,既可以被 CommonJS 引用,又能被RequireJS(基于 AMD 实现)引用。同时,若开发者在浏览器端不使用模块加载器的情况,还能正常引用你的代码。那么,你需要 UMD。
UMD,是 Universal Module Definition 的简写,即通用模块定义。
它将这两个规范封装成一套代码,为模块引用提供了跨平台解决方案,这样,你的代码就能在多平台运行。
比如,现在选择器引擎 qwery 就有类似的实现。
UMD 的基本代码如下:
;(function (context, name, definition) { |
如果你的主代码需要依赖第三方库,则可以这样:
;(function (context, name, definition) { |
另外,倘若你想要检测是否包含 CMD 模块加载器,你也可以加上相应的判断:
if (typeof define === "function" && define.cmd) { |
七、ES6 Module
上面所提及的模块加载器都不属于 ECMASCRIPT规范,但你可以把它们理解为模块加载规范的一种另类实现方式。
其实,再即将到来的ES6中,就包含了 模块加载。
这样,我们便可以通过原生语法去输出模块:
// math.js |
或导入模块:
import {add} from './math'; |
除了模块的输出导入,还有一些非常让人期待的功能。
八、总结
由于 ServerJS 在服务端、桌面端应用比较成功,于是,开发者希望将这套规范推广到客户端,并将社区生态改名为 CommonJS 。但后来发现,CommonJS 是同步加载模块,只适用于服务端。于是,针对客户端提出了AMD,基于该规范实现的库有 RequireJS。后来,玉伯团队在使用 RequireJS 遇到了一些问题,在多次提建议却屡遭被拒的情况下,自己开发了 SeaJS,并提出了 CMD 的概念。
针对不同的模块加载规范,开发者提供一套跨平台的方案,即 UMD。
然而,最终,这些都将会被ES6的 Module 所取代。