自己动手写AMD Loader

上一篇博客Javascript模块化介绍了前端的模块化的一些方案和历史,基于浏览器的AMD规范,我们尝试自己实现一个AMD Loader

AMD Loader要解决的问题

AMD主要是为了解决浏览器端的模块化,实现AMD Loader的话,以下的点要考虑到:

  • 前端的加载是异步的
  • 模块的依赖也是模块(可能会出现循环依赖的问题)
  • 每个Module只加载一次
  • AMD Loader暴露 definerequire

其实我们主要要解决的点在于,如何处理Module的加载。Module加载要解决两个主要问题,一是对模块的引用(对path的处理);二是如何维护模块之间的引用关系。

模块代码的加载

首先来看看defineAPI接口:

define(id?, dependence?, factory)

define的函数中id是表示当前模块的idid可以是字符串或者一个绝对路径的字符串,这里规定不能出现相对id,例如’./myModule’或者 ‘../myModule’;id是可选的,如果没有提供id的话,默认id名是Module的在浏览器加载当前Moduleurldependence,是一个字符串数组,字符串是依赖模块的url,可以是相对路径、绝对路径、字符串(如JQuery)。 所以,我们首先要解决的问题是,如何去规范化用户传进来的各种各样的url,例如,下面这些url的规范化:

 ./a/b/c//d --> a/b/c/d
 ./a/b/c/../../d --> ./a/d
 ./a/b/c/./d --> ./a/b/c/d
 main/test?foo#bar --> main/test

对于这些URl,我们可以写正则表达式去匹配,在用户传入模块的’url’,需要过一遍我们的正则,将其规范化,对于依赖模块,我们要基于当前ConfigbaseURL(如果在config中配置了baseUrl的话),拼成完整的url,让浏览器的script去加载,在加载完之后,将对应的script删除。

    function loadScript(url, callback) {
        var node = document.createElement('script');
        var head = document.head;
        node.setAttribute('data-module', url);
        node.async = true;
        node.src = url;
        
        function onload() {
            node.onload = node.onerror = null;
            head.removeChild(node);
            callback();
        }
        node.onerror = function(error) {
            node.onload = node.onerror = null;
            head.removeChild(node);
            callback(error)
        }
    }

如上,是使用script标签加载模块的一些代码,基本逻辑是:

创建script标签 ---> 加载模块源码 ---> 模块加载完成后触发回调函数

这里主要是利用script标签来做模块的加载,这里给script标签加上了async=true的标志,浏览器在解析HTML页面的时候,不会因为加载script阻塞住页面的解析,在script加载成功后执行script中的代码。

模块的定义

在使用AMD Loader定义我们的Module的时候,使用define(id?, dependence?, factory)的接口来定义我们的Module,在实现模块定义的时候,我们可以预想到有以下问题:

  • 模块的加载是异步的,意味着我们需要设计notify机制
  • 我们需要解决模块循环依赖问题
  • 模块仅仅被加载一次,需要有模块的cache

main 如上,对于main Moduledep0, dep1, dep2三个依赖,这里我们在定义main Module的时候,将main Module中的每个依赖(在声明中只有url)实例化成Module,在加载main Module的时候,得先要加载我们的依赖Module,这里因为Module的加载是异步的,所以在实例化我们的依赖的时候,在每个Module中保存一个refs的数组,这个数组中保存着依赖这个Moduleid,当前Module加载完成之后,在onloadcallback中去通知当前模块load完成,然后当前Module会查看当前Modulerefs中所有的Module,在refs中这些Module就会收集自己所有的依赖是不是都加载完成,如果加载完成了,当前的Module就加载完成了,否则就触发当前ref的重新load(为了触发其他未被加载的依赖的加载)。

解决了notify的问题,剩下的两个问题主要是对Module状态的维护,如下图,是整个Module的加载的状态转移图: AMDLoader

依照上图的状态转移图,Module Class的设计如下:

function Module(url, deps) {
    this.refs = [];
    this.depsUrl = getVaildUrl(deps);
    this.depsModule = [];
    this.STATUS = STATUS.INIT;
}

Module.prototype = {
    constructor: Module,
    fetch: function() {},  // 加载module的代码
    save: function(depsUrl), // 检测修正依赖的url,并剔除已经被加载的module的url
    resolve: function(), // 实例化每个依赖
    setDependencies: function(), // 设置依赖当前模块的refs
    checkCircular: function(), // 检测循环依赖
    makeExports: function(), // 调用Module的factory函数并输出exports
    load: function() {}, // 加载模块
}

对于Module本身来说我们这里也需要维护一个状态,来表明当前Module的执行状态:

init --> fetch --> save --> load --> executing --> executed or error

AMD-Loader的完整代码 —> AMD Loader代码实现仅为了解AMD模块加载的原理,并不能用在生产环境下

Reference


兴趣遍地都是,坚持和持之以恒才是稀缺的


Written by@wang yao
I explain with words and code.

GitHub