js实现一个combo算法

前言

在学习如何实现一个combo算法之前,先让我们来看看它解决了哪些问题:

  • 用过js模块化的同学应该都知道,模块化给我们带来许多好处,但如果我们在一个页面中依赖许多模块,不做一定的处理,就会发起许多的http请求,影响页面性能。通过combo算法,将请求合并,发送一个http请求,由后台解析所需文件,一次性将我们需要的文件返回。

  • 相信很多前端开发人员都做过这么一件事,将页面多个JavaScript文件合并成一个js文件,目的是减少http请求数,以提高页面性能。但同时带来的问题:维护麻烦,模块冗余,缓存粒度大。使用combo算法看起来更优雅,不需要去合并成一个文件。

Combo算法的实现

首先让我们来看一下,假设我们的页面需要加载下面这么几个文件。

1
2
3
4
var 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.jsfile:///E:/src/??b.css,d.css

不难想到这个合并的过程:

  • 解析找到文件相同的目录,与不同的路径。
    • 将数组中文件路径解析为对象,相同目录同一标识属性,不同路径赋值不同的路径。
    • 解析对象,获取相同的目录,和不同路径数组。
  • 根据经过处理的路径数组,分类文件,重新组成一个路径数组
  • 遍历分类文件,拼接字符串形成 combo链接
    接下来我们一步一步顺着思路解析。

全局变量声明

1
var comboSyntax = ["??", ","]
  • comboSyntax:combo服务语法数组。存储需要合成combo链接的字符串。

将数组中文件路径解析为路径对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function uris2meta(uris) {
var meta = {
_KEYS : []
};

for(var i = 0,len = uris.length ; i < len ; i++){
var parts = uris[i].replace("://","__").split("/");
var m = meta;
for(var j = 0,len = parts.length ; j < len ; j++){
var part = parts[j];
if (!m[part]) {
m[part] = {
_KEYS : []
};
m._KEYS.push(part);
}
m = m[part];
}
}

// console.log(JSON.stringify(meta))
return meta;
}
  • _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
    6
    while(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
    17
    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;
  • 下面我们看通过执行meta2paths(uris2meta(comboFile))得到的数组

1
[ "file:///E:/src" , ["a/c.js","a.js","b.css","d.css"] ]

根据路径数组,形成combo链接

我们需要根据文件格式进行一下分类,不然不同文件格式形成的combo链接并不是我们想要的结果。

根据经过处理的路径数组,分类文件,重新组成一个路径数组

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
// 根据文件格式,给文件分组
function files2group(files) {
var group = []
var hash = {}

for (var i = 0, len = files.length; i < len; i++) {
var file = files[i]
var ext = getExt(file)
// console.log(ext)
if (ext) {
(hash[ext] || (hash[ext] = [])).push(file)
}
}

for (var k in hash) {
if (hash.hasOwnProperty(k)) {
group.push(hash[k])
}
}

return group
}
// 根据文件判断 文件格式
function getExt(file) {
var p = file.lastIndexOf(".")
return p >= 0 ? file.substring(p) : ""
}
  • 根据文件后缀判断文件格式,file.lastIndexOf(".")获得的是文件后缀,得到文件类型。
1
2
var p = file.lastIndexOf(".")
return p >= 0 ? file.substring(p) : ""
  • 遍历路径数组,(hash[ext] || (hash[ext] = [])).push(file)将相同文件类型的放在一个数组中。group.push(hash[k])将所有的类型数组合并。
1
2
3
4
5
6
var hash = {}
……
(hash[ext] || (hash[ext] = [])).push(file)
……
group.push(hash[k])
……
  • 我们看一下我们得到的结果
1
[ ["a/c.js , a.js"],["b.css , d.css"] ]

遍历分类文件,拼接字符串形成 combo链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 通过路径数组形成 combo链接
function paths2hash(paths) {
var comboHash = [];
for (var i = 0, len = paths.length; i < len; i++) {
var path = paths[i]
var root = path[0] + "/"
var group = files2group(path[1])

for (var j = 0, m = group.length; j < m; j++) {
// 遍历某类文件,连接字符
var copy = []
for (var i = 0, len = group[j].length; i < len; i++) {
copy[i] = group[j][i].replace(/\?.*$/, '')
}
comboHash[j] = root + comboSyntax[0] + copy.join(comboSyntax[1]);
}
}
// console.log(comboHash);
return comboHash;
}
  • 通过var group = files2group(path[1])得到文件分类形成的二维数组。接下来遍历二维数组,连接字符串形成combo链接。
1
2
……
comboHash[j] = root + comboSyntax[0] + copy.join(comboSyntax[1]);
  • 最终返回的结果:
1
["file:///E:/src/??a/c.js,a.js", "file:///E:/src/??b.css,d.css"]

到此,我们就得到我们想要的combo链接。

资源:

源码: https://github.com/wxx2258/front-end-knowledge/blob/master/oldBlogjs%E8%AF%AD%E8%A8%80%E5%9F%BA%E7%A1%80/javascript%E6%A8%A1%E5%9D%97%E5%8C%96/myCombo.js