【Node.js 系列 9】用 20 行代码带你了解模板引擎实现原理

1,166 阅读2分钟

模板引擎介绍

先简单科普一下模板引擎是什么,模板引擎就是将数据(data)和模板(template)合并然后生成 HTML 文本。

html = 模板引擎(模板 + 数据)

在 Node.js 里,常见的模板引擎有:ejs、handlerbars、jad 等等,相信不少人都用过。今天主要是让大家了解模板引擎的核心原理,并使用 ejs 的语法来实现一个模板引擎,整个代码实现只有 20 行。

话不多说,先上代码。

// 模板引擎代码
function render(template, data) {
  template = `
    with(data){
      let str = '';
      str += \`${template}\`;
      return str;
    }
  `

  template = template.replace(/{%=(.+)%}/g, (...args) => {
    return '${' + args[1] + '}'
  })

  template = template.replace(/<%(.+?)%>/g, (...args) => {
    return `\`;${args[1]}; str+=\``
  })

  const res = new Function('data', template)(data)
  return res;
}

使用

// 模板字符串
// 正常情况是使用 fs.readFile() 来读取文件拿到模板字符串
const html = `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <!-- <% 这里是 js 语句 %> -->
  <!-- <%= 这里是 js 表达式 %> -->
  <% arr.forEach(item => { %>
    <p>{%= item %}</p>
  <% }) %>

  <%if(Array.isArray(arr)){%>
    console.log(1)
  <%}%>
</body>
</html>`

console.log(render(html, {arr: [1, 2, 3]}))

输出结果为

'<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>

    <p>1</p>

    <p>2</p>

    <p>3</p>



    1,2,3

</body>
</html>'

原理解析

我们可以看到,模板字符串内有 js 语句,如果想得到完整的结果,必须要执行这些 js 语句,但是现在拿到的只有字符串,很明显不能执行,那如何让字符串执行呢?

答案是:new Function(str),str 就是一个字符串,代表函数体,例如:

const fn = new Function(`
  let a = 'hello '
  let b = 'world'
  return a + b
`)
console.log(fn()) // 输出 hello world

现在有办法让字符串执行 js 语句了,但是语法不对,因为如果以现在的模板直接 new Function() 的话,相当于创建了一个如下的函数:

function tpl() {
  <!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
  </head>
  <body>
    <!-- <% 这里是 js 语句 %> -->
    <!-- <%= 这里是 js 表达式 %> -->
    <% arr.forEach(item => { %>
      <p>{%= item %}</p>
    <% }) %>

    <%if(Array.isArray(arr)){%>
      console.log(1)
    <%}%>
  </body>
  </html>
}

很明显语法错误,所以现在要把模板解析为可执行的 js 语句。

可以尝试把上面的模板字符串转换成如下可执行的字符串:

'let str = '';
str += `<!DOCTYPE html>
  <html lang="en">
  <head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  </head>
  <body>`;

arr.forEach(item => {
  str += `<p>${ item }</p>`;
});

if (Array.isArray(arr)) {
  str += `${arr}`;
}

str += `
  </body>
  </html>
`;
return str;'

为了方便大家阅读,转换后的字符串做了美化。

看到这不知道大家有没有思路了?本质就是字符串替换,只有找到一定的规律就可以完成这件事。

其实只需三步即可:

    1. 字符串内声明 str 变量,拼接并用 str 拼接模板字符串,并 return str。
template = `
  let str = "";
  str += \`${template}\`;
  return str;`

拼接完成后模板变成这样:

'let str = '';
str += `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <% arr.forEach(item => { %>
    <p>{%= item %}</p>
  <% }) %>

  <%if(Array.isArray(arr)){%>
    <p>{%= item %}</p>
  <%}%>
</body>
</html>`;
return str;'
    1. {%= %}替换成 ${}

代码如下:

template = template.replace(/{%=(.+)%}/g, (...args) => {
  return '${' + args[1] + '}'
})
    1. 让 js 语句可执行,所以要删掉<% %> ,并在语句内部去拼接 str,这里拼接的时候一定要使用 ``,因为内部解析表达式的时候用的是 ${}

实现代码:

template = template.replace(/<%(.+?)%>/g, (...args) => {
  return `\`;${args[1]}; str+=\``
})

其实到这,这个模板引擎就已经可以工作了,但是还有一个地方需要注意,就是我们在模板内部使用变量时,外层的对象默认被解构了:

`<% arr.forEach(item => { %>
  <p>{%= item %}</p>
<% }) %>

<%if(Array.isArray(arr)){%>
  <p>{%= item %}</p>
<%}%>`

我们传入模板的数据是{arr: [1, 2, 3]},理论上应该用 data.arr 去访问 arr,但实际在使用的时候直接用的是 arr,这里是怎么实现的呢?

答案是 ```with`` 语句,看一下 demo 演示:

const data = { arr: [1, 2, 3] }
with (data) {
  arr.forEach(item => console.log(item));
}

所以我们要在字符串最外层在加一个 with:

template = `
  with(data){
    let str = '';
    str += \`${template}\`;
    return str;
  }
`

现在变量访问的问题也解决了就,然后就是生成一个这样的函数 const fn = new Fuction('data', template)

调用 fn 得到解析好的模板字符串 fn(data)

模板引擎大功告成。

小结

思路就是字符串替换,关键是大家拼接的时候一定要多打印,多实验,哪里缺少补哪里。明白思路之后就是一个查缺补漏的体力活了。

最后

如果这篇文章对你有帮助的话,希望得到你的点赞认可~~~

欢迎关注公众号:前端superYue,不定期分享各种原理解析,回复 资料 有惊喜哦!!!

本文使用 mdnice 排版