在 Node.js 中,如果需要在服务端渲染 HTML 就需要通过拼接字符串的方式来实现。简单的 HTML 使用 ES6 的字符串模板也能勉强凑合,但是复杂一些的,需要涉及到循环之类的写起来就不太方便了,后期也不好维护。

模板引擎可以让你在单独的文件中按照正常的格式来编写 HTML,在 HTML 模板中也可以使用变量、循环、简单罗纪判断之类的功能。模板引擎在执行的时候,会把 HTML 中的变量之类的替换为实际的数据,然后输出 HTML String。

Node.js 有很多第三方的模板引擎,最常见的包括:

  • Handlebars
  • EJS
  • Mustache
  • pug
  • art-template

我这里使用的 Eta 是一个比较轻量的模板引擎,它的语法和 EJS 的差不多,比较简单。Eta 可以搭配 Express 之类的 Web 框架使用,也可以不使用 Web 框架,直接在 Node.js 中使用,Eta 执行完成后可以直接返回 HTML String。

安装和基本配置

初始化项目:

npm init -y

使用 npm 安装 Eta:

npm install eta --save

可以创建一个 templates 目录来存储模板文件,我这里也会使用 templates 目录来存储模板文件。

下面直接使用 Eta,不使用 Web 框架,在 templates 中创建一个 index.eta 文件作为 HTML 模板:

<a href="<%= it.url %>"><%= it.name %></a>

上面的链接 hrefhtml 都是动态生成的,下面使用 Eta 转换 index.eta

const { Eta } = require('eta');
const path = require('path');

// 初始化 eta,把 templates 设置为模板文件夹
const eta = new Eta({views: path.join(__dirname, 'templates')});
// 用于填充模板的数据
const data = {url: 'https://www.misterma.com', name: `Mr. Ma's Blog`};
// 执行 eta 模板转换
const html = eta.render('index', data);
// 在控制台输出转换后的 HTML
console.log(html);

转换后的 index.eta 模板如下:

<a href="https://www.misterma.com">Mr. Ma's Blog</a>

在 Express 中使用 Eta

下面在 Express 的项目中使用 Eta,模板使用的还是 templates/index.eta

const { Eta } = require('eta');
const express = require('express');
const path = require('path');

const app = express();
// 初始化 Eta
const eta = new Eta({
  views: path.join(__dirname, 'templates'),
  cache: false
});

// 设置使用的模板引擎和模板后缀
app.engine('ega', eta.render);
app.set('view engine', 'eta');

// 用于填充模板的数据
const data = {url: 'https://www.misterma.com', name: `Mr. Ma's Blog`};

// 处理 GET 请求
app.get('/', (req, res) => {
  // 返回 Eta 转换后的 HTML
  res.send(eta.render('index', data));
});

// 启动 http 服务
app.listen(7777, () => {
  console.log('server start http://localhost:7777');
});

使用浏览器访问 http://localhost:7777 也能看到 Mr. Ma's Blog 链接。

Eta 在 Express 中不支持使用 Express 的 res.render 输出,需要使用 Express 的 res.send 输出转换后的 HTML String。

Eta 基本语法

Eta 的模板文件使用 .eta 后缀,在模板文件中,传入 Eta 的数据都可以通过 it 变量来获取,输出数据需要写在 <%= %> 标记中。

下面是一个简单的对象:

const data = {name: '肖恩', age: 17};
eta.render('index', data);

Eta 模板:

<p>我是 <%= it.name %>,我今年 <%= it.age %> 岁</p>

转换后的 HTML 如下:

<p>我是 肖恩,我今年 17 岁</p>

默认情况下 Eta 会对 HTML 代码进行转译,也就是说 HTML 在浏览器中也会显示为 HTML 代码,如果需要 HTML 效果可以使用 <%~ %> 标记输出:

%~ it.html %>

要在 Eta 中使用 JavaScript 可以写在 <% %> 标记中:

<% it.name = '哈哈哈' %>
<%= it.name %>

如果你对 Eta 的标记,包括 <% %><%= %><%~ %> 不满意的话,也可以在初始化 Eta 的时候自定义。Eta 模板内的 it 变量也可以自定义。

关于 Eta 自定义设置之类的可以参考后面的 选项配置

在模板中引入其它模板

在 HTML 中可能会有一些每个页面都会用到的部分,比如 headerfooter、导航栏、侧边栏,这些部分没必要在每个页面中都写一份。

下面把一个 HTML 页面拆分为 headerfooter

header.eta

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title><%= it.pageTitle %></title>
</head>
<body>

footer.eta

</body>
</html>

下面在 index.eta 中引入 headerfooter 来组成一个完整的页面:

