Notes.

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

2015-12-21 • ☕️ 2 min read

前言

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

设计

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

入口函数 use(ids, callback)

use

模块定义函数 define(factory)

define

模块加载函数 require(id)

require

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

define

代码实现

use(ids, callback)

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

  1. 加载指定的模块
  2. 待模块加载完成后,调用回调函数
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的时候,对接口暴露对象moduleexports的区别不是很清楚,学习完别人的源码并尝试自己实现一遍的时候,它们的区别已经非常明朗了: exports只是module.exports的一个引用,单纯的改变 exports 的值并不会对module.exports造成任何影响,所以通过

exports = {
   foo: function(o){
      return o;
   }
}

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

测试

DEMO 请见 这里

请打开控制台查看结果

总结

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

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

参考资料


Natumsol

Personal blog by Natumsol.
Note thoughts and experience.