如何构建一个微型的CMD模块化加载器

前言

前端模块化是一个老生常谈的话题,模块化的好处是不言而喻,比如易于代码复用、易于维护、易于团队开发d等云云。对于前端模块加载器,以前仅仅止步于会用的阶段,为了加深对前端模块化的理解,大概花了一周的时间来学习、调研并尝试自己实现一个简易版的符合CMD规范的加载器。

设计

加载器是按照CMD规范进行设计的,具体的CMD规范就不列出了,详情请见CMD规范

入口函数 use(ids, callback)

模块定义函数 define(factory)

模块加载函数 require(id)

取得模块接口函数 getModuleExports(module)


代码实现

use(ids, callback)

use为程序启动的入口,主要干两件事:

  1. 加载指定的模块
  2. 待模块加载完成后,调用回调函数
 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的时候,对接口暴露对象moduleexports的区别不是很清楚,学习完别人的源码并尝试自己实现一遍的时候,它们的区别已经非常明朗了: 

exports只是module.exports的一个引用,单纯的改变exports的值并不会对module.exports造成任何影响,所以通过

1 exports = {
2    foo: function(o){
3       return o;
4    }
5 }

 


这样的形式来定义接口是无效的。
 

测试

DEMO请见这里, 源码请见这里

请打开控制台查看结果

总结

果然学习技术最好方法之一就是阅读别人的代码。阅读别人的代码是痛苦的,因为代码里充斥这他个人的代码癖好,有时候一个很简单的条件判语句可能用一些hack技巧实现了之后,在不了解的情况下,看的就比较痛苦了,以为另有玄机,傻乎乎的看了半天。不过,到最后搞明白之后,还是有些许成就感的。

前端模块化加载器,以前是只见树木不见森林,通过这次学习,不能说完全搞清楚了一个模块加载器的所有实现细节,但是对于像模块是怎样实现异步加载的,模块是如何定义的,模块间如何进行依赖分析的这些问题有了一个更深的认识和理解。

参考资料

posted @ 2015-12-21 16:33  Natumsol  阅读(854)  评论(0编辑  收藏  举报