Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AMD加载器分析与实现 #17

Open
creeperyang opened this issue Mar 10, 2016 · 2 comments
Open

AMD加载器分析与实现 #17

creeperyang opened this issue Mar 10, 2016 · 2 comments

Comments

@creeperyang
Copy link
Owner

什么是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
Copy link
Owner Author

好,上面大概聊完了模块化的背景,顺便安利了_模块化七日谈_(写的真的很好),下面步入正题:怎么实现一个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
Copy link
Owner Author

3. 最终实现

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant