创建 JS 模板引擎
创建 JS 模板引擎
创建 JS 模板引擎
大家好,我是@shadowtime2000 , Eta的维护者之一,Eta 是一个快速可嵌入的模板引擎。在本教程中,我将向您展示如何创建一个同构(浏览器/Node)模板引擎。
设计
模板引擎的初始设计会非常简单。它只会对data对象中的值进行插值。它将使用{{valueName}}插值方法。
简单渲染
首先,我们创建一个简单的渲染函数,它接收模板和数据,并渲染该值。
var render = (template, data) => {
return template.replace(/{{(.*?)}}/g, (match) => {
return data[match.split(/{{|}}/).filter(Boolean)[0]]
})
}
基本上,它的作用是查找方括号内的所有内容,并将其替换为括号内的名称data。你可以像这样编写模板,它会从数据对象中获取内容。
Hi my name is {{name}}!
render("Hi, my name is {{name}}!", {
name: "shadowtime2000"
});
但是存在一个问题,插值中不能有空格。
render("Hi, my name is {{ name }}!", {
name: "shadowtime2000"
})
/*
Hi, my name is undefined!
*/
这要求数据对象内部包含空格,这不太规范。我们可以通过在插值之前去除数据名称的前导和尾随空格来解决这个问题。
var render = (template, data) => {
return template.replace(/{{(.*?)}}/g, (match) => {
return data[match.split(/{{|}}/).filter(Boolean)[0].trim()]
})
}
这已经相当不错了,但对于较大的模板来说速度就会慢下来,因为它每次都需要解析。这就是为什么许多模板引擎支持编译的原因:将模板编译成一个速度更快的 JavaScript 函数,该函数可以接收数据并进行插值。让我们为模板引擎添加编译功能,但在此之前,我们需要添加一个特殊的解析函数。
解析
由于解析过程可能有点枯燥,我们不妨直接复用其他 JS 模板引擎的代码。我本来想用 Eta 解析引擎,但它已经过高度优化,对一些用户来说可能比较复杂。所以,我们改用另一个流行的 JS 模板引擎mde/ejs 的解析代码。请务必注明解析引擎的作者。
var parse = (template) => {
let result = /{{(.*?)}}/g.exec(template);
const arr = [];
let firstPos;
while (result) {
firstPos = result.index;
if (firstPos !== 0) {
arr.push(template.substring(0, firstPos));
template = template.slice(firstPos);
}
arr.push(result[0]);
template = template.slice(result[0].length);
result = /{{(.*?)}}/g.exec(template);
}
if (template) arr.push(template);
return arr;
}
这段代码的基本功能是循环遍历模板,执行正则表达式匹配,并将结果添加到数据结构中。以下是该数据结构的示例:
["Hi my name is ", "{{ name }}", "!"]
汇编
让我们快速概览一下编译后的输出结果。假设您输入以下模板:
Hi my name is {{ name }}!
它会给你提供以下功能:
function (data) {
return "Hi my name is "+data.name+"!";
}
我们先创建一个解析函数,然后再创建一个可以使用的字符串。我们首先需要解析模板。
const compileToString = (template) => {
const ast = template;
}
我们还需要创建一个字符串,该字符串将用作函数。
const compileToString = (template) => {
const ast = template;
let fnStr = `""`;
}
之所以在开头使用引号,是因为在编译模板等内容时,它们都会以引号开头+。现在我们需要遍历抽象语法树 (AST)。
const compileToString = (template) => {
const ast = template;
let fnStr = `""`;
ast.map(t => {
// checking to see if it is an interpolation
if (t.startsWith("{{") && t.endsWith("}}")) {
// append it to fnStr
fnStr += `+data.${t.split(/{{|}}/).filter(Boolean)[0].trim()}`;
} else {
// append the string to the fnStr
fnStr += `+"${t}"`;
}
});
}
该函数的最后一部分是返回函数字符串。
const compileToString = (template) => {
const ast = template;
let fnStr = `""`;
ast.map(t => {
// checking to see if it is an interpolation
if (t.startsWith("{{") && t.endsWith("}}")) {
// append it to fnStr
fnStr += `+data.${t.split(/{{|}}/).filter(Boolean)[0].trim()}`;
} else {
// append the string to the fnStr
fnStr += `+"${t}"`;
}
});
return fnStr;
}
所以如果它采用这个模板:
Hi my name is {{ name }}!
它将返回以下内容:
""+"Hello my name is "+data.name+"!"
现在这一步完成了,创建编译函数就相对简单了。
const compile = (template) => {
return new Function("data", "return " + compileToString(template))
}
现在我们的模板引擎编译已经完成。
总结
在本教程中,我演示了如何:
- 实现一个简单的渲染函数
- 了解基于 EJS 的解析引擎
- 遍历抽象语法树 (AST) 以创建快速编译函数