很久之前看过几篇模板引擎相关文章,当时没什么感觉。最近参与了一些面试,再看一篇模板引擎文章时,突然觉得其中涉及的很多知识点,是很好的笔试/面试话题,对于基础的考察还是不错的。
整理一篇文章记录下,本文内容参考逐步实现简单的 JS 模板引擎
简单版模板方法
最基础的模板引擎,是定义一个正则表达式,在模板文本中,按照正则表达式匹配的格式,定义占位变量。最后解析正则表达式,找出匹配的变量名,替换成变量的实际值。
模板引擎中,包含三个组成部分
- template,模板内容
- pattern,正则表达式
- data,数据对象
假如用 <% variable %> 来表示变量,则可用正则 /<%\s(\w?)\s*%>/ 来匹配变量。
var name = 'Han Meimei';
var gender = '男';
var tpl = '<% name %>,欢迎来到这里,祝你早日找到<% gender %>盆友!';
var html = tpl.replace(/<%\s*(\w*?)\s*%>/g, function (match, variable) {
if (variable === 'name') {
return name;
}
else if (variable === 'gender') {
return gender;
}
});
//html 的值就为 Han Meimei,欢迎来到这里,祝你早日找到男盆友!。
上述代码实现了一个文本替换。首选定义了 /<%\s(\w?)\s*%>/g 正则表达式来定义变量模式,然后在模板文本中,使用<% name %>和<% gender %>定义了两个变量,在代码的初始部分,分别为两个变量赋值。
上述代码还不能称之为模板引擎,对引擎来讲,数据对象和模板内容应当是通用的,匹配模式是固定的,将上述代码进行封装
function render(tpl, data) {
return tpl.replace(/<%\s*(\w*?)\s*%>/g, function (match, variable) {
if (data.hasOwnProperty(variable)) {
return data[variable];
}
});
}
var tpl = '<% name %>,欢迎来到这里,祝你早日找到<% gender %>盆友!';
var data = {
name: 'Han Meimei',
gender: '男'
};
render(tpl, data);
// output: Han Meimei,欢迎来到这里,祝你早日找到男盆友!
var tpl = '姓名:<% name %>,年龄:<% age %>,电话:<% phone %>';
var data = {
name: 'Li Lei',
age: 28,
phone: '123456789'
};
render(tpl, data);
// output: 姓名:Li Lei,年龄:28,电话:123456789
对基础知识的考察
上述代码已经实现了一个最简单的模板引擎。在这一过程中,由浅入深,有几个笔试/面试过程中值得考察的点了。
- 了解模板引擎基本组成,可以写出一个最简单的正则表达式来匹配模板内容中的占位变量。这一条是最基本要求,提到模板引擎如果一脸茫然不知如何分解问题,说明解决问题能力太差或者经验实在是太少。匹配一个固定占位变量模式的正则表达式还是很简单的,如果完全无法写出,说明完全不掌握正则表达式。如果做不到这一步,可以考虑结束笔试/面试了。
- 掌握string.replace方法的使用,了解string.replace(regPattern,func)的执行过程。string.replace方法是一个常用的方法,如果不了解这一方法发使用,对一个有经验的开发者来说是不应该的。不过我个人认为,这里如在面试中,可以给一些提示,不至于因为这里一票否决。可以完成前述两步,达到初级水平。
- 有一定的代码组织能力,可以将上述过程封装为一个方法,完成一个最基本的模板引擎方法。这一步需要一定的代码经验,可以完成这一步,稍高于初级水平了。
进一步完善模板引擎
上述 render 函数可以实现变量替换,但当模板中带有逻辑语句便不再适用。字符串的 replace 方法只能替换模板字符串中的变量标识符为实际的变量值,但没法处理控制逻辑。
现在问题演化为,当模板内容中带有逻辑语句时,如何实现模板引擎?
var tpl = 'Hi <%= name %>,你好!<% if (date >= 1 && date < 6) { %>今天是工作日'
+ ',好沮丧啊!<% } else { %>今天是周末,好开心啊!<% } %> 再见!';
var data = {
name: 'Li Lei',
date: '3'
};
function render(tpl, data) {
???
}
继续分析,相比之前的模板内容,现在的模板包含了固定文本、代表变量的标识文本以及代表处理逻辑的标识文本。
目标是根据输入数据,将模板内容转换为‘Hi Li Lei,你好!今天是工作日,好沮丧啊! 再见!’
在这一过程中,对三种类型文本有如下处理逻辑
- 当遇到固定文本时,直接输出;
- 当遇到变量标识时,替换成具体变量值输出;
- 当遇到代码逻辑标识时,执行代码逻辑
已知可以通过文本替换方式,将代表变量的标识进行替换。现在问题变为,如何执行处理逻辑的文本?
答案是通过new Function()方式动态创建函数对象
过程如下
- 定义正则表达式 regExp = /<%(=?)\s(.?)\s*%>/g; 变量为<%= variable %>,逻辑语句为<% code %>
- 利用数组,将解析的文本暂存起来。在匹配过程中,每一个片段作为一个语句,保存为一个数组元素。固定文本,直接输出;变量标识,替换为具体变量后输出;代码逻辑标识,保存至数组待执行。
- 将数组合并为一个字符串,使用new Function(codeStr)生成函数对象并执行
同时,在这一过程中,有几个优化的点
- 将模板的编译和模板语句的执行分离开,使得编译模板后生成的函数对象可缓存,而不需要每次执行时重新编译模板
- 变量替换时,支持对象的属性和数组这类复杂对象。即支持obj.attr和array[index]这类变量替换
- 对 "、\r、\n 等一些特殊字符进行转义
最终实现一个较为完善的版本
var regExp = /<%(=?)\s*(.*?)\s*%>/g;
/**
* Template 构造函数
*
* @constructor
* @param {string} template 模板文本
*/
function Template(template) {
if (!(this instanceof Template)) {
return new Template(template);
}
this.template = template || '';
}
/**
* 编译模板
*
* @return {Function} 编译后的渲染函数
*/
Template.prototype.compile = function () {
var match;
var cursor = 0;
var codes = [];
// 因为构造函数体内容拼字符串是使用`"`,所以模板中输出的双引号需要转义
// 同样,模板中的`\n、\r`也需要转义,因为正常代码是需要`;`作为一行的,但`\n、\r`会使代码在一行中折断
var tpl = this.template
.replace(/\"/g, '\\\"')
.replace(/\n/g, '\\\n')
.replace(/\r/g, '\\\r');
codes.push(getValue.toString() + ';');
codes.push('var r = "";');
// 默认情况下,模板中访问数据`data`的属性时,需要`data.xxx`。为方便,使用`with`语法可以使变量访问的
// 作用域限制在`data`下,所以直接使用`xxx`就可以访问(当然,平常开发中不推荐使用`with`)
codes.push('with (data || {}) {');
while (match = regExp.exec(tpl)) {
// 固定文本
codes.push('r += "' + tpl.slice(cursor, match.index) + '";');
// 变量
if (match[1]) {
codes.push('r += getValue("' + match[2] + '");');
}
// 代码逻辑
else {
codes.push(match[2]);
}
cursor = match.index + match[0].length;
}
codes.push('r += "' + tpl.slice(cursor) + '";');
codes.push('}');
codes.push('return r;');
return new Function('data', codes.join(''));
};
/**
* Template 构造函数
*
* @param {Object} data 渲染时用到的数据
* @return {string} 渲染后的文本
*/
Template.prototype.render = function (data) {
if (!this.renderer) {
this.renderer = this.compile();
}
return this.renderer(data);
};
/**
* 获取字段的值,支持对象取值和数组取值
*
* - 对象支持如下取值方式:a.b.c 或 a[b][c]
* - 数组支持如下取值方式:a[0][1]
*
* @param {string} name 字段名,可以包含`.`和`[]`
* @return {Mixed} 字段的值
*/
function getValue(name) {
if (!name) {
return '';
}
var fields = name.split(/\.|\[(\d+)\]/);
// 正则匹配出的结果有包含空值,过滤一下
for (var i = 0; i < fields.length;) {
if (!fields[i]) {
fields.splice(i, 1);
}
else {
i++;
}
}
var value = data;
for (var i = 0, len = fields.length; i < len; i++) {
var field = fields[i];
if (typeof value === 'object') {
value = value[field];
}
else {
return '';
}
}
// null或undefined值转换成空字符串
return value == null ? '' : value;
}
对进阶知识的考察
这一版的模板引擎,对知识的要求高了不少,有以下一些考察的点
- 掌握通过new Function()方式动态创建函数对象
- 对正则表达式掌握多一些,可以写出/<%(=?)\s(.?)\s*%>/g 来分别匹配变量和语句
- 掌握regExp.exec方法,并且了解string.match和regExp.exec方法的差异。掌握上述三点,认为可以达到中级的水平
- 可以完整写出一个基本可用的实现,可以正确解析上述模板
- 在给出提示,如几个优化方向的前提下,可以继续优化实现。可以做到这两点的,可以认为能达到中级以上水平。
- 视过程具体表现,及对其他模板引擎实现有无了解,可以判断是否达到高级水平
参考文章
如果觉得有帮助,可以扫描二维码对我打赏,谢谢

网友评论