模块加载器


模块加载器

最近在做新项目的时候自己利用一点业余时间写了一个简单的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文件了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function_createScript(url) {
//创建script
varscript = doc.createElement('script');
varme =this;
//设置属性为异步加载
script.async =true;
script.src = url +'.js';
//为脚本添加加载完成事件
if('onload'inscript) {
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,然后去其中寻找依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function_analyseDepend(func) {
//匹配依赖,所有在.reqiure()括号内的依赖都会被匹配出来。
varfirstReg = /\.require\((\"|\')[^\)]*(\"|\')\)/g,
secondReg = /\((\"|\')[^\)]*(\"|\')\)/g,
lastReplaceRge = /\((\"|\')|(\"|\')\)/g;
//将模块字符串化
var string = func.toString();
var allFiles = string.match(firstReg);
var newArr = [];
if (!allFiles) {
return '';
}
//将依赖的文件名存入一个堆栈内
allFiles.map(function(v) {<br>  //对文件名做处理
var m = v.match(secondReg)[0].replace(lastReplaceRge, '');
//只有在异步加载的情况下需要 返回解析依赖
if(!modules[_analyseName(m)]) {
newArr.push(m);
}
});
if(newArr.length > 0) {
return newArr;
}else{
return ''
}
}

三、建立脚本加载队列

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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;
returnthis;
}
/**
* To pass the authority to next function excute
* @param
* @returns
*/
_Chain.prototype.passRequest =function() {
varresult ='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;
}
varexcuteChain =new_Chain();

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

四、递归加载

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

1
2
3
4
5
6
7
8
9
10
11
12
function_excuteRequire(depends) {
if(depends.length === 0) {
varu = excuteStack.length;
while(u--) {
varparams = excuteStack[u]();
if(u === 0) {
Events.trigger('excute', params);
excuteStack = [];
}
}
}
}

五、为模块建立缓存对象

1
2
//在文件加载完毕后将模块存入缓存
returnmodules[string] = func();

六、currentScript

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

1
2
3
4
5
6
7
function_getCurrentScript() {
//取得正在解析的script节点
if(doc.currentScript) {
//firefox 4+
returndoc.currentScript;
}
}

七、定义module

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

1.define('a', function(){})

2.define(function(){})

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
functiondefine() {
vararg = Array.prototype.slice.call(arguments);
varparamType = Object.prototype.toString.call(arg[0]).split(' ')[1].replace(/\]/,'');
defineParamObj[paramType].apply(null, arg);
// Chain.excute();
}
function_String(string, func) {
string = _analyseName(string);
//分析依赖
vardepends = _analyseDepend(func) || [];
// 将加载好的模块存入缓存
excuteStack.push(function() {
returnmodules[string] = func();
});
//执行加载依赖函数
_excuteRequire(depends);
for(vari = 0, l = depends.length; i < l; i++) {
(function(i) {
excuteChain.after(function() {
varc = require(depends[i]);
if(c) {
this.next();
};
});
})(i);
}
}
function_Function(func) {
varname = _analyseName(_getCurrentScript().src);
_String(name, func);
}

结束

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

优质内容筛选与推荐>>
1、Ubuntu安装启动SSH服务
2、Centos7安装dnf工具管理rpm包
3、C#自定义分页控件3.0
4、如何阻止浏览器默认事件
5、spring项目读取配置文件


长按二维码向我转账

受苹果公司新规定影响,微信 iOS 版的赞赏功能被关闭,可通过二维码转账支持公众号。

    阅读
    好看
    已推荐到看一看
    你的朋友可以在“发现”-“看一看”看到你认为好看的文章。
    已取消,“好看”想法已同步删除
    已推荐到看一看 和朋友分享想法
    最多200字,当前共 发送

    已发送

    朋友将在看一看看到

    确定
    分享你的想法...
    取消

    分享想法到看一看

    确定
    最多200字,当前共

    发送中

    网络异常,请稍后重试

    微信扫一扫
    关注该公众号





    联系我们

    欢迎来到TinyMind。

    关于TinyMind的内容或商务合作、网站建议,举报不良信息等均可联系我们。

    TinyMind客服邮箱:support@tinymind.net.cn