AMD加载器实现笔记(二)

 AMD加载器实现笔记(一)中,我们实现了一个简易的模块加载器。但到目前为止这个加载器还并不能称为AMD加载器,原因很简单,我们还不支持AMD规范中的config配置。这篇文章中我们来添加对config的中baseUrl和packages的支持。API设计如下:

require.config({
    baseUrl: "./",
    packages: [{
        name: "more",
        location: "./more"
    }, {
        name: "mass",
        location: "../"
    }, {
        name: "wab",
        location: "../../../"
    }]
  });

 主要原则是将baseUrl和packages中location的路径转化为绝对路径。核心算法如下:

  翻译成代码则为:

function getRoute(base, target) {
        var bts = base.replace(/\/$/, "").split('/');  //base dir
        var tts = target.split('/'); //target parts
        while (isDefined(tts[0])) {
            if (tts[0] === '.') {
                return bts.join('/') + '/' + tts.slice(1).join('/');
            } else if (tts[0] === '..') {
                bts.pop();
                tts.shift();
            } else {
                return bts.join('/') + '/' + tts.join('/');
            }
        }
    };

剩下的处理就变得简单起来,首先得到baseUrl的绝对路径,然后根据baseUrl得到各个package中location的绝对路径。代码如下:

global.require.config = function(config) {
        this.parsedConfig = {};
        if (config.baseUrl) {
            var currentUrl = getCurrentScript();
            var parts = currentUrl.split('/');
            parts.pop();
            var currentDir = parts.join('/');
            this.parsedConfig.baseUrl = getRoute(currentDir, config.baseUrl);
        }
        var burl = this.parsedConfig.baseUrl;
        // 得到baseUrl后,location相对baseUrl定位
        this.parsedConfig.packages = [];
        if (config.packages) {
            for (var i = 0, len = config.packages.length; i < len; i++) {
                var pck = config.packages[i];
                var cp = {
                    name: pck.name,
                    location: getRoute(burl, pck.location)
                }
                this.parsedConfig.packages.push(cp);
            }
        }

        console.log(this.parsedConfig);
    }

到了这里模块的依赖模块Id就不用再使用绝对路径了,可以按照正常AMD规范中的来了。如:

define(["aaa",
"bbb",
"ccc",
"fff"],function(a,b,c,f){
    $.log("已加载ddd模块", 7);
    return {
        bbb: b,
        ddd: "ddd",
        length: arguments.length
    }
})

那么问题来了,这个时候module仓库的key该变成生么样子呢?继续保持原来的绝对路径形式,还是使用上文中的moduleId(aaa、bbb、ccc、fff)。答案是前者;使用后者的话,如果一个依赖是相对路径,比如:"./utils",可能会有多个模块都依赖这个id,但这些模块未必是需要同一个utils文件。所以我们程序中对require函数需要做一些修改,将deps中的moduleId转化为绝对路径。

// dep为非绝对路径形式,而modules的key仍然需要绝对路径
        deps = deps.map(function(dep) {
            var rel = "";
            if (/^Bodhi/.test(id)) {
                rel = global.require.parsedConfig.baseUrl;
            } else {
                var parts = parent.split('/');
                parts.pop();
                rel = parts.join('/');
            }
            return getModuleUrl(dep, rel);
        });

  getModuleUrl函数的处理方式为:

  • 如果dep在某一package中,则将package的location作为参考目录
  • 如果dep像相对路径,则将baseUrl作为参考目录
  • 以上两种除外,则使用baseUrl来拼接路径

  代码如下:

function getModuleUrl(moduleId, relative) {
        function getPackage(nm) {
            for (var i = 0, len = require.parsedConfig.packages.length; i < len; i++) {
                var pck = require.parsedConfig.packages[i];
                if (nm === pck.name) {
                    return pck;
                }
            }
            return false;
        }
        var mts = moduleId.split('/');
        var pck = getPackage(mts[0]);
        if (pck) {
            mts.shift();
            return getRoute(pck.location, mts.join('/'));
        } else if (mts[0] === '.' || mts[0] === '..') {
            return getRoute(relative, moduleId);
        } else {
            return getRoute(require.parsedConfig.baseUrl, moduleId);
        }
    }

 到目前为止我们加载器已经支持了config中的baseUrl和packages,下篇文章我们让它来支持paths与shim。

  加载器整体代码如下:

