# 模板引擎的实现原理
# ejs模板引擎的初体验
我们以ejs为例子,这个模板引擎是非常常见的。我们创建一个模板文件,以ejs的语法在模板中声明变量,最终ejs会将占位符替换为变量。
./src/template.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<%=name%>
<%=age%>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
./src/ejs.js
const ejs = require('ejs');
// 复杂的情况
(async function() {
let r = await ejs.renderFile('template.html', {name: "louis", age: 18})
console.log(r);
})();
1
2
3
4
5
6
2
3
4
5
6
最终渲染出来的字符串
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
louis
18
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
我们可以尝试写一版自己的ejs的模板引擎。
# 简单的变量替换
./src/ejs.js
const fs = require("fs")
const util = require("util")
const path = require("path")
const read = util.promisify(fs.readFile)
let ejs = {
async renderFile(filename, options) {
let content = await read(filename, "utf8")
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/replace
// replace 接收一个回调函数作为参数 第一个参数是模式 第二个参数是匹配到的
content = content.replace(/<%=(.+?)%>/g, function () {
// arguments[1] => name arguments[1] => age
return options[arguments[1]]
})
return content
}
}
;(async function() {
let r = await ejs.renderFile('template.html', {name: "louis", age: 18})
console.log(r);
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 复杂版本的替换: 列表遍历
./src/template.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<% arr.forEach((item) => { %>
<li><%=item%></li>
<% }) %>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
对于模板引擎来说,最底层的逻辑都是字符串的拼接 拼接出想要的代码之后,借助with 和 new Function 这就是最终的答案。
let str = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>`
arr.forEach((item) => {
str += `<li>1</li>`
})
str += `</body>
</html>`
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
来解释一下上述这个字符串的拼接,我们最终渲染出来的代码中肯定不包含 arr.forEach 这种代码,但是内部的li元素肯定是需要的。因此就想办法构造这种形式。
let ejs = {
async renderFile(filename, options) {
let content = await read(filename, "utf8")
content = content.replace(/<%=(.+?)%>/g, function () {
return "${" + arguments[1] + "}" // 获取对应的内容做这件事
})
let head = 'let str = "";\n with(obj){ \n str+=`'
let body = (content = content.replace(/<%(.+?)%>/g, function () {
return "`\n" + arguments[1] + "\nstr+=`"
}))
let tail = "`} return str"
let fn = new Function("obj", head + body + tail)
return fn(options)
},
}
;(async function () {
let r = await ejs.renderFile(path.resolve(__dirname + "/template.html"), {
arr: [1, 2, 3],
})
console.log(r)
})()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 详解 new Function的使用方法
在使用js的时候,一般我们生命函数 使用这种方法:
function foo(arg1,arg2...) {
// ...
}
1
2
3
2
3
我们来看看下面这种写法
let str = 'return ' + '`Hello ${name}!`';
let func = new Function('name', str);
console.log(func('Jack')) // "Hello Jack!"
1
2
3
2
3
事实上 new Function 的使用方法是这样的:
var function_name=new function(arg1,arg2, ..., argN, function_body)
1
其中 arg1, arg2 直到 argN 就是我们需要传递的参数,可以写任意个,最后一个function_body就是我们希望函数执行的函数体,这里函数体必须放在最后,而且参数和函数体都必须用字符串的形式写入。
知道了这点,我们再来梳理下这段代码
let str = 'return ' + '`Hello ${name}!`';
let func = new Function('name', str);
console.log(func('Jack')) // "Hello Jack!"
// 第二行代码中,第一个参数name表示形参,需要我们调用时传递,
// 第二个参数str是我们自定义的一个字符串"return hello ${name}!",这就相当于如下写法
function func(name) {
return "hello" + name;
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8