creeperyang / blog

前端博客,关注基础知识和性能优化。
MIT License
2.64k stars 211 forks source link

AMD加载器分析与实现 #17

Open creeperyang opened 8 years ago

creeperyang commented 8 years ago

什么是AMD(不是做显卡的:joy:)?如果不熟的话,require.js总应该比较熟。

AMD是_Asynchronous Module Definition_的缩写,字面上即异步模块定义。require.js是模块加载器,实现了AMD的规范。

本文想说的就是怎么实现一个类似require.js的加载器。但在这之前,我们应该了解下JS模块化的历史。

https://github.com/Huxpro/js-module-7day

这个Slides讲的比我好的多,所以想了解前端模块化的前世今生的可以去看看。这里简单总结下:

为什么需要模块化?

  1. Web Pages正在变成 Web App,应用越大那么代码也越复杂;
  2. 模块化利于解耦,降低复杂性和提高可维护性;
  3. 应用部署可以优化代码,减少http请求(避免多模块文件造成的多请求)。

前端模块历史?

  1. 无模块,全局作用域冲突;
  2. namespace封装,减少暴露给全局作用域的变量,本质是对象,不安全;
  3. IIFE;
  4. 添加依赖的IIFE,即模块化,也是现代模块化的基础;

但模块化还需要解决加载问题:

  1. 原始的script tag,有难以维护,依赖模糊,请求过多的问题;
  2. script loader,如Lab.js,基于文件的依赖管理;
  3. module loader,YUI;
  4. CommonJS,node提供的模块化和加载方案,由于是同步/阻塞加载,所以只适合服务器/本地;
  5. AMD/CMD,异步加载;
  6. Browserify/Webpack,去掉define包裹,在打包时解决模块化;
  7. ES6带来语言原生的模块化方案。
creeperyang commented 8 years ago

好,上面大概聊完了模块化的背景,顺便安利了模块化七日谈(写的真的很好),下面步入正题:怎么实现一个AMD Loader?

读读Amd的规范,结合我们使用require.js的经验,其实核心就是要实现definerequire两个函数。

当然在这之前,我们先设定一下目标,或者说手撸一个_Amd loader_的背景。

理解_Amd loader_的原理,让新手去除对require.js等loader的神秘感,理解模块化运作机制。这是我写这篇文章的目的。

在写这篇文章的过程中,我阅读了一些相关文章,看了require.js的某些实现,这些都对本文有所帮助,非常感谢🙏。

1. 准备工作

首先把一些工具函数,一些前置工作先拎出来讲。

1.1 怎么加载模块/文件?

通过script标签。这是最简单自然的方法,其它可以ajax加载源代码eval,利用worker等等。

    /**
     * load script
     * @param  {String}   url      script path
     * @param  {Function} callback function called after loaded
     */
    function loadScript(url, callback) {
        var node = document.createElement('script');
        var supportOnload = 'onload' in node;

        node.charset = CONFIG.charset || 'utf-8';
        node.setAttribute('data-module', url);

        // bind events
        if (supportOnload) {
            node.onload = function() {
                onload();
            };
            node.onerror = function() {
                onload(true);
            };
        } else {
            // https://github.com/requirejs/requirejs/blob/master/require.js#L1925-L1935
            node.onreadystatechange = function() {
                if (/loaded|complete/.test(node.readyState)) {
                    onload();
                }
            }
        }

        node.async = true;
        node.src = url;

        // For some cache cases in IE 6-8, the script executes before the end
        // of the appendChild execution, so to tie an anonymous define
        // call to the module name (which is stored on the node), hold on
        // to a reference to this node, but clear after the DOM insertion.
        currentlyAddingScript = node;

        // ref: #185 & http://dev.jquery.com/ticket/2709
        baseElement ? head.insertBefore(node, baseElement) : head.appendChild(node);

        currentlyAddingScript = null;

        function onload(error) {
            // ensure only execute once
            node.onload = node.onerror = node.onreadystatechange = null;
            // remove node
            head.removeChild(node);
            node = null;
            callback(error);
        }
    }

代码没什么复杂逻辑,很好理解,就是创建<script>标签加载脚本,完成后删除标签,调用回调。

稍微需要注意的是这里设置了currentlyAddingScript,用于指明当前加载执行的是哪个脚本。

1.2. module id的命名规则,id和url的转换规则

define(id, deps, factory)定义模块

对于define而言,id如果出现,必须是“顶级”的和绝对的(不允许相对名字)。比如jquery是合法的,./jquery是非法的。

deps里也是id,但和require(deps, callback)中deps情形一致,所以放到下面讲。

依赖模块id