(function(global){
    global.$ = {
        log: function(m) {
            console.log(m);
        }
    };
    global = global || window;
    modules = {};
    loadings = [];
    loadedJs = [];
    //module: id, state, factory, result, deps;
    global.require = function(deps, callback, parent){
        var id = parent || "Bodhi" + Date.now();
        var cn = 0, dn = deps.length;
        var args = [];

         // dep为非绝对路径形式,而modules的key仍然需要绝对路径
        deps = deps.map(function(dep) {
            var rel = "";
            if (/^Bodhi/.test(id)) {
                rel = global.require.parsedConfig.baseUrl;
            } else {
                var parts = parent.split('/');
                parts.pop();
                rel = parts.join('/');
            }
            return getModuleUrl(dep, rel);
        });

        var module = {
            id: id,
            deps: deps,
            factory: callback,
            state: 1,
            result: null
        };
        modules[id] = module;

        deps.forEach(function(dep) {
            if (modules[dep] && modules[dep].state === 2) {
                cn++
                args.push(modules[dep].result);
            } else if (!(modules[dep] && modules[dep].state === 1) && loadedJs.indexOf(dep) === -1) {
                loadJS(dep);
                loadedJs.push(dep);
            }
        });
        if (cn === dn) {
            callFactory(module);
        } else {
            loadings.push(id);
            checkDeps();
        }
    };

    global.require.config = function(config) {
        this.parsedConfig = {};
        if (config.baseUrl) {
            var currentUrl = getCurrentScript();
            var parts = currentUrl.split('/');
            parts.pop();
            var currentDir = parts.join('/');
            this.parsedConfig.baseUrl = getRoute(currentDir, config.baseUrl);
        }
        var burl = this.parsedConfig.baseUrl;
        // 得到baseUrl后,location相对baseUrl定位
        this.parsedConfig.packages = [];
        if (config.packages) {
            for (var i = 0, len = config.packages.length; i < len; i++) {
                var pck = config.packages[i];
                var cp = {
                    name: pck.name,
                    location: getRoute(burl, pck.location)
                }
                this.parsedConfig.packages.push(cp);
            }
        }

        console.log(this.parsedConfig);
    }

    global.define = function(deps, callback) {
        var id = getCurrentScript();
        if (modules[id]) {
            console.error('multiple define module: ' + id);
        }

        require(deps, callback, id);
    };

    function getRoute(base, target) {
        var bts = base.replace(/\/$/, "").split('/');  //base dir
        var tts = target.split('/'); //target parts
        while (isDefined(tts[0])) {
            if (tts[0] === '.') {
                return bts.join('/') + '/' + tts.slice(1).join('/');
            } else if (tts[0] === '..') {
                bts.pop();
                tts.shift();
            } else {
                return bts.join('/') + '/' + tts.join('/');
            }
        }
    };

    function isDefined(v) {
        return v !== null && v !== undefined;
    }

    function getModuleUrl(moduleId, relative) {
        function getPackage(nm) {
            for (var i = 0, len = require.parsedConfig.packages.length; i < len; i++) {
                var pck = require.parsedConfig.packages[i];
                if (nm === pck.name) {
                    return pck;
                }
            }
            return false;
        }
        var mts = moduleId.split('/');
        var pck = getPackage(mts[0]);
        if (pck) {
            mts.shift();
            return getRoute(pck.location, mts.join('/'));
        } else if (mts[0] === '.' || mts[0] === '..') {
            return getRoute(relative, moduleId);
        } else {
            return getRoute(require.parsedConfig.baseUrl, moduleId);
        }
    }

    function loadJS(url) {
        var script = document.createElement('script');
        script.type = "text/javascript";
        //var url = getModuleUrl(mId, rel);
        script.src = url + '.js';
        script.onload = function() {
            var module = modules[url];
            if (module && isReady(module) && loadings.indexOf(url) > -1) {
                callFactory(module);
            }
            checkDeps();
        };
        var head = document.getElementsByTagName('head')[0];
        head.appendChild(script);
    };

    function checkDeps() {
        for (var p in modules) {
            var module = modules[p];
            if (isReady(module) && loadings.indexOf(module.id) > -1) {
                callFactory(module);
                checkDeps(); // 如果成功,在执行一次,防止有些模块就差这次模块没有成功
            }
        }
    };

    function isReady(m) {
        var deps = m.deps;
        var allReady = deps.every(function(dep) {
            return modules[dep] && isReady(modules[dep]) && modules[dep].state === 2;
        })
        if (deps.length === 0 || allReady) {
            return true;
        }
    };

    function callFactory(m) {
        var args = [];
        for (var i = 0, len = m.deps.length; i < len; i++) {
            args.push(modules[m.deps[i]].result);
        }
        m.result = m.factory.apply(window, args);
        m.state = 2;

        var idx = loadings.indexOf(m.id);
        if (idx > -1) {
            loadings.splice(idx, 1);
        }
    };

    function getCurrentScript(base) {
        // 参考 https://github.com/samyk/jiagra/blob/master/jiagra.js
        var stack;
        try {
            a.b.c(); //强制报错,以便捕获e.stack
        } catch (e) { //safari的错误对象只有line,sourceId,sourceURL
            stack = e.stack;
            if (!stack && window.opera) {
                //opera 9没有e.stack,但有e.Backtrace,但不能直接取得,需要对e对象转字符串进行抽取
                stack = (String(e).match(/of linked script \S+/g) || []).join(" ");
            }
        }
        if (stack) {
            /**e.stack最后一行在所有支持的浏览器大致如下:
             *chrome23:
             * at http://113.93.50.63/data.js:4:1
             *firefox17:
             *@http://113.93.50.63/query.js:4
             *opera12:http://www.oldapps.com/opera.php?system=Windows_XP
             *@http://113.93.50.63/data.js:4
             *IE10:
             *  at Global code (http://113.93.50.63/data.js:4:1)
             *  //firefox4+ 可以用document.currentScript
             */
            stack = stack.split(/[@ ]/g).pop(); //取得最后一行,最后一个空格或@之后的部分
            stack = stack[0] === "(" ? stack.slice(1, -1) : stack.replace(/\s/, ""); //去掉换行符
            return stack.replace(/(:\d+)?:\d+$/i, "").replace(/\.js$/, ""); //去掉行号与或许存在的出错字符起始位置
        }
        var nodes = (base ? document : head).getElementsByTagName("script"); //只在head标签中寻找
        for (var i = nodes.length, node; node = nodes[--i]; ) {
            if ((base || node.className === moduleClass) && node.readyState === "interactive") {
                return node.className = node.src;
            }
        }
    };
})(window)

