前言
前端模块化是一个老生常谈的话题,模块化的好处是不言而喻,比如易于代码复用、易于维护、易于团队开发d等云云。对于前端模块加载器,以前仅仅止步于会用的阶段,为了加深对前端模块化的理解,大概花了一周的时间来学习、调研并尝试自己实现一个简易版的符合CMD
规范的加载器。
设计
加载器是按照CMD
规范进行设计的,具体的CMD
规范就不列出了,详情请见CMD规范。
入口函数 use(ids, callback)
模块定义函数 define(factory)
模块加载函数 require(id)
取得模块接口函数 getModuleExports(module)
代码实现
use(ids, callback)
use
为程序启动的入口,主要干两件事:
- 加载指定的模块
- 待模块加载完成后,调用回调函数
1 function use(ids, callback) { 2 if (!Array.isArray(ids)) ids = [ids]; 3 Promise.all(ids.map(function (id) { 4 return load(myLoader.config.root + id); 5 })).then(function (list) { 6 callback.apply(global, list);// 加载完成, 调用回调函数 7 }, function (error) { 8 throw error; 9 }); 10 }
1 function load(id) { 2 return new Promise(function (resolve, reject) { 3 var module = myLoader.modules[id] || Module.create(id); // 取得模块或者新建模块 此时模块正在加载或者已经加载完成 4 module.on("complete", function () { 5 var exports = getModuleExports(module); 6 resolve(exports);// 加载完成-> 通知调用者 7 }) 8 module.on("error", reject); 9 }) 10 }
use
会调用load
函数,这个函数的作用是根据模块的id
,加载模块,并返回一个Promise
对象。
define(factory)
define
的作用主要是用来定义一个模块。按照CMD
的规范,定义一个模块的代码类似:
1 var factory = function(require, exports, module){ 2 // some code 3 } 4 define(factory);
为了方便说明,我给匿名函数取名为factory, factory就是我们模块定义的工厂函数,它只是define函数的一个参数,并不会被直接执行,而是会在需要的时候由专门的函数来调用生成接口。
所以, 一个模块文件被浏览器下载下来后,并不会直接运行我们的模块定义代码,而是会首先执行一个define
函数,这个函数会取得模块定义的源代码(通过函数的toString()
函数来取得源代码),然后利用正则匹配找到依赖的模块(匹配require("dep.js")
这样的字符串),然后加载依赖的模块,最后发射一个自定义事件complete
,通知当前模块, 模块已经加载完成,此时,当前模块的就会调用与complete
事件绑定的回调函数,完成与这个模块相关的任务,比如resolve
与这个模块加载绑定的Promise
。
具体实现为:
1 function define(factory) { 2 var id = getCurrentScript(); 3 id = id.replace(location.origin, ""); 4 var module = myLoader.modules[id]; 5 module.factory = factory; 6 var dependences = getDependcencs(factory); 7 if (dependences) { 8 Promise.all(dependences.map(function (dep) { 9 return load(myLoader.config.root + dep); 10 })).then(function () { 11 module.fire("complete"); // 依赖加载完成,通知模块。 12 }, function () { 13 module.fire("error"); 14 }); 15 } else { 16 module.fire("complete");//没有依赖,通知模块加载完成 17 } 18 }
require(id)
require
函数比较简单,主要作用就是根据模块id
获取指定的模块,然后返回这个模块的对外接口。
1 function require(id) { 2 var module = myLoader.modules[myLoader.config.root + id]; 3 if (!module) throw "can not load find module by id:" + id; 4 else { 5 return getModuleExports(module); // 返回模块的对外接口。 6 } 7 }
模块定义代码直到现在,才会被运行。运行模块定义代码的函数就是getModuleExports
函数:
1 function getModuleExports(module) { 2 if (!module.exports) { 3 module.exports = {}; 4 module.factory(require, module.exports, module); 5 } 6 return module.exports; 7 }
记得刚接触sea.js
的时候,对接口暴露对象module
和exports
的区别不是很清楚,学习完别人的源码并尝试自己实现一遍的时候,它们的区别已经非常明朗了:
exports
只是module.exports
的一个引用,单纯的改变exports的值并不会对module.exports
造成任何影响,所以通过
1 exports = { 2 foo: function(o){ 3 return o; 4 } 5 }
这样的形式来定义接口是无效的。
测试
请打开控制台查看结果
总结
果然学习技术最好方法之一就是阅读别人的代码。阅读别人的代码是痛苦的,因为代码里充斥这他个人的代码癖好,有时候一个很简单的条件判语句可能用一些hack技巧实现了之后,在不了解的情况下,看的就比较痛苦了,以为另有玄机,傻乎乎的看了半天。不过,到最后搞明白之后,还是有些许成就感的。
前端模块化加载器,以前是只见树木不见森林,通过这次学习,不能说完全搞清楚了一个模块加载器的所有实现细节,但是对于像模块是怎样实现异步加载的,模块是如何定义的,模块间如何进行依赖分析的这些问题有了一个更深的认识和理解。