微信号:FrontDev

介绍:分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯

模块加载器

2016-04-12 19:56 前端大全

(点击上方公众号,可快速关注)


来源:卖烧烤夫斯基

链接:http://www.cnblogs.com/constantince/p/5374371.html


最近在做新项目的时候自己利用一点业余时间写了一个简单的js模块加载器。后来因为用了webpack就没有考虑把它放到项目里面去,也没有继续更新它了。模块加载器开源的有很多,一般来说seaJS和reqiureJS都能满足基本需求。本篇博文主要分享一下卤煮写这个加载器的一些想法和思路,作为学习的记录。


js模块化加载已经不是一个新鲜概念了,很多人都一再强调,大型项目要使用模块化开发,因为一旦随着项目的增大,管理和组织代码的难度会越来越难,使得我们对代码的管理变得重要起来。当然,在后端模块化已经相当成熟,而作为前端的模块化概念,是很久之后才提出来的。模块化好处是使得代码结构更加清晰,高的内聚,功能独立,复用等等。在服务端,随着nodejs 的兴起,js模块化被越来越多地引起人们的注意。但是对于后端和前端来说,最大的区别就是同步和异步加载的问题,因为服务器上获取模块是不需要花费很多的,模块加载进来的时间就操作系统文件的时间,这个过程可以看成是同步的。而在浏览器的前端却需要发送请求到服务器来获取文件,这导致了一个异步延迟的问题,针对这个问题,以AMD规范的异步模块加载器requireJS应运而生。


加载原理


以上简单介绍了一下前端模块化的历程,下面主要介绍一下模块加载主要原理:


1. createElement(‘script’)和appendChild(script) 动态创建脚本,添加到head元素中。


2. fn.toString().match(/.require((“|’)[^)]*(“|’))/g) 将模块转换为字符串,然后通过正则表达式,匹配每个模块中的的依赖文件。


3. 建立脚本加载队列。


4.递归加载,分析完依赖之后,我们需要按照依赖出现的位置,将它们加载到客户端。


5.为每一个命名的模块建立缓存,即 module[name] = callback;


6.currentScript : 对于匿名模块,通过currentScript 来获取文件名,存入到缓存中。


下面贴出对应主要的代码:


一、动态创建脚本


创建脚本较为简单,主要是用createElement方法和appendChild。在创建脚本函数中,我们需要为该脚本绑定一个onload事件,这个事件是为了通知加载脚本队列执行的时间,告诉它什么时候可以加载下一个js文件了。


function _createScript(url) {

//创建script

var script = doc.createElement('script');

var me = this;

//设置属性为异步加载

script.async = true;

script.src = url + '.js';

//为脚本添加加载完成事件

if ('onload' in script) {

script.onload = function(event) {

return _scriptLoaded.call(me, script);

};

} else {

script.onreadystatechange = function() {

if (/loaded|complete/.test(node.readyState)) {

me.next();

_scriptLoaded(script);

}

};

}

//加入script

head.appendChild(script);

}


二、分析依赖建立


分析依赖是模块加载器中最重要的环节之一。每个模块可能会依赖不同的模块,我们需要理清楚这些模块之间的依赖关系,然后分别将它们加载进来。为了分析依赖关系,我们使用toString的方法,将模块转化为一个string,然后去其中寻找依赖。