测试:

require([
  'bbb',
  'aaa.bbb.ccc',
  'ccc',
  'ddd',
  'fff'
  ], function(aaabbbccc){
    console.log('simple loader');
    console.log(arguments);
  });

 输出结果:

时间: 2024-10-01 01:35:54

AMD加载器实现笔记(二)的相关文章

AMD加载器实现笔记(一)

代码我是有看过的,基本的原理也都明白,但实际动手去实现却是没有的.因为今年计划的dojo教程<静静的dojo>中,有一章节来专门讲解AMD,不免要把对AMD的研究回炉一下.时隔多日,再回头探索AMD实现原理时,竟抓耳挠腮,苦苦思索不得要领.作为开发人员,深感惭愧.故有此文,记录我在实现一个AMD加载器时的思考总结.   requireJS是所有AMD加载器中,最广为人知的一个.目前的版本更凝聚了几位大牛数年心血,必然不是我这个小虾米一晚上的粗制滥造能够比拟的,所以目前为止这篇文章里的加载器尚不

AMD加载器实现笔记(五)

前几篇文章对AMD规范中的config属性几乎全部支持了,这一节主要是进一步完善.到目前为止我们的加载器还无法处理环形依赖的问题,这一节就是解决环形依赖. 所谓环形依赖,指的是模块A的所有依赖项的依赖中有没有依赖A模块本身的模块.如果有那就说明存在环形依赖.所以检验的方式是利用递归,检查一个模块的依赖的依赖项中有没有依赖A模块,以及依赖项的依赖项的依赖项中有没有A模块,核心代码如下: function checkCircleRef(start, target){ var m = modules[

AMD加载器实现笔记(四)

继续这一系列的内容,到目前为止除了AMD规范中config的map.config参数外,我们已经全部支持其他属性了.这一篇文章中,我们来为增加对map的支持.同样问题,想要增加map的支持首先要知道map的语义. 主要用于解决在两个不同模块集中使用一个模块的不同版本,并且保证两个模块集的交互没有冲突. 假设磁盘有如下文件: 当'some/newmodule'请求'foo'模块时,它将从foo1.2.js总得到'foo1.2'模块:当'some/oldmodule'请求'foo'模块时它将从foo

使用 AMD 加载器整合多种主流 JavaScript 框架

AMD 简要介绍 AMD 是 Asynchronous Module Definition 的简称,即异步模块加载机制.该规范是由 CommonJS 提出的一种异步动态加载 JavaScript 代码的 API 规范.AMD 以简洁优雅的运行方式得到了很多主流 JavaScript 框架的 青睐,主流 JavaScript 开源框架逐渐开始用 AMD 规范来实现对其代码模块的动态加载.在 AMD 日益流行的情况下,开发 过程中的 JavaScript 框架迁移变得更加方便,同时,在一个应用中引入

PHP MVC框架之加载器学习笔记

上节说了MVC的路由原理,这节我们来说说MVC框架的loader,也称为加载器. 虽然不同的框架,加载器的用法不同,但是其原理都是相通的...都是通过单例模式加载文件,而且把已加载的文件cache起来,避免重复加载...因为在开发业务的过程中,常常需要加载不同的模块,不同的类库等,MVC的loader可以帮助我们实现单例模式,更加符合开发需求. 下面我们开始动手,利用其原理,做一个简单的loader. 首先,我们设计的加载器,要满足可以全局使用,无论在controller里还是在model.li

概述如何实现一个简单的浏览器端js模块加载器_javascript技巧

在es6之前,js不像其他语言自带成熟的模块化功能,页面只能靠插入一个个script标签来引入自己的或第三方的脚本,并且容易带来命名冲突的问题.js社区做了很多努力,在当时的运行环境中,实现"模块"的效果. 通用的js模块化标准有CommonJS与AMD,前者运用于node环境,后者在浏览器环境中由Require.js等实现.此外还有国内的开源项目Sea.js,遵循CMD规范.(目前随着es6的普及已经停止维护,不论是AMD还是CMD,都将是一段历史了) 浏览器端js加载器 实现一个简

我理解的Android加载器

Android的加载器(loader)是从Android 3.0开始出来的东西.要理解这里需要先理解为什么会出现加载器(也有地方把它说成是装载器)呢? 如果没有加载器... 首先Activity是我们的前端页面展现,数据库是我们的数据持久化地址,那么正常的逻辑就是在展示页面的渲染页面的阶段进行数据库查询.拿到数据以后才展示页面. 但是这个逻辑有一些缺点:   首先是查询数据的逻辑放在了UI生成的同个线程中,这个就意味着在查询数据的时候,UI页面生成的工作被阻塞住了.UI一旦被阻塞用户就会被感知出

如何通过预加载器提升网页加载速度

预加载器(Pre-loader)可以说是提高浏览器性能最重要的举措.Mozilla 官方发布数据,通过预加载器技术网页的加载性能提升了19%,Chrome测试了 Alexa 排名前2000名网站,性能有20%的提升. 它并不是一门新技术,有人认为只有 Chrome 才具备这个功能.也有人认为它是有史以来提升浏览器性能最有效的方法.如果你第一次接触预加载器,也许心中已经有了无数个问号.什么是预加载器?它是如何提升浏览器性能的? 首先需要了解浏览器是如何加载网页的 一个网页的加载依赖于脚本文件.CS

加快页面的载入速度:异步模块加载器In.js

文章简介:用In.js颗粒化管理.加载你的Javascript模块. 近一年来,国内外都十分热衷于异步加载的研究,为了加快页面的载入速度,无阻塞加载Javascript的方法和框架成为了前端开发的焦点和亮点之一. 国外的像基于jQuery的RequireJs,YUI Loader,LabJs,RunJs,国内也有淘宝的SeaJs,豆瓣的DoJs等,这些都是一些十分优秀的模块加载器.但是本文将会向大家介绍一个新的开源的轻量级"多线程"异步模块加载器In.js,In的开发借鉴了Do的一些思