模块化是现在编写 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 |
总结
虽然仅仅是一个简单的模块加载器,但是也能够大概了解如何获取模块、如何保存模块。