在看《深入浅出NodeJS》的时候发现对模板引擎有比较详细的讲解,看下来还是比较清晰的。于是决定记录一下_(:з」∠)_

从最简单开始

var str = 'test: {{obj}} and {{obj}}';
render(str, 123);
// => "test: 123 and 123"
function render(str, obj) {
var compiled = compile(str);
return compiled(obj);
}
function compile(str) {
var reg = /\{\{([\w\W]+?)\}\}/g,
tpl = str.replace(reg, function() {
return '\' + obj + \'';
});
// => "test: ' + obj + ' and ' + obj + '"
var fn = 'var ret = \'' + tpl + '\';return ret;';
return new Function('obj', fn);
}

代码思路比较清晰,compile() 函数将字符串中的变量占位符通过正则表达式替换为字符串拼接的形式,如'hehe' + obj + 'hehe',然后通过new Function生成函数体;而 render() 函数将调用 compile() 返回的函数并传入 obj 参数,返回最终字符串

变得厉害一点

上面的函数仅仅允许每次传入一个或多个基本类型进行替换。现在做简单的调整,通过传入对象实现

var str = 'test: {{obj.cat}} and {{obj.dog}}';
render(str, {cat: 'tom', dog: 'doge'});
// => "test: tom and doge"
function render(str, obj) {
var compiled = compile(str);
return compiled(obj);
}
function compile(str) {
var reg = /\{\{([\w\W]+?)\}\}/g,
tpl = str.replace(reg, function(match, data) {
return '\' + ' + data + ' + \'';
});
// => "test: ' + obj.cat + ' and ' + obj.dog + '"
var fn = 'var ret = \'' + tpl + '\';return ret;';
return new Function('obj', fn);
}

使用 with 调整

with 允许修改当前作用域,尽管在语言精粹一书中被点名批评,但在这里能够发挥作用

var str = 'test: {{cat}} and {{dog}}';
render(str, {cat: 'tom', dog: 'doge'});
// => "test: tom and doge"
function render(str, obj) {
var compiled = compile(str);
return compiled(obj);
}
function compile(str) {
var reg = /\{\{([\w\W]+?)\}\}/g,
tpl = str.replace(reg, function(match, data) {
return '\' + ' + data + ' + \'';
});
tpl = 'ret = \'' + tpl + '\';';
// => "ret = 'test: ' + cat + ' and ' + dog + ''"
var fn = 'var ret = \'\'; with(obj) {' + tpl + '} return ret;';
return new Function('obj', fn);
}

现在,模板字符串中不再需要带上obj.

添加模板逻辑

现在尝试为模板引擎添加业务逻辑。首先要区分逻辑代码和输出数据的代码,原因在于逻辑代码不会被包含在最终输出的字符串中。为此,使用 # 标记逻辑代码

var str = 'test: {{cat}} and {{# for(var i=0;i<3;i++) { }} {{dog}}123 {{# } }}';
render(str, {cat: 'tom', dog: 'doge'});
// => "test: tom and doge123 doge123 doge123"
function render(str, obj) {
var compiled = compile(str);
return compiled(obj);
}
function compile(str) {
var reg = /\{\{(?!#)([\w\W]+?)\}\}/g,
logic = /\{\{#([\w\W]+?)\}\}/g,
tpl = str.replace(reg, function(match, data) {
// 输出数据
return '\' + ' + data + ' + \'';
}).replace(logic, function(match, data) {
// 可执行代码
return '\';' + data + ' ret+=\'';
});
tpl = 'ret = \'' + tpl + '\';';
var fn = 'var ret = \'\'; with(obj) {' + tpl + '} return ret;';
return new Function('obj', fn);
}

代码挤在一行不好看,拆分看一下

// 逻辑部分
{{# for(var i=0; i<3; i++) { }}
{{doge}}123
{{# } }}
// 希望编译成这样的代码
var ret = '';
with(obj) {
...
for(var i=0; i<3; i++) {
ret += dog + 123;
}
}

根据希望编译出来的代码去处理字符串,这样看起来就比较清楚了

考虑模板安全

避免 XSS 攻击,为模板提供转义函数

var escape = function(html) {
return String(html)
.replace(/&(?!\w+;)/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;'); // IE 下不支持 &apos; 转义
}

在 chrome 下做了简单测试,将 <> 转义能有效防止插入标签,避免了 onerror 之类的脚本注入;转义 “‘ 避免注入 input 等标签;替换 &#xxx; 避免了其他编码的字符插入。更具体的内容之后会继续尝试

继续进行完善

一般会将模板写在 script 标签或者 textarea 内,因此存在换行符需要提前处理。根据书上的例子调整得到以下代码

var escape = function(html) {
return String(html)
.replace(/&(?!\w+;)/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;'); // IE 下不支持 &apos; 转义
}
var compile = function(str) {
var tpl = str.replace(/\n/g, '\\n')
.replace(/\{\{(?!#)([\w\W]+?)\}\}/g, function(match, data) {
// 转义
return '\' + escape(' + data + ') + \'';
}).replace(/\{\{#([\w\W]+?)\}\}/g, function(match, data) {
// 可执行代码
return '\';\n' + data + '\n tpl+=\'';
}).replace(/\'\n/g, '\'')
.replace(/\n\'/gm, '\'');
tpl = 'tpl = \'' + tpl + '\';';
// 转换空行
tpl = tpl.replace(/''/g, '\'\\n\'');
tpl = 'var tpl = \'\';\n with (obj || {}) {\n' + tpl + '\n}\n return tpl;';
return new Function('obj', 'escape', tpl);
}
var render = function(str, obj) {
var compiled = compile(str);
return compiled(obj, escape);
}
var str = 'test: {{cat}} and {{# for(var i=0;i<3;i++) { }} {{dog}}123 {{# } }}';
render(str, {cat: 'tom', dog: 'doge'});
// => "test: tom and doge123 doge123 doge123"

除此之外,可扩展外部文件模板加载、模板嵌套等功能,这里就不继续写了~