神的尾巴

全栈工程师、独立开发者

0%

源码解析之AlloyTeam-sodajs

目前项目刚好有用到,简单看下源码

遵循 UMD 规范,在 Node 和浏览器端均可使用

判断是 Node 还是浏览器端

如果存在 require 且 window 未定义,引入 nodeWindow 模块,设置变量 document 和是否是浏览器的变量 isBrowser。

1
2
3
4
5
6
7
8
9
10
11
12
if (typeof require === "function" && typeof window === "undefined") {
var NodeWindow = require("nodewindow");
var nodeWindow = new NodeWindow();

var win = nodeWindow.runHTML("", {}, {});

document = win.document;
isBrowser = false;
} else {
document = window.document;
isBrowser = true;
}

导出模块

如果设置 module 和 exports 为 CMD 规范,使用 module.exports 导出模块,如果有 define 和 define.amd 为 AMD 规范,使用 define return 方式导出模块。只有 exports 为 ES6 的模块化,使用 exports 导出,否则为浏览器模式,绑定到全局对象 window 上。

1
2
3
4
5
6
7
8
if (typeof exports === "object" && typeof module === "object")
module.exports = sodaRender;
else if (typeof define === "function" && define.amd)
define([], function () {
return sodaRender;
});
else if (typeof exports === "object") exports["soda"] = sodaRender;
else window.soda = sodaRender;

一些填补和工具方法

包含数组的 map、forEach,字符串的 trim

填补

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
if (!Array.prototype.map) {
Array.prototype.map = function (func) {
var arr = [];
for (var i = 0; i < this.length; i++) {
var item = this[i];
[].push(func && func.call(item, item, i));
}

return arr;
};
}
if (!Array.prototype.forEach) {
Array.prototype.forEach = function (callback) {
var T, k;
if (this == null) {
throw new TypeError("this is null or not defined");
}
var O = Object(this);
var len = O.length >>> 0;
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
if (arguments.length > 1) {
T = arguments[1];
}
k = 0;
while (k < len) {
var kValue;
if (k in O) {
kValue = O[k];
callback.call(T, kValue, k, O);
}
k++;
}
};
}

if (!String.prototype.trim) {
String.prototype.trim = function () {
return this.replace(/^\s*|\s*$/g, "");
};
}

工具方法

这里的 removeClass 有点问题,className 如果出现在中间,直接进行 replace 会导致 className 错误。

