目前项目刚好有用到,简单看下源码
遵循 UMD 规范,在 Node 和浏览器端均可使用
判断是 Node 还是浏览器端
如果存在 require 且 window 未定义,引入 nodeWindow 模块,设置变量 document 和是否是浏览器的变量 isBrowser。
1 | if (typeof require === "function" && typeof window === "undefined") { |
导出模块
如果设置 module 和 exports 为 CMD 规范,使用 module.exports 导出模块,如果有 define 和 define.amd 为 AMD 规范,使用 define return 方式导出模块。只有 exports 为 ES6 的模块化,使用 exports 导出,否则为浏览器模式,绑定到全局对象 window 上。
1 | if (typeof exports === "object" && typeof module === "object") |
一些填补和工具方法
包含数组的 map、forEach,字符串的 trim
填补
1 | if (!Array.prototype.map) { |
工具方法
这里的 removeClass 有点问题,className 如果出现在中间,直接进行 replace 会导致 className 错误。
例如: `className = ‘first test second’, removeClass test 后的结果会是’firstsecond’
从入口开始分析
可能有些遗漏的地方,后面发现了再补上。
从模块导出和使用方式,可以得出,入口方法为 sodaRender。
简单看下入口方法的执行流程:
先对 Directive(按照 angular 的叫法为指令)进行按照 priority 排序,不过这边默认值设置的为 0,很多看的是以 10 为基准,感觉默认值设置为 10 好点,否则,对于那些没有设置 priority 的属性,优先级始终最高,另外看 if repeat 指令都是按照 10 位基准的。
1
2
3sodaDirectiveArr.sort(function (b, a) {
return Number(a.opt.priority || 0) - Number(b.opt.priority || 0);
});如果 ie9 以下浏览器不支持自定义标签,所以需要添加到 body 里面去,不过这边值判断了 documentMode,并没有判断是否是 ie 浏览器,在 chrome 上测试,发现并没有这个属性。
1
2
3
4
5
6
7
8var div = document.createElement("div");
// 必须加入到body中去,不然自定义标签不生效
if (document.documentMode < 9) {
div.style.display = "none";
document.body.appendChild(div);
}
div.innerHTML = str;调用 compileNode 方法,遍历解析模板的子元素,返回解析完成的 innerHTML,nodes2Arr 是为了把 node 节点转换为标砖 array,方便调用 map 方法。
1
2
3
4
5
6
7
8
9
10nodes2Arr(div.childNodes).map(function (child) {
compileNode(child, data);
});
var innerHTML = div.innerHTML;
if (document.documentMode < 9) {
document.body.removeChild(div);
}
return innerHTML;继续分析下 compileNode 方法,其中 parseSodeExpression 是用来解析 soda expression 的,后面再来分析。
分析 compileNode 方法
如果是文本节点,使用 valueoutReg 匹配
{{soda expression}}
,并使用 parseSodaExpression 解析 soda expression。1
2
3
4
5
6
7
8
9
10
11var 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;
});
}遍历处理节点的属性,如果是指令,执行指令相关的逻辑(调用指令的 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);
}
});处理指令外其他属性的解析,如果也是设置前缀开头的,解析后设置回去,如果其他元素,直接调用 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);
});
}
}
}递归子节点进行解析。
1
2
3nodes2Arr(node.childNodes).map(function (child) {
compileNode(child, scope);
});
sodaExpression 分析
首先提取出 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);处理字符串常量,存储到 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;
});处理属性,使用正则 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;
});
}对象处理,使用 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() + "')";
});对 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
32var 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();解析 getValue 和 sodaFilterMap,使用 new Function 动态创建 func。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17var 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
对于常量类型,直接从 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;
}对于其他类型,调用_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
54var _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 | sodaRender.filter = sodaFilter; |
Directive
指令内置有 repeat、if、class、src、bind-html、html、replace、include、style…,不过没提供扩展的方法,内部需要调用 parseSodaExperssion,估计处理起来比较麻烦,后面可以进一步封装后,提供外部扩展的方法,具体的分析,下次再来研究。