依赖模块数组中的id有以下几个情形:

  1. id是绝对路径,如require(['/lib/util', 'http://cdn.com/lib.js'], callback)
  2. id是相对路径,如require(['./lib/util', '../a/b'], callback)
  3. id是ID名,如jquery

对于1和2而言,id都是url,可以统一处理。对于2,以当前模块所在的目录来把相对路径转化成绝对路径。对于绝对路径,那么此时是合法的id,此时id和url相等。

对于3而言,一般需要设置config.paths,因为仅仅根据这个id无法获取url,即无法加载。

    var dotReg = /\/\.\//g;
    var doubleDotReg = /\/[^/]+\/\.\.\//;
    var multiSlashReg = /([^:/])\/+\//g;
    var ignorePartReg = /[?#].*$/;
    var suffixReg = /\.js$/;
    var dirnameReg = /[^?#]*\//;

    function fixPath(path) {
        // /a/b/./c/./d --> /a/b/c/d
        path = path.replace(dotReg, "/");

        // a//b/c --> a/b/c
        // a///b////c --> a/b/c
        path = path.replace(multiSlashReg, "$1/");

        // a/b/c/../../d --> a/b/../d --> a/d
        while (path.match(doubleDotReg)) {
            path = path.replace(doubleDotReg, "/");
        }

        // main/test?foo#bar  -->  main/test
        path = path.replace(ignorePartReg, '');

        if (!suffixReg.test(path)) {
            path += '.js';
        }

        return path;
    }

    function dirname(path) {
        var m = path.match(dirnameReg);
        return m ? m[0] : "./";
    }

    function id2Url(url, baseUrl) {
        url = fixPath(url);
        if (baseUrl) {
            url = fixPath(dirname(baseUrl) + url);
        }
        if (CONFIG.urlArgs) {
            url += CONFIG.urlArgs;
        }
        return url;
    }

2. 整体设计

首先我们必然要暴露definerequire函数给全局对象,加载的模块也应该缓存,那么loader的基本结构应该如下:

(function(root) {
    var CONFIG = {
        baseUrl: '',
        charset: '',
        paths: {},
        shim: {}
    };
    var MODULES = {};
    var cache = {
        modules: MODULES,
        config: CONFIG
    };

   ...

    var define = function(id, deps, factory) {};
    define.amd = {};

    var require = function(ids, callback) {};

    require.config = function(config) {};

    // export to root
    root.define = define;
    root.require = require;
})(this);

然后设计我们的模块系统。以面向对象的思维,把每个模块抽象成Module类的实例:

  1. 当我们需要获取一个模块时,首先尝试从缓存中查找,没有则以url和deps(可选)创建一个模块实例。
  2. 模块开始初始化。
  3. 模块按deps获取自己的所有依赖模块,获取方式按第一步开始。
  4. 模块把自己添加到deps中各个依赖的引用模块列表中。
  5. 如果所有的依赖模块加载完毕,则模块自身运行factory,设置exports,标志自己加载完成,并notify自己的引用模块列表。

设计中最核心的一点是分治思想。我们知道,要把一个模块A真正加载完成,必须确认它的所有依赖模块加载完成。然后模块A本身也可以作为其它模块的依赖模块。So,我们可以转换一下,把模块A设置为其依赖模块的引用模块,当依赖模块加载完成时通知A来执行工厂函数完成挂载exports。

最终我们的Module类设计成:

function Module(url, deps) {}

Module.prototype = {
    constructor: Module,

    load: function() {
        var mod = this;
        var args = [];

        if (mod.status >= STATUS.LOAD) return mod;

        mod.status = STATUS.LOAD;
        mod.resolve();
        mod.setDependents();
        mod.checkCircular();

        // about to execute/load dependencies
        each(mod.dependencies, function(dep) {
            if (dep.status < STATUS.FETCH) {
                dep.fetch();
            } else if (dep.status === STATUS.SAVE) {
                dep.load();
            } else if (dep.status >= STATUS.EXECUTED) {
                args.push(dep.exports);
            }
        });

        mod.status = STATUS.EXECUTING;

        // means load all dependencies
        if (args.length === mod.dependencies.length) {
            args.push(mod.exports);
            mod.makeExports(args);
            mod.status = STATUS.EXECUTED;
            mod.notifyDependents();
        }
    },

    resolve: function() {},

    setDependents: function() {},

    checkCircular: function() {},

    notifyDependents: function() {},

    fetch: function() {},

    onload: function(error) {},

    save: function(deps) {}
}
creeperyang commented 8 years ago

3. 最终实现

https://github.com/creeperyang/amd-loader/blob/master/amd.js 有比较完整的注释,结合上面所讲的,应该比较容易理解。