function _analyseDepend(func) {

//匹配依赖,所有在.reqiure()括号内的依赖都会被匹配出来。

var firstReg = /.require(("|')[^)]*("|'))/g,

secondReg = /(("|')[^)]*("|'))/g,

lastReplaceRge = /(("|')|("|'))/g;

//将模块字符串化

var string = func.toString();

var allFiles = string.match(firstReg);

var newArr = [];

if (!allFiles) {

return '';

}

//将依赖的文件名存入一个堆栈内

allFiles.map(function(v) {  //对文件名做处理

var m = v.match(secondReg)[0].replace(lastReplaceRge, '');

//只有在异步加载的情况下需要 返回解析依赖

if(!modules[_analyseName(m)]) {

newArr.push(m);

}

});

if(newArr.length > 0) {

return newArr;

}else{

return ''

}

}


三、建立脚本加载队列


分析完依赖之后,我们可以得到一个脚本名称的栈,我们从其中获取脚本名称,依次按照顺序地加载它们。因为每个脚本加载过程都是异步的,所以,我们需要有一个异步加载机制。在这里,我们使用了设计模式中的职责链条模式来完成整个异步加载过程。通过在onload事件通知队列加载的完成情况。下面是职责链模式的实现代码


function _Chain() {

    this.cache = [];

}

/**

 * add function to order stack

 * @param func (func)

 * @returns {_Chain}

 */

_Chain.prototype.after = function(fn) {

        this.cache.push(fn);

        this.cur = 0;

        return this;

    }

    /**

     * To pass the authority to next function excute

     * @param

     * @returns

     */

_Chain.prototype.passRequest = function() {

        var result = 'continue';

        while (this.cur < this.cache.length && result === 'continue') {

            result = this.cache[this.cur++].apply(this, arguments);

            if (this.cur === this.cache.length) {

                this.clear();

            }

        }

    }

    /**

     * an api to excute func in stack

     * @param

     * @returns

     */

_Chain.prototype.next = function() {

        this.excute();

    }

    /**

     * let use to excute those function

     * @param

     * @returns

     */

_Chain.prototype.excute = function() {

    this.passRequest.apply(this, arguments)

}


/**

 * to clear stack all function

 * @param

 * @returns

 */

_Chain.prototype.clear = function() {

    this.cache = [];

    this.cur = 0;

}


var excuteChain = new _Chain();


每个脚本加载完毕后调用next函数,可以通知职责链中的下一个函数继续执行,这样解决了异步加载问题。这里将模式的实现代码放到模块加载器中是不太合适的,一般情况下我们可以将它独立出来,放入公共模块当中,为其他的模块共同使用。但这里纯粹是一个单文件的项目,所以就暂时将它放入此处。


四、递归加载


根据模块中的依赖出现的次序,依次加载各个模块。


function _excuteRequire(depends) {

if (depends.length === 0) {

var u = excuteStack.length;

while (u--) {

var params = excuteStack[u]();

if (u === 0) {

Events.trigger('excute', params);

excuteStack = [];

}

}

}

}


五、为模块建立缓存对象


//在文件加载完毕后将模块存入缓存

return modules[string] = func();


六、currentScript


currentScript主要是用来解决获取那些未命名的模块的js文件名,如 define(function(){})这样的模块是匿名的,我们通过这个方法可以获取正在执行的脚本文件名,从而为其建立缓存。


function _getCurrentScript() {

//取得正在解析的script节点

if (doc.currentScript) {

//firefox 4+

return doc.currentScript;

}

}


七、定义module


最后我们需要做的事给出定义模块的方法,一般情况下定义方法主要分以下几种:


1.define(‘a’, function(){})


2.define(function(){})


第一种是命名的模块,第二种是未命名的模块,我们需要对它们分别处理。用typeof方法分析参数,建立以string方法为基础的加载模式:


function define() {

    var arg = Array.prototype.slice.call(arguments);

    var paramType = Object.prototype.toString.call(arg[0]).split(' ')[1].replace(/]/, '');

    defineParamObj[paramType].apply(null, arg);

    // Chain.excute();

}


function _String(string, func) {

    string = _analyseName(string);

    //分析依赖

    var depends = _analyseDepend(func) || [];

    // 将加载好的模块存入缓存

    excuteStack.push(function() {

        return modules[string] = func();

    });

    //执行加载依赖函数

    _excuteRequire(depends);

    for (var i = 0, l = depends.length; i < l; i++) {

        (function(i) {

            excuteChain.after(function() {

                var c = require(depends[i]);

                if(c) {

                    this.next();

                };

            });

        })(i);

    }

}


function _Function(func) {

    var name = _analyseName(_getCurrentScript().src);

    _String(name, func);

}


结束


以上就是一个实现模块加载器的主要原理,卤煮写完发现也只有四百行的代码,实现了最基本的模块加载功能。当然,其中还有很多细节没有实现,比起大而全的requireJs来说,只是一个小儿科而已。但是明白了主要这几项后,对于我们来说就足够理解一个模块加载器的实现方式了。代码存入github上: https://github.com/constantince/require




【今日微信公号推荐↓】

更多推荐请看值得关注的技术和设计公众号


其中推荐了包括技术设计极客 和 IT相亲相关的热门公众号。技术涵盖:Python、Web前端、Java、安卓、iOS、PHP、C/C++、.NET、Linux、数据库、运维、大数据、算法、IT职场等。点击《值得关注的技术和设计公众号》,发现精彩!

 
前端大全 更多文章 详解Javascript中的Object对象 结合个人经历总结的前端入门方法 前端不为人知的一面–前端冷知识集锦 一份优秀的前端开发工程师简历是怎么样的? 浅谈Web缓存
猜您喜欢 最优雅退出 Android 应用程序的 6 种方式 Android UI线程和非UI线程 儿童节和你有关系么?当然有 为什么你得学些 TCP 的知识? LinkedME,用深度链接打破App信息孤岛