不久前看过一篇不错的文章,作者用了15行代码就实现了一个简单的模板引擎,我觉得很有趣,建议在读这篇文章之前先看一下这个,这里是传送门:只有20行的Javascript模板引擎
这个模板引擎实现的核心点是利用正则表达式来匹配到模板语法里面的变量和JS语句,再将这些匹配到的字段push到一个数组中,最后连接起来,用Function来解析字符串,最后将执行后的结果放到指定DOM节点的innerHTML里面。
但是这个模板引擎还是有很多不足,比如不支持取余运算,不支持自定义模板语法,也不支持if、for、switch之外的JS语句,缺少HTML实体编码。
恰好我这阵子也在看underscore源码,于是就参考了一下underscore中template方法的实现。
这个是我参考template后实现的模板,一共只有60行代码。
(function () {
var root = this;
var html2Entity = (function () {
var escapeMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'`': '`'
};
var escaper = function (match) {
return escapeMap[match];
};
return function (string) {
var source = "(" + Object.keys(escapeMap).join("|") + ")";
var regexp = RegExp(source), regexpAll = RegExp(source, "g");
return regexp.test(string) ? string.replace(regexpAll, escaper) : string;
}
}())
var escapes = {
'"': '"',
"'": "'",
"\\": "\\",
'\n': 'n',
'\r': 'r',
'\u2028': 'u2028',
'\u2029': 'u2029'
}
var escaper = /\\|'|"|\r|\n|\u2028|\u2029/g;
var convertEscapes = function (match) {
return "\\" + escapes[match];
}
var template = function (tpl, settings) {
var templateSettings = Object.assign({}, {
interpolate: /<%=([\s\S]+?)%>/g,
escape: /<%-([\s\S]+?)%>/g,
evaluate: /<%([\s\S]+?)%>/g,
}, template.templateSettings);
settings = Object.assign({}, settings);
var matcher = RegExp(Object.keys(templateSettings).map(function (key) {
return templateSettings[key].source
}).join("|") + "|$", "g")
var source = "", index = 0;
tpl.replace(matcher, function (match, interpolate, escape, evaluate, offset) {
source += "__p += '" + tpl.slice(index, offset).replace(escaper, convertEscapes) + "'\n";
index = offset + match.length;
if (evaluate) {
source += evaluate + "\n"
} else if (interpolate) {
source += "__p += (" + interpolate + ") == null ? '' : " + interpolate + ";\n"
} else if (escape) {
source += "__p += (" + escape + ") == null ? '' : " + html2Entity(escape) + ";\n"
}
return match;
})
source = "var __p = '';" + source + 'return __p;'
if (!settings.variable) source = "with(obj||{}) {\n" + source + "\n}"
var render = new Function(settings.variable || "obj", source);
return render
}
root.templateY = template
}.call(this))
复制代码
转义
我们知道,在字符串中有一些特殊字符是需要转义的,比如"'", '"',不然就会和预期展示不一致,甚至是报错,所以我们一般会用反斜杠来表示转义,常见的转义字符有\n, \t, \r等等。
但是这里的convertEscapes里面我们为什么要多加一个反斜杠呢?
这是因为在执行new Function里面的语句时,也需要对字符进行一次转义,可以看一下下面这行代码:
var log = new Function("var a = '1\n23';console.log(a)");
log() // Uncaught SyntaxError: Invalid or unexpected token
复制代码
这是因为Function函数在执行的时候,里面的内容被解析成了这样。
var a = '1
23';console.log(a)
复制代码
在JS里面是不允许字符串换行出现的,只能使用转义字符\n。
正则表达式
underscore中摒弃了用正则表达式匹配for/if/switch/{/}等语句的做法,而是使用了不同的模板语法(<%=%>和<%%>)来区分当前是变量还是JS语句,这样虽然需要用户自己区分语法,但是给开发者减少了很多不必要的麻烦,因为如果用正则来匹配,那么后面就无法使用类似{# #}和{ {}}的语法了。 这里正则表达式的重点是+?,+?是惰性匹配,表示以最少的次数匹配到[\s\S],所以我们/<%=([\s\S]+?)%>/g是不会匹配到类似<%=name<%=age%>%>这种语法的,只会匹配到<%=name%>语法。
replace
这里我们用到了replace第二个参数是函数的情况。
var pattern = /([a-z]+)\s([a-z]+)/;
var str = "hello world";
str.replace(pattern, function(match, p1, p2, offset) {
// p1 is "hello"
// p2 is "world"
return match;
})
复制代码
在JS正则表达式中,使用()包起来的叫着捕获性分组,而使用(?:)的叫着非捕获性分组,在replace的第二个参数是函数时,每次匹配都会执行一次这个函数,这个函数第一个参数是pattern匹配到的字符串,在这个里面是"hello world"。
p1是第一个分组([a-z]+)匹配到的字符串,p2是第二个分组([a-z]+)匹配到的字符串,如果有更多的分组,那还会有更多参数p3, p4, p5等等,offset是最后一个参数,指的是在第几个索引处匹配到了,这里的offset是0,因为是从一开始就刚好匹配到了hello world。
字符串拼接
underscore中使用+=字符串拼接的方式代替了数组push的方式,这样是因为+=相比push的性能会更高。
setting.variable
underscore这里使用with来改变了作用域,但是with会导致性能比较差,关于with的弊端可以参考一下这篇文章: Javascript中的with关键字
你还可以在variable设置里指定一个变量名,这样能显著提升模板的渲染速度。不过语法也和之前有一些不同,模板里面必须要用你指定的变量名来访问,而不能直接用answer这种形式,这种形式下没有使用with实现,所以性能会高很多。
_.template("Using 'with': <%= data.answer %>", {variable: 'data'})({answer: 'no'});
复制代码
参考链接:
- js正则进阶
- JavaScript函数replace揭秘
- JavaScript正则表达式分组模式:捕获性分组与非捕获性分组及前瞻后顾
- underscore 系列之字符实体与 _.escape
- Javascript中的with关键字