模块化是现在编写 JavaScript 的必然选择,而模块规范和我们如何写模块化的代码有很大关系,比如AMD规范与CMD规范,而这些规范具体是指什么呢,下面以仿照sea.js的源码自己实现一个简单的模块加载器来具体了解CMD规范。
还是先来实现一个最简单的模块加载器。
现代模块机制
下面以一个简单的例子来介绍模块机制。html文件内加载了三个js文件,fakeSea为核心库,say.js声明了一个方法say()可以打印hello,main.js引入say.js并使用该方法。
所以下面的代码可以在浏览器中打印出hello。
1 |
|
1 | // fakeSea.js |
1 | // say.js |
1 | // main.js |
为什么使用模块化
当然在一个js文件中声明方法,另一个文件中使用,不用这种所谓的模块化也可以实现,但是存在一个问题就是存在太多全局变量。而使用模块化,全局只存在module一个变量。
而且,现在虽然需要手动在html内引入say.js和main.js,但完善后的fakeSea.js,仅仅只需要引入fakeSea.js一个文件。
同时,需要注意到先引入了say.js再引入main.js,因为main.js需要用到say.js内的函数,即main.js依赖say.js,使用模块化后就不需要担心先后顺序,都在内部进行了处理。
原理
很明显能够看出这是利用了“闭包”,providedMods变量存在于自执行函数的作用域内,按理说外部无法访问到,但是同时存在于该函数作用域内还有load以及declare函数,这两个函数能够访问到,所以declare用来修改providedMods变量,load用来获取providedMods变量。
最后将这两个函数暴露至全局,外部就能够借助着两个函数访问作用域内的变量了。
开始
sea.js最初版本代码量虽然不多只有七百余行,但为了降低难度还是从”简陋版“过渡到”完善版“,一个一个功能点进行实现。
自动处理依赖
只需要指定入口文件,不需要关心先引入哪个模块,我们现在来实现该功能点。
1 |
|
1 | // say.js |
1 | // main.js |
可以看到main.js中指定了say模块的地址为./say.js,声明模块的方式也改变为declare方法接收函数作为参数,在函数内使用exports导出模块。
生成 script 标签
当load模块时,如果模块存在,就可以像上面一样直接使用,不存在则需要借助script标签加载该文件。
1 | function load (ids, callback) { |
重点在于provide函数,
1 | // 加载新模块 |
可想而知,当provide('./say.js')的时候,会生成一个script标签插入head内,当文件下载成功,就会立即执行其中的代码:
1 | module.declare(function (exports) { |
我们”假设“该文件加载成功后,模块就被定义好了可以使用了,那就应该在文件加载成功的时候,通知load函数可以执行回调函数了是吧,所以需要用到多个回调函数:

增加好回调函数后,刷新浏览器如果报错TypeError: say in not a function表示成功,能够执行我们想要执行的函数。
保存模块
由于load时会从providedMods对象中获取需要的模块,所以肯定要在script标签下载文件成功后,将该模块加入到该全局对象中。
为什么不在declare函数内完成该功能?因为declare只接收一个函数,没有name字段,不知道该怎么保存。
sea.js的实现是,在declare内将函数加入到一个数组中,再在能够拿到name的地方取出来。
比如provide函数内,因为此时有url。
1 | // 加载新模块 |
获取模块
OK,我们现在假定已经完成了模块加载,现在是要调用load的回调函数了,我们需要将模块作为参数传入。
1 | // 使用模块 |
这部分代码肯定是没有用了,因为providedMods['./say.js']对应的值是:
1 | function (exports) { |
我们需要的是exports这个变量,而不是整个函数。我们声明一个require函数用来获取依赖。
1 | // 获取依赖 |
通过声明一个空对象,作为参数传入后,经过factory的”加工“后,就变成了say函数。
不过发现之前load代码存在很大的问题,如果
1 | load(['a.js', 'b.js'], function (a, b){ |
a.js和b.js都不存在需要使用script加载,而provide函数在for循环内,每加载成功一个js文件就要调用一次callback很明显不对。
1 | for(var i = 0, len = urls.length; i < len; i++) { |
所以要重写。。。。怎么写呢,将load函数名改为provide,provide改为fetch,再新增load函数:
1 | // 使用模块 |
完整代码
1 | // fakeSea.js |
1 | // main.js |
1 | // say.js |
总结
虽然仅仅是一个简单的模块加载器,但是也能够大概了解如何获取模块、如何保存模块。