模块化是现在编写 JavaScript 的必然选择,而模块规范和我们如何写模块化的代码有很大关系,比如AMD
规范与CMD
规范,而这些规范具体是指什么呢,下面以仿照sea.js
的源码自己实现一个简单的模块加载器来具体了解CMD
规范。
还是先来实现一个最简单的模块加载器。
现代模块机制 下面以一个简单的例子来介绍模块机制。html
文件内加载了三个js
文件,fakeSea
为核心库,say.js
声明了一个方法say()
可以打印hello
,main.js
引入say.js
并使用该方法。
所以下面的代码可以在浏览器中打印出hello
。
1 2 3 4 5 6 7 8 9 10 11 12 <!DOCTYPE html > <html lang ="zh" > <head > <meta charset ="UTF-8" > <title > 实现简单的 CMD 模块加载器</title > </head > <body > <script src ="./fakeSea.js" > </script > <script src ="./say.js" > </script > <script src ="./main.js" > </script > </body > </html >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var module = {};(function (global ) { var providedMods = {} function load (ids, callback ) { var deps = [] for (var i = 0 , len = ids.length; i < len; i++) { deps[i] = providedMods[ids[i]] } callback && callback.apply(null , deps) } function declare (name, mod ) { providedMods[name] = mod } module .load = load module .declare = declare })(this )
1 2 3 4 module .declare('say' , function ( ) { alert('hello' ) })
1 2 3 4 module .load(['say' ], function (say ) { say() })
为什么使用模块化 当然在一个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 2 3 4 5 6 7 8 9 10 11 <!DOCTYPE html > <html lang ="zh" > <head > <meta charset ="UTF-8" > <title > 实现简单的 CMD 模块加载器</title > </head > <body > <script src ="./fakeSea.js" > </script > <script src ="./main.js" > </script > </body > </html >
1 2 3 4 5 6 module .declare(function (exports ) { exports .say = function ( ) { alert('hello' ) } })
1 2 3 4 module .load(['./say.js' ], function (module ) { module .say() })
可以看到main.js
中指定了say
模块的地址为./say.js
,声明模块的方式也改变为declare
方法接收函数作为参数,在函数内使用exports
导出模块。
生成 script 标签 当load
模块时,如果模块存在,就可以像上面一样直接使用,不存在则需要借助script
标签加载该文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function load (ids, callback ) { var urls = getUnMemoized(ids) if (urls.length === 0 ) { var deps = [] for (var i = 0 , len = ids.length; i < len; i++) { deps[i] = providedMods[ids[i]] } callback && callback.apply(null , deps) } else { for (var i = 0 , len = urls.length; i < len; i++) { provide(urls[i]) } } } function getUnMemoized (ids ) { var unLoadMods = [] for (var i = 0 , len = ids.length; i < len; i++) { if (!providedMods[ids[i]]) unLoadMods.push(ids[i]) } return unLoadMods }
重点在于provide
函数,
1 2 3 4 5 6 7 8 function provide (url ) { var script = document .createElement('script' ) script.src = url script.async = true var head = document .getElementsByTagName('head' )[0 ] head.appendChild(script) }
可想而知,当provide('./say.js')
的时候,会生成一个script
标签插入head
内,当文件下载成功,就会立即执行其中的代码:
1 2 3 4 5 module .declare(function (exports ) { exports = function ( ) { alert('hello' ) } })
我们”假设“该文件加载成功后,模块就被定义好了可以使用了,那就应该在文件加载成功的时候,通知load
函数可以执行回调函数了是吧,所以需要用到多个回调函数:
增加好回调函数后,刷新浏览器如果报错TypeError: say in not a function
表示成功,能够执行我们想要执行的函数。
保存模块 由于load
时会从providedMods
对象中获取需要的模块,所以肯定要在script
标签下载文件成功后,将该模块加入到该全局对象中。
为什么不在declare
函数内完成该功能?因为declare
只接收一个函数,没有name
字段,不知道该怎么保存。
sea.js
的实现是,在declare
内将函数加入到一个数组中,再在能够拿到name
的地方取出来。
比如provide
函数内,因为此时有url
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function provide (url, callback ) { var script = document .createElement('script' ) script.addEventListener('load' , function ( ) { for (var i = 0 , len = pendingMods.length; i < len; i++) { var mod = pendingMods[i] mod && memoize(url, mod) } callback() }) script.src = url script.async = true var head = document .getElementsByTagName('head' )[0 ] head.appendChild(script) }
获取模块 OK,我们现在假定已经完成了模块加载,现在是要调用load
的回调函数了,我们需要将模块作为参数传入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function load (ids, callback ) { var urls = getUnMemoized(ids) if (urls.length === 0 ) { var deps = [] for (var i = 0 , len = ids.length; i < len; i++) { deps[i] = providedMods[ids[i]] } callback && callback.apply(null , deps) } else { for (var i = 0 , len = urls.length; i < len; i++) { provide(urls[i], function ( ) { callback() }) } } }
这部分代码肯定是没有用了,因为providedMods['./say.js']
对应的值是:
1 2 3 4 5 function (exports ) { exports .say = function ( ) { alert('hello' ) } }
我们需要的是exports
这个变量,而不是整个函数。我们声明一个require
函数用来获取依赖。
1 2 3 4 5 6 7 8 9 10 function require (id ) { var factory = providedMods[id] var exports = {} if (typeof factory === 'function' ) { factory(exports ) } return exports }
通过声明一个空对象,作为参数传入后,经过factory
的”加工“后,就变成了say
函数。
不过发现之前load
代码存在很大的问题,如果
1 2 3 load(['a.js' , 'b.js' ], function (a, b ) { })
a.js
和b.js
都不存在需要使用script
加载,而provide
函数在for
循环内,每加载成功一个js
文件就要调用一次callback
很明显不对。
1 2 3 4 5 6 7 for (var i = 0 , len = urls.length; i < len; i++) { provide(urls[i], function ( ) { callback() }) }
所以要重写。。。。怎么写呢,将load
函数名改为provide
,provide
改为fetch
,再新增load
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 function load (ids, callback ) { provide.call(null , ids, function ( ) { var args = [] for (var i = 0 , len = ids.length; i < len; i++) { var mod = require (ids[i]) if (mod) { args.push(mod) } } callback && callback.apply(null , args) }) }
完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 var module = {};(function (global ) { var providedMods = {} var pendingMods = [] function load (ids, callback ) { provide.call(null , ids, function ( ) { var args = [] for (var i = 0 , len = ids.length; i < len; i++) { var mod = require (ids[i]) if (mod) { args.push(mod) } } callback && callback.apply(null , args) }) } function provide (ids, callback ) { var urls = getUnMemoized(ids) if (urls.length === 0 ) { return callback && callback.apply(null , deps) } else { for (var i = 0 , len = urls.length, count = len; i < len; i++) { fetch(urls[i], function ( ) { --count === 0 && callback() }) } } } function declare (factory ) { pendingMods.push(factory) } function memoize (url, mod ) { providedMods[url] = mod } function getUnMemoized (ids ) { var unLoadMods = [] for (var i = 0 , len = ids.length; i < len; i++) { if (!providedMods[ids[i]]) unLoadMods.push(ids[i]) } return unLoadMods } function fetch (url, callback ) { var script = document .createElement('script' ) script.addEventListener('load' , function ( ) { for (var i = 0 , len = pendingMods.length; i < len; i++) { var mod = pendingMods[i] mod && memoize(url, mod) } callback() }) script.src = url script.async = true var head = document .getElementsByTagName('head' )[0 ] head.appendChild(script) } function require (id ) { var factory = providedMods[id] var exports = {} if (typeof factory === 'function' ) { factory(exports ) } return exports } module .load = load module .declare = declare })(this )
1 2 3 4 module .load(['./say.js' ], function (module ) { module .say() })
1 2 3 4 5 6 module .declare(function (exports ) { exports .say = function ( ) { alert('hello' ) } })
总结 虽然仅仅是一个简单的模块加载器,但是也能够大概了解如何获取模块、如何保存模块。