<%~ include('./header', {pageTitle: it.pageTitle}) %>
<h1><%= it.text %></h1>
<%~ include('./footer') %>

我使用的数据如下:

const data = {pageTitle: '一个完整的页面', text: 'Hello'};

最终的 index 页面如下:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>一个完整的页面</title>
</head>
<body>
<h1>Hello</h1>
</body>
</html>

在 Eta 模板中使用 include() 函数可以引入一个 Eta 模板,第一个参数是模板文件名称,不需要后缀,第二个参数是数据,include 需要写在 <%~ %> 标记里。

我上面的 header 的页面标题是动态输出的,我在 index 中引入 header 的时候也传入了一个 pageTitle 用于页面标题。

条件判断

下面根据条件显示内容:

<% if (it.login) { %>
  <a>用户信息</a>
  <a>退出登录</a>
<% } else { %>
  <a>登录</a>
  <a>注册</a>
<% } %>

上面如果 logintrue 就会输出 用户信息退出登录 链接,否则就输出 登录注册 链接。

循环

下面是一个数组对象:

const data = [
  {url: 'https://www.misterma.com', name: `Mr. Ma's Blog`},
  {url: 'https://github.com/changbin1997', name: 'Github'}
];

下面通过 forEach 循环的方式输出数据:

<% it.forEach(link => { %>
  <a href="<%= link.url %>" ><%= link.name %></a>
<% }) %>

循环输出的 HTML 如下:

<a href="https://www.misterma.com" >Mr. Ma&#39;s Blog</a>
<a href="https://github.com/changbin1997" >Github</a>

同样的数据,下面使用 for 循环输出:

<% for (let i = 0;i < it.length;i ++) { %>
  <a href="<%= it[i].url %>" ><%= it[i].name %></a>
<% } %>

API 说明

要使用 Eta 需要先使用:

const eta = new Eta(options);

初始化 Eta。

渲染模板文件可以使用:

eta.render('index', {name: 'Hello'});

第一个参数是模板文件名称,不需要加 eta 后缀,第二个参数是数据。

如果需要异步渲染可以使用:

eta.renderAsync('index', {name: 'Hello'}).then(result => {
  res.send(result);
});

renderAsyncrender 的参数是一样的,renderAsync 渲染完成后会返回一个 Promise。

如果需要在 JS 中使用字符串模板可以使用 renderString 渲染,下面是一个字符串模板:

// 字符串模板
const templateStr = `
<a href="<% it.url %>"><%= it.name %></a>
`;
// 数据
const data = {name: `Mr. Ma's Blog`, url: 'https://www.misterma.com'};
// 渲染字符串模板
const html = eta.renderString(templateStr, data);

renderString 的第一个参数是模板字符串,第二个参数是数据。

异步方式渲染字符串模板可以使用 renderStringAsyncrenderStringAsyncrenderString 的参数是一样的,只是 renderStringAsync 返回的是 Promise。

配置选项

在使用 new Eta 初始化的时候需要传入一个配置对象,下面是一些配置选项说明:

// 配置选项
const options = {
  // 设置模板文件夹的位置
  views: path.join(__dirname, 'templates'),
  // 对 XML 和 HTML 代码进行自动转译,默认为 true
  autoEscape: false,
  // 是否缓存模板,默认为 false
  cache: false,
  // 保存已解析文件路径的缓存,默认为 false
  cacheFilepaths: false,
  // debug 模式,出错时对错误信息进行美化输出,默认为 false
  debug: false,
  // 删除 HTML 中的空格,默认为 false
  rmWhitespace: false,
  // 自定义 Eta 模板标记符,默认为 ['<%', '%>']
  tags: ['<?eta', '?>'],
  // 在 Eta 模板内不使用 it 变量,直接访问传入的对象,默认为 false
  useWith: false,
  // Eta 模板内数据变量的名称,默认为 it
  varName: 'it',
  // 解析设置
  parse: {
    // 在 Eta 模板内使用 JavaScript 的前缀,默认没有前缀,也就是在 <% %> 使用 JS 代码
    exec: '',
    // 在 Eta 模板内输出内容的前缀,默认为 '=' ,也就是在 <%= %> 内输出
    interpolate: '=',
    // 在 Eta 模板内输出原始内容的前缀,默认为 '~' ,也就是在 <%~ %> 内输出 HTML 代码和引入模板
    raw: '~'
  }
};

// 初始化 Eta
const eta = new Eta(options);

以上就是 Eta 的基本使用。Eta 模板比较简单,Eta 标记之类的都可以按照自己的习惯配置,简单看一下说明就可以直接上手使用。