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