之前写过一个Demo,还顺便写了一篇文 模块管理与打包实践,但对于前端模块化的规范和模块加载器原理没有深入探究。
关于为什么要前端模块化,实际上是要解决文件依赖
和命名空间
的问题,具体可以看 模块管理与打包实践 或者 玉伯的这篇 前端模块化开发的价值
了解历史
JavaScript本身一直没有模块体系,js文件的加载都是使用最简单的script标签,使得每个模块的接口都暴露在全局作用域中。容易造成一些问题,比如:
- 全局作用域下变量冲突
- 相互依赖的文件需要按以来的顺序书写,开发人员需要自己解决模块间依赖关系
- 模块只能按script标签的顺序加载
所以后来就有社区制定了一些模块加载方案,经常会看到CommonJS、AMD、CMD等规范,还有一些常见的模块加载器RequireJS、SeaJS等。
关于这些模块加载方案的由来可以看下玉伯的这篇 前端模块化开发那点历史 或者 阮一峰的 Javascript模块化编程(二):AMD规范
大概的历史是这样的:
- 2009年1月,Mozilla 的工程师 Kevin Dangoor创建了ServerJS;2009年8月,项目改名为 CommonJS。
CommonJS
是一套规范。- 2009年,美国程序员Ryan Dahl创造了node.js项目,
node.js的模块系统
就是参照CommonJS的模块规范写的。- 但是CommonJS规范中的require是同步的,这在浏览器端是不能接受的。所以后来就有了
AMD 规范
,RequireJS
是AMD规范的实现。- 再后来玉伯觉得RequireJS不够完善,给RequireJS团队提的很多意见都不被采纳,就自己写了Sea.js,并制定了
CMD规范
,Sea.js
遵循CMD规范。- 2015年6月正式发布了
ECMAScript6标准
,在语言标准层面实现了模块功能,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
主流模块加载器和它的规范
CommonJS 与 Nodejs
CommonJS对模块的定义十分简单,主要分为模块加载、模块定义和模块标识3个部分。
模块加载
通过require方法引入一个模块,require方法接受一个模块标识作为参数。
|
|
模块定义
module
对象表示模块本身,exports
是module
对象的属性,module.exports是模块唯一的导出口,用于导出当前模块的方法或变量。Node为每个模块提供一个exports变量,指向module.exports。这也是为什么不能直接对exports赋值的原因,因为这样就切断了exports与module.exports的联系。我们可以直接向exports对象添加方法
|
|
在另一个文件中,通过require就可以引入了:
|
|
模块标识
模块标识就是传递给require的参数,可以是以.、..等开头的相对路径、或者绝对路径、或者字符串。
关于Node模块系统更详细的使用方法可以参考阮一峰的 CommonJS规范
AMD 与 RequireJS
全称: Asynchronous Module Definition ,异步模块加载机制。RequireJS正是AMD规范的一种实现。
AMD规范的的核心思想:模块的加载不影响在它之后的程序的执行,所有依赖这个模块的语句都定义在一个回调函数中,模块加载完成后,再运行回调函数。
AMD规范也分模块加载、模块定义。
模块加载
也采用require()语句加载模块,并执行加载完之后的逻辑
|
|
例如:index.js中依赖module1,module2
|
|
模块定义
通过define函数定义在闭包中,define函数格式为:
|
|
参数 | 说明 |
---|---|
id | 模块的名字,可选参数 |
deps | 自身依赖的模块列表,这些依赖模块的输出作为factory的参数。可选参数。 如果没有指定,则默认的依赖模块为[‘require’,’exports’,’module’] |
factory | 一个函数或者对象。若是一个函数,该函数的返回值就是模块的输出。 |
例如定义module1:
|
|
CMD 与 SeaJS
全称: Common Module Definition。
CMD也采用define来定义模块,require来加载模块,但用法不太相同。
模块加载
require方法,接受 模块标识 作为唯一参数,用来获取其他模块提供的接口。
模块定义
define的用法有点像CommonJS 与 AMD的结合体,用法多样。可以直接看 CMD 模块定义规范
|
|
ES6模块
模块功能主要由两个命令构成:export和import。关于ES6模块的用法可以参考 Module 的语法 和 深入浅出ES6(十六):模块 Modules.ES6模块系统与那些模块加载器不同,它是一个静态的模块系统,在编译时就能确定模块关系。
总结
CommonJS 没有什么好说的,同步加载模块,只有加载完成,才能执行后面的操作。反而是在浏览器端使用的模块加载器 Require.js 与 Sea.js网上有很多争论和对比,然而我哪个都没用过。。。现在的项目都是用es6。
这里直接给个传送门,还有知乎上一段很精彩的讨论: LABjs、RequireJS、SeaJS 哪个最好用?为什么?
nodejs | requirejs | seajs |
理解原理
requirejs
module.js
|
|
index.js
|
|
1、 解析模块路径和依赖关系
2、 按依赖关系递归执行创建script标签,请求js,并绑定onload事件来获取该模块加载完成的通知。(有的加载器也会通过 AJAX 去请求脚本然后eval执行,前提是没有跨域。)
|
|
3、依赖的js加载完成后,执行依赖的模块中的回调函数,并将结果给当前回调函数作为参数
seajs
module.js
|
|
index.js
|
|
- 通过回调函数的Function.toString函数,使用正则表达式解析找到require(‘module.js’),找到内部依赖的模块
- 根据配置文件,找到依赖的js文件的实际路径,在dom中插入script标签,载入模块指定的js,绑定加载完成的事件。(加载依赖模块逻辑跟requirejs是一样的)。载入后存储对应的factory函数
- 回调函数内部依赖的js全部加载(暂不调用)完后,调用回调函数
- 当回调函数调用require(‘module.js’)时,执行绑定在’module’这个id上的factory函数,并将返回值传给var module
从seajs源码可以看出在调用require时才执行:
|
|
总结
requirejs与seajs都是提前加载好所有依赖模块,只是模块的factory函数执行时机不同,requirejs是在模块的回调执行前执行依赖模块的factory,拿到依赖模块的输出作为自己的回调的入参。seajs在加载依赖模块后只是存储了模块id与对应的factory函数的关系,在调用require时才执行factory函数并返回结果。对于这一点,我感觉两者没什么差异。
关于循环依赖的问题,例如:在引入b.js时,会去加载b模块的依赖,并执行依赖的factory函数,而它依赖的a模块又依赖了b,就会加载并执行b模块的factory函数。
所以会出现 a模块和b模块的factory同时执行的情况,这样只会执行到相互引用的部分。
b.js
|
|
如果出现循环依赖,最好是重构解决。或者换种写法:
b.js
|
|