模板引擎实现原理(EJS篇)

原理就是正则替换 + 字符串拼接,以 ejs 为例,具体步骤为:

  1. 读取模板文件得到原始字符串
  2. 正则替换 <% %><%= %>
  3. 拼接成代码形式的字符串
  4. 使用 new Function 将字符串转化成函数
  5. 函数内部使用 with 进行取值

效果预览

不妨写一个 index.html,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>EJS</title>
</head>
<body>
<h2><%=title %></h2>
<p><%=date%></p>
<ul>
<%arr.forEach(item=>{%><li><%=item%></li><%})%>
</ul>
<div>
姓名:<%=obj.name%>,年龄:<%=obj.age%>
</div>
</body>
</html>

先用原生的 ejs 测试一下输出效果:

1
2
3
4
5
6
7
8
9
10
11
const fs = require('fs')
const ejs = require('ejs')
const data = {
title: 'EJS引擎',
date: new Date('2020-02-02'),
obj: { name: '张三', age: 10 },
arr: [1, 2, 3, 4, 5],
}
const tpl = fs.readFileSync('index.html', 'utf-8')
const html = ejs.render(tpl, data)
console.log(html)

可以看到输出结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>EJS</title>
</head>
<body>
<h2>EJS引擎</h2>
<p>Sun Feb 02 2020 08:00:00 GMT+0800 (GMT+08:00)</p>
<ul>
<li>1</li><li>2</li><li>3</li><li>4</li><li>5</li>
</ul>
<div>
姓名:张三,年龄:10
</div>
</body>
</html>

如何实现

整个模板渲染其实就是一个大的字符串,其中穿插着 EJS 语法,主要是 <%=xx %><% xx %> 语法:

处理 <%=xx %>

对于 <%=xx %> 这种语法,我们很容易想到直接用 data 里面的值替换进去即可,正则为:

1
tpl = tpl.replace(/<%=(.+?)%>/g, '${$1}')

替换之后的字符串为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>EJS</title>
</head>
<body>
<h2>${title }</h2>
<p>${date}</p>
<ul>
<%arr.forEach(item=>{%><li>${item}</li><%})%>
</ul>
<div>
姓名:${obj.name},年龄:${obj.age}
</div>
</body>
</html>

处理 <% xx %>

对于 <% xx %> 这种语法,因为里面有 JS 语句,要想让其执行并得到返回结果则必须让其运行起来,可以拼接出一个完整的可运行的 JS 代码:

1
2
3
4
const head = 'let str = ``\r\nwith(data){\r\nstr+=`'
const body = tpl.replace(/<%(.+?)%>/g, '`\r\n$1\r\nstr+=`')
const tail = '`}\r\nreturn str'
tpl = head + body + tail

得到的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let str = ``
with(data){
str+=`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>EJS</title>
</head>
<body>
<h2>${title }</h2>
<p>${date}</p>
<ul>
`
arr.forEach(item=>{
str+=`<li>${item}</li>`
})
str+=`
</ul>
<div>
姓名:${obj.name},年龄:${obj.age}
</div>
</body>
</html>
`}
return str

然后将其包装在一个 function 里面并运行即可:

1
2
const f = new Function('data', tpl)
f(data)

输出结果跟官方一致。这种拼代码字符串的方式,任何 JS 语句都可以渲染出来,例如下面的 if 语句:

1
2
3
<% if (obj) { %>
<h2><%= obj.name %></h2>
<% } %>