前言
在学习如何实现一个combo算法之前,先让我们来看看它解决了哪些问题:
用过js模块化的同学应该都知道,模块化给我们带来许多好处,但如果我们在一个页面中依赖许多模块,不做一定的处理,就会发起许多的http请求,影响页面性能。通过combo算法,将请求合并,发送一个http请求,由后台解析所需文件,一次性将我们需要的文件返回。
相信很多前端开发人员都做过这么一件事,将页面多个JavaScript文件合并成一个js文件,目的是减少http请求数,以提高页面性能。但同时带来的问题:维护麻烦,模块冗余,缓存粒度大。使用combo算法看起来更优雅,不需要去合并成一个文件。
Combo算法的实现
首先让我们来看一下,假设我们的页面需要加载下面这么几个文件。1
2
3
4var comboFile = ["file:///E:/src/a/c.js",
"file:///E:/src/a.js",
"file:///E:/src/b.css",
"file:///E:/src/d.css"];
那么最终我们需要合并成两个combo请求,file:///E:/src/??a/c.js,a/e.b.js,b/d.js
和 file:///E:/src/??b.css,d.css
。
不难想到这个合并的过程:
- 解析找到文件相同的目录,与不同的路径。
- 将数组中文件路径解析为对象,相同目录同一标识属性,不同路径赋值不同的路径。
- 解析对象,获取相同的目录,和不同路径数组。
- 根据经过处理的路径数组,分类文件,重新组成一个路径数组
- 遍历分类文件,拼接字符串形成 combo链接
接下来我们一步一步顺着思路解析。
全局变量声明
1 | var comboSyntax = ["??", ","] |
- comboSyntax:combo服务语法数组。存储需要合成combo链接的字符串。
将数组中文件路径解析为路径对象
1 | function uris2meta(uris) { |
- _KEYS用于记录关键字数组,也就是路径值,该对象的属性名称集合。
var parts = uris[i].replace("://","__").split("/");
通过正则先修改://
为__
,然后根据 字符’/‘ 进行split() 的到路径数组。- 一层一层的玩下创建对象属性,如果该路径未存储,就以其“路径值”作为标志 建立一个新的属性,并添加到__KEYS中。
- 下面是我们通过执行
uris2meta(comboFile)
处理数据得到的对象。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{
"_KEYS": [
"file__"
],
"file__": {
"_KEYS": [
"E:"
],
"E:": {
"_KEYS": [
"src"
],
"src": {
"_KEYS": [
"a",
"a.js",
"b.css",
"d.css"
],
"a": {
"_KEYS": [
"c.js"
],
"c.js": {
"_KEYS": []
}
},
"a.js": {
"_KEYS": []
},
"b.css": {
"_KEYS": []
},
"d.css": {
"_KEYS": []
}
}
}
}
}
根据路径对象获取相同目录和不同路径
代码
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// 根据路径对象形成一个 paths 路径数组[相同目录,不同路径]
function meta2paths(meta) {
var paths = [];
var _KEYS = meta._KEYS;
for(var i = 0,len = _KEYS.length; i < len; i++){
var part = _KEYS[i];
var root = part;
var m = meta[part];
var KEYS = m._KEYS;
while(KEYS.length === 1){
root += '/' + KEYS[0];
m = m[KEYS[0]];
KEYS = m._KEYS;
// console.log(root,i)
}
if(KEYS.length){
paths.push([root.replace("__", "://"), meta2arr(m)])
}
}
console.log(paths)
return paths;
}
// 根据后面的不同路径,形成一个数组
function meta2arr(meta) {
var arr = [];
var _KEYS = meta._KEYS;
for (var i = 0, len = _KEYS.length; i < len; i++) {
var key = _KEYS[i];
var r = meta2arr(meta[key])
var m = r.length;
if(m){
for (var j = 0; j < m; j++){
arr.push(key + '/' + r[j]);
}
}else{
arr.push(key);
}
}
return arr;
}当_KEYS数组长度为1,说明所有的请求都通过该目录,所以我们循环并通过
m._KEYS
一层一层的往下叠加,得到相同目录。1
2
3
4
5
6while(KEYS.length === 1){
root += '/' + KEYS[0];
m = m[KEYS[0]];
KEYS = m._KEYS;
// console.log(root,i)
}这里主要用的是递归的思想。
meta2arr(meta[key])
将子目录属性 往下传递调用,直到最后一层。此时它已经没有_KEY属性了,便开始向上返回值。arr.push(key + '/' + r[j]);
不断叠加形成完整的目录。遍历完数组后,所有不同的路径都被添加到数组中,最终函数返回。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17var arr = [];
var _KEYS = meta._KEYS;
for (var i = 0, len = _KEYS.length; i < len; i++) {
var key = _KEYS[i];
var r = meta2arr(meta[key])
var m = r.length;
if(m){
for (var j = 0; j < m; j++){
arr.push(key + '/' + r[j]);
}
}else{
arr.push(key);
}
}
return arr;下面我们看通过执行
meta2paths(uris2meta(comboFile))
得到的数组
1 | [ "file:///E:/src" , ["a/c.js","a.js","b.css","d.css"] ] |
根据路径数组,形成combo链接
我们需要根据文件格式进行一下分类,不然不同文件格式形成的combo链接并不是我们想要的结果。
根据经过处理的路径数组,分类文件,重新组成一个路径数组
1 | // 根据文件格式,给文件分组 |
- 根据文件后缀判断文件格式,
file.lastIndexOf(".")
获得的是文件后缀,得到文件类型。
1 | var p = file.lastIndexOf(".") |
- 遍历路径数组,
(hash[ext] || (hash[ext] = [])).push(file)
将相同文件类型的放在一个数组中。group.push(hash[k])
将所有的类型数组合并。
1 | var hash = {} |
- 我们看一下我们得到的结果
1 | [ ["a/c.js , a.js"],["b.css , d.css"] ] |
遍历分类文件,拼接字符串形成 combo链接
1 | // 通过路径数组形成 combo链接 |
- 通过
var group = files2group(path[1])
得到文件分类形成的二维数组。接下来遍历二维数组,连接字符串形成combo链接。
1 | …… |
- 最终返回的结果:
1 | ["file:///E:/src/??a/c.js,a.js", "file:///E:/src/??b.css,d.css"] |
到此,我们就得到我们想要的combo链接。