# 模板引擎的实现原理

# 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

./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

最终渲染出来的字符串

<!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

我们可以尝试写一版自己的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

# 复杂版本的替换: 列表遍历

./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

对于模板引擎来说,最底层的逻辑都是字符串的拼接 拼接出想要的代码之后,借助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

来解释一下上述这个字符串的拼接,我们最终渲染出来的代码中肯定不包含 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

# 详解 new Function的使用方法

在使用js的时候,一般我们生命函数 使用这种方法:

function foo(arg1,arg2...) {
  // ...
}
1
2
3

我们来看看下面这种写法

let str = 'return ' + '`Hello ${name}!`';
let func = new Function('name', str);
console.log(func('Jack')) // "Hello Jack!"
1
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
最后更新时间: 10/12/2021, 1:41:05 PM