例如: `className = ‘first test second’, removeClass test 后的结果会是’firstsecond’

从入口开始分析

可能有些遗漏的地方,后面发现了再补上。

从模块导出和使用方式,可以得出,入口方法为 sodaRender。

简单看下入口方法的执行流程:

  1. 先对 Directive(按照 angular 的叫法为指令)进行按照 priority 排序,不过这边默认值设置的为 0,很多看的是以 10 为基准,感觉默认值设置为 10 好点,否则,对于那些没有设置 priority 的属性,优先级始终最高,另外看 if repeat 指令都是按照 10 位基准的。

    1
    2
    3
    sodaDirectiveArr.sort(function (b, a) {
    return Number(a.opt.priority || 0) - Number(b.opt.priority || 0);
    });
  2. 如果 ie9 以下浏览器不支持自定义标签,所以需要添加到 body 里面去,不过这边值判断了 documentMode,并没有判断是否是 ie 浏览器,在 chrome 上测试,发现并没有这个属性。

    1
    2
    3
    4
    5
    6
    7
    8
    var div = document.createElement("div");

    // 必须加入到body中去,不然自定义标签不生效
    if (document.documentMode < 9) {
    div.style.display = "none";
    document.body.appendChild(div);
    }
    div.innerHTML = str;
  3. 调用 compileNode 方法,遍历解析模板的子元素,返回解析完成的 innerHTML,nodes2Arr 是为了把 node 节点转换为标砖 array,方便调用 map 方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    nodes2Arr(div.childNodes).map(function (child) {
    compileNode(child, data);
    });

    var innerHTML = div.innerHTML;
    if (document.documentMode < 9) {
    document.body.removeChild(div);
    }

    return innerHTML;
  4. 继续分析下 compileNode 方法,其中 parseSodeExpression 是用来解析 soda expression 的,后面再来分析。

分析 compileNode 方法

  1. 如果是文本节点,使用 valueoutReg 匹配{{soda expression}},并使用 parseSodaExpression 解析 soda expression。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var valueoutReg = /\{\{([^\}]*)\}\}/g; // 其他地方定义的,放到这里方便查看

    if (node.nodeType === 3) {
    node.nodeValue = node.nodeValue.replace(valueoutReg, function (item, $1) {
    var value = parseSodaExpression($1, scope);
    if (typeof value === "object") {
    value = JSON.stringify(value, null, 2);
    }
    return value;
    });
    }
  2. 遍历处理节点的属性,如果是指令,执行指令相关的逻辑(调用指令的 link 方法),具体的一些指令后面再分析。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /*
    exp:
    <ul soda-if="xxx"></ul>
    */

    sodaDirectiveArr.map(function (item) {
    var name = item.name;

    var opt = item.opt;

    if (node.getAttribute(name) && node.parentNode) {
    opt.link(scope, node, node.attributes);
    }
    });
  3. 处理指令外其他属性的解析,如果也是设置前缀开头的,解析后设置回去,如果其他元素,直接调用 parseSodaExpression 解析。

    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
    /*
    exp:
    <ul data-info={{xxx}}></ul>
    */

    // 如果dirctiveMap有的就跳过不再处理
    if (!sodaDirectiveMap[attr.name]) {
    if (prefixReg.test(attr.name)) {
    var attrName = attr.name.replace(prefixReg, "");

    if (attrName) {
    if (attr.value) {
    var attrValue = attr.value.replace(valueoutReg, function (item, $1) {
    return parseSodaExpression($1, scope);
    });

    node.setAttribute(attrName, attrValue);
    }
    }

    // 对其他属性里含expr 处理
    } else {
    if (attr.value) {
    attr.value = attr.value.replace(valueoutReg, function (item, $1) {
    return parseSodaExpression($1, scope);
    });
    }
    }
    }
  4. 递归子节点进行解析。

    1
    2
    3
    nodes2Arr(node.childNodes).map(function (child) {
    compileNode(child, scope);
    });

sodaExpression 分析

  1. 首先提取出 filter,因为或运算符’||’包含了 fiter 的’|’,所以这里把或运算符先转换一下,后面再转换回来。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 一些定义放到这里方便查看
    var OR_REPLACE = "OR_OPERATOR\x1E";

    str = str.replace(OR_REG, OR_REPLACE).split("|");

    for (var i = 0; i < str.length; i++) {
    str[i] = (str[i].replace(new RegExp(OR_REPLACE, "g"), "||") || "").trim();
    }

    var expr = str[0] || "";
    var filters = str.slice(1);
  2. 处理字符串常量,存储到 scope 中,STRING_REG 正则匹配格式为”xxx”或者’xxx’的字符串常量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 一些定义放到这里方便查看
    var STRING_REG = /"([^"]*)"|'([^']*)'/g;

    var getRandom = function () {
    return "$$" + ~~(Math.random() * 1e6);
    };

    // 将字符常量保存下来
    expr = expr.replace(STRING_REG, function (r, $1, $2) {
    var key = getRandom();
    scope[key] = $1 || $2;
    return key;
    });
  3. 处理属性,使用正则 ATTR_REG,匹配格式为[xxx]的属性进行解析,带 NG 的为不设置全局模式的正则,用来解析是否处理完成。例如:my[name] => my.name(解析后)。因为设置了全局检索,所以这里设置了 lastIndex = 0。这里使用while(ATTR_REG_NG.test)是为了进行属性的嵌套解析。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 一些定义放到这里方便查看
    var ATTR_REG = /\[([^\[\]]*)\]/g;
    var ATTR_REG_NG = /\[([^\[\]]*)\]/;

    var getAttrVarKey = function () {
    return CONST_PRIFIX + ~~(Math.random() * 1e6);
    };

    while (ATTR_REG_NG.test(expr)) {
    ATTR_REG.lastIndex = 0;

    //对expr预处理
    expr = expr.replace(ATTR_REG, function (r, $1) {
    var key = getAttrVarKey();
    // 属性名称为字符常量
    var attrName = parseSodaExpression($1, scope);

    // 给一个特殊的前缀 表示是属性变量

    scope[key] = attrName;

    return "." + key;
    });
    }
  4. 对象处理,使用 OBJECT_REG 进行匹配,格式为:标准标识符(首字字母、下划线或$开头,其他字符为字母、下划线或数字)+ ([空字符] . [空字符] 标识符| 数字)(可选) 如果是对象转换为字符串 getValue(scope, object),这个后面有 evalFunc 来调用解析。

    1
    2
    3
    4
    5
    6
    7
    // 一些定义放到这里方便查看
    var OBJECT_REG = /[a-zA-Z_\$]+[\w\$]*(?:\s*\.\s*(?:[a-zA-Z_\$]+[\w\$]*|\d+))*/g;
    var OBJECT_REG_NG = /[a-zA-Z_\$]+[\w\$]*(?:\s*\.\s*(?:[a-zA-Z_\$]+[\w\$]*|\d+))*/;

    expr = expr.replace(OBJECT_REG, function (value) {
    return "getValue(scope,'" + value.trim() + "')";
    });
  5. 对 filters,递归处理,参数格式为 arg0:arg1:arg2,如果参数中有对象格式处理为 getValue 的方式,最后转化为字符串sodaFilterMap[name](args)。在后面的 evalFunc 解析。

    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
    var parseFilter = function () {
    var filterExpr = filters.shift();

    if (!filterExpr) {
    return;
    }

    var filterExpr = filterExpr.split(":");
    var args = filterExpr.slice(1) || [];
    var name = filterExpr[0] || "";

    var stringReg = /^'.*'$|^".*"$/;
    for (var i = 0; i < args.length; i++) {
    //这里根据类型进行判断
    if (OBJECT_REG_NG.test(args[i])) {
    args[i] = "getValue(scope,'" + args[i] + "')";
    } else {
    }
    }

    if (sodaFilterMap[name]) {
    args.unshift(expr);

    args = args.join(",");

    expr = "sodaFilterMap['" + name + "'](" + args + ")";
    }

    parseFilter();
    };

    parseFilter();
  6. 解析 getValue 和 sodaFilterMap,使用 new Function 动态创建 func。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    var evalFunc = new Function(
    "getValue",
    "sodaFilterMap",
    "return function sodaExp(scope){ return " + expr + "}"
    )(getValue, sodaFilterMap);

    return evalFunc(scope);

    //其实上面等同于
    var evalFUnc = (function (getValue, sodaFilterMap) {
    return function sodaExp(scope) {
    return "getValue(scope, name)"; // 例子
    };
    })(getValue, sodaFilterMap);

    // 最后调用返回生成的sodaExp function
    return sodaExp(scope);

分析 getValue

  1. 对于常量类型,直接从 scope 中获取,否则返回 key,如果是 true/false,转换为 bool 类型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 定义放到这里方便查看
    var CONST_REGG = /_\$C\$_[^\.]+/g; // 和之前属性替换的前缀一致

    CONST_REGG.lastIndex = 0;
    var realAttrStr = _attrStr.replace(CONST_REGG, function (r) {
    if (typeof _data[r] === "undefined") {
    return r;
    } else {
    return _data[r];
    }
    });

    if (_attrStr === "true") {
    return true;
    }

    if (_attrStr === "false") {
    return false;
    }
  2. 对于其他类型,调用_getValue 方法如果是对象类型[xxx.xxx]递归解析,否则直接获取值。

    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
    var _getValue = function (data, attrStr) {
    var dotIndex = attrStr.indexOf(".");

    if (dotIndex > -1) {
    var attr = attrStr.substr(0, dotIndex);
    attrStr = attrStr.substr(dotIndex + 1);

    // 检查attrStr是否属于变量并转换
    if (typeof _data[attr] !== "undefined" && CONST_REG.test(attr)) {
    attr = _data[attr];
    }

    if (typeof data[attr] !== "undefined") {
    return _getValue(data[attr], attrStr);
    } else {
    var eventData = {
    name: realAttrStr,
    data: _data
    };

    triggerEvent("nullvalue", {
    type: "nullattr",
    data: eventData
    }, eventData);

    // 如果还有
    return "";
    }
    } else {

    // 检查attrStr是否属于变量并转换
    if (typeof _data[attrStr] !== "undefined" && CONST_REG.test(attrStr)) {
    attrStr = _data[attrStr];
    }

    var rValue;
    if (typeof data[attrStr] !== "undefined") {
    rValue = data[attrStr];
    } else {
    var eventData = {
    name: realAttrStr,
    data: _data
    };

    triggerEvent("nullvalue", {
    type: "nullvalue",
    data: eventData
    }, eventData);

    rValue = '';
    }

    return rValue;
    }

Filter

内置的 filter 有 date,提供了扩展 filter 的方法,如下:

1
2
3
4
5
sodaRender.filter = sodaFilter;

var sodaFilter = function (name, func) {
sodaFilterMap[name] = func;
};

Directive

指令内置有 repeat、if、class、src、bind-html、html、replace、include、style…,不过没提供扩展的方法,内部需要调用 parseSodaExperssion,估计处理起来比较麻烦,后面可以进一步封装后,提供外部扩展的方法,具体的分析,下次再来研究。

觉得对你有帮助的话,请我喝杯咖啡吧~.