JavasScript 模块规范整理
整理自他人博客,原文地址为 Javascript 模块规范,CommonJS 的模块系统,Javascript 模块化开发
CommonJS
规范
- module 拥有 id, uri 属性;在 module 中,有 require, exports, module 三个自由变量
- module 可通过 require 引入外部 module. 通过 exports 等方式向外部提供 api
示例
|
模块传送
Modules/1.1.1 规范里,只定义了模块的基本特性,并没有定义模块的存在形态。比如上面例子中的 module a, 可以是文件系统中的 a.js, 也可以是数据库中的某个字段,或者仅是封装在闭包里的一段代码。在服务器端,最常见的场景是一个模块一个文件
在服务器端,文件读取操作是同步的,模块的通讯不会很复杂,而浏览器端的则不适用。为了让模块能在不同的环境下都适用,CommonJS 需要定义 Module/Transport 规范。Module/Transport(模块传送),可以同步也可以异步
同步方案示例
|
首先引入 require.js, 实现模块定义和模块加载等方法,比如 declareModule 方法。
然后在服务器端,部署时,将模块自动转换为:
|
将上面的代码文档化,就能定义出一个模块同步传送规范。
AMD
CommonJS 的 Module/Transport 规范里,目前认可度最高的提议是 Modules/AsynchronousDefinition(简称 AMD)。AMD 定义了用于异步加载的一种模块定义方式
|
同步方案中,依赖关系由页面中引入的静态 script 来保障。异步方案中,依赖关系管理就不那么简单了。对于模块a, 对应文件 a.js, 其加载执行过程可分解为:
- 脚本的下载过程:浏览器将 a.js 从服务器下载到本地。
- 脚本的解析(parse)和执行(execute)过程:浏览器解析脚本,并执行 define 函数。
- 模块的 attach 过程:执行模块的 factory 函数。
AMD 规定 dependencies 中的模块,可以作为 factory 的参数,这就隐性要求在执行 factory 前,所有 dependencies 的 factory 都必须已执行,这种方式可称之为 execution 模式
|
b.js 的写法可以有很多种,下面是另一种很常见的写法:
|
但下面这种写法是不允许的:
|
当 dependencies 参数存在时,模块 b 依赖的模块,必须全部显式指定。在上面的例子中,模块 b 明显还依赖模块 a, 但在 dependencies 中没有,因此不符合 AMD 规范。
但很多时候,开始书写模块代码时,我们并不能很明确的知道需要依赖哪些模块。每次调用某个依赖模块时,需要跳转到模块顶部,手动添加下 dependencies。这对开发者来说,不太友好。因此 AMD 允许以下写法:
|
当 define 只有 factory 参数时,dependencies 无需开发者提前指定,define 会调用 factory.toString 方法,通过正则匹配,自动找出需要依赖的模块
除了define外,AMD还保留一个关键字require。require 作为规范保留的全局标识符,可以实现为 module loader,也可以不实现
理解了 AMD, RequireJS 的 api 也就很容易上手了,RequireJS 是遵循 AMD 规范的。实际上,RequireJS 的作者 James Burke, 为 AMD 规范贡献了很多 idea
Wrappings
AMD 规范已经很不错,RequireJS 也很流行,jQuery 近期也加入了对 AMD 规范的支持。然而,CommonJS 社区近期有件不大不小的事,有人提出了另一种异步加载模块的定义方式:Modules/Wrappings
规范
- 定义模块用module变量,它有一个方法declare
- declare接受一个函数类型的参数,如称为factory
- factory有三个参数分别为require、exports、module
- factory使用返回值和exports导出API
- factory如果是对象类型,则将该对象作为模块输出
示例
|
注意:wiki 的当前版本是 module.declare(factory). 但在这篇讨论里 AMD vs Wrappings 里,已经有了更完善的方案。
从表面上看,AMD 和 Wrappings 唯一的不同是 define 还是 module.declare 的命名差异。如果仅是这点差异的话,实在不值得新增加一个提议。Wrappings 和 AMD 最大的不同,在于 Wrappings 方案里,factory 的参数更简单,和 dependencies 无对应关系。也就是说,可以如下写代码:
|
这个看似非常小的差异,可以让下面的代码合理存在并达到预期目的:
|
AMD 里的 download/parse/execute/attach,在 Wrappings 里,attach 过程可以延后,可以等到第一次 require 时,才调用 factory. 这种模式称之为 availability 模式
availability 模式存在的问题是,需要声明变量引用模块后才能使用,否则会报错
RequireJS
终于说到 RequireJS 了。RequireJS 很优秀,用户群也不少。从目前的特性和功能来看,感觉有以下不如意:
- 文件太大,用 google closure compiler 压缩后,12.2k. 这是在页头必须引入的脚本,还是希望越小越好
- 功能太多。这本是优点,比如能够在各种环境下跑。但对于真实的 web 应用来说,还是希望用情专一,尽量无无用代码
- 给 require 方法赋予了双重含义。一重含义是 CommonJS/Modules/1.1.1 规范里定义的 require, 另一重是 RequireJS 里用来加载模块和调用回调。这导致 require 的 dependencies 参数的格式,和 define 中的 dependencies 参数的格式不一致
- 目前不支持 availability 模式
- require.js 代码里,有 only for jQuery 的代码
CMD
AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。类似的还有 CommonJS Modules/2.0 规范,是 BravoJS 在推广过程中对模块定义的规范化产出。这些规范的目的都是为了 JavaScript 的模块化开发,特别是在浏览器端的。目前这些规范的实现都能达成浏览器端模块化开发的目的
与 AMD 的区别
- 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible
- CMD 推崇依赖就近,AMD 推崇依赖前置。看下面的代码说明。虽然 AMD 也支持 CMD 的写法,同时还支持将 require 作为依赖项传递,但 RequireJS 的作者默认是最喜欢上面的写法,也是官方文档里默认的模块定义写法。CMD 推崇依赖就近需要遍历所有的require关键字,找出后面的依赖。具体做法是将function toString后,用正则匹配出require关键字后面的依赖。显然,这是一种牺牲性能来换取更多开发便利的方法
- AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。比如 AMD 里,require 分全局 require 和局部 require,都叫 require。CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都简单纯粹
|
SeaJS
- SeaJS未实现全部的 Modules 1.1.1。如require函数的main,paths属性在SeaJS中没有。但SeaJS给require添加了async、resolve、load、constructor
- SeaJS没有使用 Modules/Wrappings 中的module.declare定义模块,而是使用define函数(看起来象AMD中的define,实则不然)
与 RequireJS 的异同
相同之处:
RequireJS 和 Sea.js 都是模块加载器,倡导模块化开发理念,核心价值是让 JavaScript 的模块化开发变得简单自然。
不同之处:
- 定位有差异。RequireJS 想成为浏览器端的模块加载器,同时也想成为 Rhino / Node 等环境的模块加载器。Sea.js 则专注于 Web 浏览器端,同时通过 Node 扩展的方式可以很方便跑在 Node 环境中。
- 遵循的规范不同。RequireJS 遵循 AMD(异步模块定义)规范,Sea.js 遵循 CMD (通用模块定义)规范。规范的不同,导致了两者 API 不同。Sea.js 更贴近 CommonJS Modules/1.1 和 Node Modules 规范。
- 推广理念有差异。RequireJS 在尝试让第三方类库修改自身来支持 RequireJS,目前只有少数社区采纳。Sea.js 不强推,采用自主封装的方式来“海纳百川”,目前已有较成熟的封装策略。
- 对开发调试的支持有差异。Sea.js 非常关注代码的开发调试,有 nocache、debug 等用于调试的插件。RequireJS 无这方面的明显支持。
- 插件机制不同。RequireJS 采取的是在源码中预留接口的形式,插件类型比较单一。Sea.js 采取的是通用事件机制,插件类型更丰富。
总之,如果说 RequireJS 是 Prototype 类库的话,则 Sea.js 致力于成为 jQuery 类库。
UMD
AMD以浏览器为第一(browser-first)的原则发展,选择异步加载模块。它的模块支持对象(objects)、函数(functions)、构造器(constructors)、字符串(strings)、JSON等各种类型的模块。因此在浏览器中它非常灵活。
CommonJS module以服务器端为第一(server-first)的原则发展,选择同步加载模块。它的模块是无需包装的(unwrapped modules)且贴近于ES.next/Harmony的模块格式。但它仅支持对象类型(objects)模块。
这迫使一些人又想出另一个更通用格式 UMD(Universal Module Definition)。希望提供一个前后端跨平台的解决方案
UMD 定义那些既能在客户端又能在服务器端工作的模块,这样的模块同时也能和目前可用的主流脚本加载器一同工作。
实现
UMD的实现很简单,先判断是否支持Node.js模块格式(exports是否存在),存在则使用Node.js模块格式。
再判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。前两个都不存在,则将模块公开到全局(window或global)。
|
|
ES Harmony/Modules
ECMAScript的下一个版本Harmony已经考虑到了模块化的需求,目前还在努力指定中。
在 ES.next 中,用import和export导入和导出模块:
import声明把某个模块的导出绑定为本地变量,并可以重命名来避免命名冲突。
export声明声明了某个模块的本地绑定是外部可见的,这样其它模块就能够读取它们但却无法进行修改。有趣的是,模块可以导出子模块,却无法导出已经在别处定义过的模块。你同样可以给导出重命名来让它们不同于本地的名字。
定义模块
使用module关键字来定义一个模块
|
使用import关键字来加载外部模块
|
|
除此之外,还可以远程载入的模块、异步加载模块等,请参考使用 AMD、CommonJS 及 ES Harmony 编写模块化的 JavaScript