Skip to content

ExpressJS - 模板引擎

在构建 Web 应用程序时,你经常需要生成动态 HTML 页面。与其在服务器代码中手动构建 HTML 字符串(这容易出错且难以维护),不如使用模板引擎。模板引擎允许你编写带有特殊语法的静态模板文件,以便在将页面发送到客户端之前在服务器上动态插入数据。

Pug(以前称为 Jade)是一个流行且功能强大的 Node.js 和浏览器模板引擎。它具有简洁、基于缩进的语法,简化了 HTML 编写,并支持包含(includes)、继承(inheritance)和插值(interpolation)等功能。虽然我们将重点关注 Pug,但其他流行的引擎如 EJS(使用带有 <% %> 标签的纯 HTML)和 Handlebars(使用 {{ }} 标签)也是不错的选择。

首先,你需要将 Pug 添加为项目依赖项。在项目根目录中打开终端并运行:

npm install pug

接下来,配置 Express 使用 Pug 作为其视图引擎(view engine)。你不需要显式地 require('pug');当你设置引擎时,Express 会在内部处理它。将这两行添加到你的主应用程序文件(例如 index.js)中:

// 将视图引擎设置为 Pug
app.set('view engine', 'pug');
// 指定模板文件所在的目录
app.set('views', './views');

按照惯例,Express 会在项目根目录中的 views 目录中查找模板。让我们创建这个目录并在其中创建一个简单的 Pug 文件。

创建一个名为 views 的新目录。在其中,创建一个名为 first_view.pug 的文件,内容如下:

doctype html
html(lang="en")
head
meta(charset="UTF-8")
meta(name="viewport", content="width=device-width, initial-scale=1.0")
title Hello Pug
body
p.greetings#people Hello World!

关于现代实践的注意事项:

  • <html> 标签上的 lang="en" 属性有利于可访问性(accessibility)和 SEO。
  • viewport meta 标签对于移动设备上的响应式设计至关重要。

现在,在 index.js 中创建一个路由,使用 res.render() 渲染此模板:

app.get('/first-template', (req, res) => {
// Express 会在 'views' 目录中查找 'first_view.pug'
res.render('first_view');
});

你的完整 index.js 现在应该看起来像这样:

import express from 'express';
const app = express();
const PORT = process.env.PORT || 3000;
// 配置视图引擎
app.set('view engine', 'pug');
app.set('views', './views');
// 渲染模板的路由
app.get('/first-template', (req, res) => {
res.render('first_view');
});
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});

运行你的服务器(如果你已经使用 nodemon 设置了脚本,则运行 npm run dev),然后导航到 http://localhost:3000/first-template。你将看到一个显示“Hello World!”文本的页面。Pug 会将你简洁的 .pug 文件编译成标准的 HTML:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello Pug</title>
</head>
<body>
<p class="greetings" id="people">Hello World!</p>
</body>
</html>

模板的真正强大之处在于能够从路由处理程序向模板传递动态数据。res.render() 的第二个参数是一个包含你要传递的数据的对象。

让我们创建一个新路由和相应的模板:

app.get('/profile/:username', (req, res) => {
const user = {
name: req.params.username,
isLoggedIn: true,
lastLogin: new Date(),
interests: ['Web Development', 'Node.js', 'Express']
};
res.render('profile', {
title: `${user.name}'s Profile`,
user: user
});
});
doctype html
html(lang="en")
head
title= title
body
//- 这是一个 Pug 注释。它不会出现在 HTML 输出中。
// 使用 '=' 输出转义的 HTML。这可以防止 XSS 攻击。
h1 Welcome, #{user.name}!
// 使用 'if/else' 进行条件判断
if user.isLoggedIn
p Your last login was on #{user.lastLogin.toLocaleDateString()}.
else
p Please log in to see your details.
// 使用 'each' 进行循环
if user.interests.length
h2 Your Interests:
ul
each interest in user.interests
li= interest
else
p You have no listed interests.

现在,如果你访问 http://localhost:3000/profile/Alice,Pug 将使用 user 对象生成一个动态 HTML 页面。这种将变量直接嵌入文本的技术称为插值(#{variableName})。

如上所示,你可以在 Pug 模板中直接使用类似 JavaScript 的控制流:

  • 条件判断:if、else if、else 和 unless(即 if !(...))。
  • 迭代:使用 each val in array 或 each val, index in array 来遍历数组。你也可以使用 while 循环。

为了避免在每个页面上重复编写像页眉和页脚这样的代码(遵循 Don’t Repeat Yourself 即不要重复你自己原则),Pug 提供了 include 和模板继承(extends 和 block)。

使用 include:创建可重用组件。

header
nav
a(href='/') Home
a(href='/about') About
footer
p &copy; #{new Date().getFullYear()} My Awesome App
doctype html
html(lang="en")
head
title Home Page
body
include partials/header.pug
main
h1 Welcome to the Home Page!
include partials/footer.pug

使用布局:一种更强大的方法是定义一个基础布局并对其进行扩展。

doctype html
html(lang="en")
head
title= title || 'My App'
link(rel='stylesheet', href='/css/style.css')
body
include partials/header.pug
main.container
block content
//- 子模板的内容将插入到这里
include partials/footer.pug
extends layout.pug
block content
h1 About Us
p This is the about page for our awesome application.

使用像 Pug 这样的模板引擎是一种**服务器端渲染(SSR)**形式。这种方法非常适用于:

  • 内容密集型网站:博客、电子商务网站和新闻门户网站受益于快速的初始加载时间和强大的 SEO。
  • 搜索引擎优化 (SEO):搜索引擎可以轻松抓取和索引预渲染的 HTML 内容。
  • 简洁性:对于许多应用程序而言,SSR 比完整的客户端框架更易于设置和管理。

另一种方法是使用 Express 构建一个 REST API,将 JSON 数据发送到使用 React、Vue 或 Angular 等框架构建的独立单页应用程序 (SPA)。SPA 然后在用户的浏览器中处理所有渲染。这对于高度交互的、类似应用程序的体验(例如仪表板、社交媒体动态)来说是一个很好的选择。