模板引擎
模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。就是将模板文件和数据通过 模板引擎 的解析生成一个HTML代码。
{{}} 表达式
比如我们有这样的模板:
<div>{{ name }}</div>
我们要将其解析为标准的 HTML 代码:
<div>jack</div>
又比如对字符进行大小写转换的一个模板:
<div>{{ name.toUpperCase() }}</div>
转换为标准的HTML代码:
<div>JACK</div>
forEach 循环
模板:
{%arr.forEach(item => {%}
<li>{{item}}</li>
{%})%}
经过模板引擎解析后生成的结果:
<li>aaa</li>
<li>bbb</li>
<li>ccc</li>
if判断
模板文件:
{% if(isShow) { %} <div>{{ name }}</div> {% } %}
经过模板引擎解析后的结果:
<div>jack</div>
实现思路
模板引擎的解析我们分为两个步骤:
- 利用正则表达式和 Function 构造函数将模板编译为一个 Generate 函数
- 执行 Generate 函数,输出编译结果
具体实现
第一步 {{ xxx }}表达式 转换
利用正则表达式将 {{ xxx }} 表达式转换成 ES6 模板字符串 ${xxx}
// [^}] 匹配除了 } 以外的所有字符
template = template.replace(/\{\{([^}]+)\}\}/g, function () {
// arguments 对象 replace 回调函数的 参数
let key = arguments[1].trim();
return "${" + key + "}" // 将模板字符串 <b>{{ name }}</b> 转换成 <b>${name}</b>
})
上面的代码,将输入的模板
{{ name }}
转换成了 ${name}
的形式
第二步 {% %}表达式 转换
同样是利用正则将 {% %} 表达式转换成 js 语句:
// [^%] 匹配除了 % 以外的任意字符
template = template.replace(/\{\%([^%]+)\%\}/g, function () {
return "`\r\n" + arguments[1] + "\r\nstr+=`\r\n"
})
例如我们输入的模板是:
{% if(isShow) { %} <div>{{ name }}</div> {% } %}
则经过上面代码解析后输出的结果是:
`
if(isShow) {
str+=`
<div>${name}</div>`
}
str+=`
第三步 创建 Generate 函数
利用 Function 构造函数创建一个 Generate 函数
new Function('obj', body)
其中 obj 为传递给 Generate 函数的参数,body
为 Generate 函数的函数体。
我们仍然以模板 {% if(isShow) { %} <div>{{ name }}</div> {% } %}
为例:
// head 为函数的函数体
let head = `let str = '';\r\n with(obj){\r\n`;
head += "str+=`";
// 模板解析
template = template.replace(/\{\%([^%]+)\%\}/g, function () {
return "`\r\n" + arguments[1] + "\r\nstr+=`\r\n";
});
let tail = "`}\r\n return str;";
const body = head + template + tail
使用 new Function('obj', body) 后生成的函数如下:
function anonymous(obj) {
let str = '';
with(obj) {
str+=``
if(isShow) {
str+=`
<b>${name}</b> `
}
str+=`
`}
return str;
}
第四步 执行 Generate 函数
const ret = generate({name : 'tom'})
// 输出结果: <div>jack</div>
完整代码实现
module.exports.compile = (template) => {
// [^}] 匹配除了 } 以外的所有字符
template = template.replace(/\{\{([^}]+)\}\}/g, function () {
// arguments 对象 replace 回调函数的 参数
let key = arguments[1].trim();
return "${" + key + "}" // 将模板字符串 <div>{{ name }}</div> 转换成 <div>${name}</div>
})
// head 为函数体
let head = `let str = '';\r\n with(obj) {\r\n`
head += "str+=`";
// [^%] 匹配除了 % 以外的任意字符
template = template.replace(/\{\%([^%]+)\%\}/g, function () {
return "`\r\n" + arguments[1] + "\r\nstr+=`\r\n"
})
let tail = "`}\r\n return str;"
// 利用 Function 构造函数创建一个新的函数
return new Function('obj', head + template + tail)
}
测试用例
const { compile } = require("../index");
describe("模板编译", () => {
it("{{}} 表达式", () => {
const output = compile("<div>{{ name }}</div>")({ name: "jack" });
expect(output).toBe(`<div>jack</div>`);
});
it("{{}} toUpperCase 表达式", () => {
const output = compile("<div>{{ name.toUpperCase() }}</div>")({ name: "jack" });
expect(output).toBe(`<div>JACK</div>`);
});
it("{{}} +连接", () => {
const output = compile("<div>{{ '[' + name + ']' }}</div>")({ name: "jack" });
expect(output).toBe(`<div>[jack]</div>`);
});
it("forEach 遍历", () => {
const output = compile(
`{%arr.forEach(item => {%}
<li>{{item}}</li>
{%})%}`
)
({
arr: ["aaa", "bbb"],
});
expect(output).toBe(
`
<li>aaa</li>
<li>bbb</li>
`);
});
it("if 表达式", () => {
const output = compile(`{% if(isShow) { %} <div>{{ name }}</div> {% } %}`
)({ isShow: true, name: "jack" });
expect(output).toBe(
`
<div>jack</div>
`);
});
});
测试结果
运行 jest template --watchAll 命令,结果如下