Node.js: 模板引擎与 SSR
最后更新:2026-08-26
1. 1 故事:Charlie 的管理工具界面
Charlie 是一家初创公司的后端开发者,团队需要一个内部管理工具来查看订单、用户统计和系统状态。同事们建议用 React 或 Vue,但 Charlie 觉得对于这种内部工具太重了——页面不多、交互简单,用模板引擎在服务端渲染就够了。他选择了 EJS,因为它几乎就是 HTML 加上几个标签,上手零门槛。一天之内,他就用 Express + EJS 搭出了带布局复用的完整管理页面。
你将学到:
- EJS 的四种核心语法标签及其用途
- Express 集成 EJS 与 Pug 的配置方法
- Pug 的缩进语法、变量、循环与条件
- 模板渲染的完整流程
- 局部视图与布局的复用策略
- 数据从路由传递到模板的方式
- SSR 与 CSR 的核心区别与选型依据
2. 2 模板渲染流程
模板引擎的核心工作是将模板文件与数据合并,生成最终的 HTML 字符串返回给浏览器。
flowchart LR
A[浏览器请求] --> B[Express 路由]
B --> C[获取数据]
C --> D["res.render()"]
D --> E[EJS / Pug 编译]
E --> F[生成 HTML]
F --> G[HTTP 响应]
整个流程可以概括为:
- 浏览器发起 HTTP 请求
- Express 路由匹配到对应处理函数
- 处理函数从数据库或 API 获取数据
- 调用
res.render()传入模板名和数据 - 模板引擎将模板 + 数据编译为 HTML
- HTML 作为 HTTP 响应返回给浏览器
3. 3 EJS 基础语法
EJS(Embedded JavaScript)是最直观的模板引擎——模板就是普通 HTML,逻辑用 <% %> 标签嵌入。
▶ 示例:EJS 四种核心标签
JAVASCRIPT
// <%= %> 输出转义后的值(安全,防 XSS)
<p>Hello, <%= userName %>!</p>
// <% %> 执行逻辑,不输出
<% if (isAdmin) { %>
<span>Admin Panel</span>
<% } %>
// <%- %> 输出原始 HTML(不转义,慎用)
<%- articleHtml %>
// <%- include %> 引入子模板
<%- include('partials/header') %>
(1) EJS 语法速查表
| 标签 | 功能 | 输出 | 转义 | 示例 |
|---|---|---|---|---|
<%= %> |
输出变量值 | 是 | 是 | <%= name %> |
<%- %> |
输出原始内容 | 是 | 否 | <%- html %> |
<% %> |
执行逻辑 | 否 | — | <% if (x) { %> |
<%# %> |
注释 | 否 | — | <%# comment %> |
<%- include('x') %> |
引入子模板 | 是 | 否 | <%- include('nav') %> |
▶ 示例:EJS 循环与条件
JAVASCRIPT
<ul>
<% items.forEach(function(item) { %>
<li>
<strong><%= item.name %></strong>
<% if (item.onSale) { %>
<span class="badge">Sale</span>
<% } %>
</li>
<% }); %>
</ul>
4. 4 Express + EJS 配置
▶ 示例:完整 Express + EJS 项目搭建
项目结构:
TEXT
📖 仅展示
views/
partials/
header.ejs
footer.ejs
layout.ejs
index.ejs
app.js
BASH
npm init -y
npm install express ejs
JAVASCRIPT
const express = require('express');
const app = express();
app.set('view engine', 'ejs');
app.set('views', './views');
app.get('/', (req, res) => {
res.render('index', {
title: 'Dashboard',
user: { name: 'Charlie', role: 'admin' },
stats: { orders: 128, users: 56 }
});
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
(1) 关键 API 说明
| API | 作用 | 示例 |
|---|---|---|
app.set('view engine', 'ejs') |
设置默认模板引擎 | 渲染时无需写扩展名 |
app.set('views', path) |
设置模板目录 | 默认为 ./views |
res.render(view, data) |
渲染模板并响应 | res.render('index', {title: 'Hi'}) |
res.render(view, data, callback) |
渲染后获取 HTML 字符串 | 可用于发送邮件等场景 |
▶ 示例:res.render 第二个参数传递数据
JAVASCRIPT
app.get('/profile/:id', (req, res) => {
const user = { id: req.params.id, name: 'Alice', bio: 'Full-stack dev' };
res.render('profile', {
user: user,
pageTitle: user.name + "'s Profile",
isLoggedIn: true
});
});
模板中直接使用变量名访问:
JAVASCRIPT
<h1><%= pageTitle %></h1>
<p>Name: <%= user.name %></p>
<p>Bio: <%= user.bio %></p>
5. 5 Pug 基础语法
Pug(原名 Jade)用缩进代替标签闭合,语法极其简洁,但学习曲线比 EJS 陡峭。
▶ 示例:Pug 基本写法
TEXT
📖 仅展示
//- Pug 模板
doctype html
html
head
title= pageTitle
body
h1= message
p Welcome to #{siteName}
编译后的 HTML:
TEXT
📖 仅展示
<!DOCTYPE html>
<html>
<head><title>My Page</title></head>
<body>
<h1>Hello</h1>
<p>Welcome to MySite</p>
</body>
</html>
(1) Pug 语法速查表
| 语法 | 功能 | 示例 |
|---|---|---|
tag= value |
属性绑定 | h1= title |
#{expr} |
插值 | `p Hello #{name}`` |
- code |
执行 JS | - const x = 1 |
if/else if/else |
条件 | if (admin) ... |
each val in arr |
循环 | each item in list |
extends layout |
继承布局 | extends layout |
block name |
定义/填充块 | block content |
include path |
引入子模板 | include header |
▶ 示例:Pug 循环与条件
TEXT
📖 仅展示
ul
each item in items
li
strong= item.name
if item.onSale
span.badge Sale
6. 6 局部视图与布局复用
避免在每个页面重复写 header、footer 和 HTML 骨架,是模板引擎的核心价值。
▶ 示例:EJS 布局 + 局部视图
views/partials/header.ejs:
JAVASCRIPT
<header>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>
views/partials/footer.ejs:
JAVASCRIPT
<footer>© 2026 My App</footer>
views/layout.ejs:
JAVASCRIPT
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<%- include('partials/header') %>
<main>
<%- body %>
</main>
<%- include('partials/footer') %>
</body>
</html>
EJS 没有原生 layout 机制,需要借助
express-ejs-layouts中间件或手动 include 实现。
BASH
npm install express-ejs-layouts
JAVASCRIPT
const expressLayouts = require('express-ejs-layouts');
app.use(expressLayouts);
app.set('layout', 'layout');
▶ 示例:Pug 布局继承
views/layout.pug:
TEXT
📖 仅展示
doctype html
html
head
title= title
link(rel="stylesheet" href="/style.css")
body
include partials/header
block content
include partials/footer
views/index.pug:
TEXT
📖 仅展示
extends layout
block content
h1= pageTitle
p Welcome!
(1) 布局策略对比
| 特性 | EJS | Pug |
|---|---|---|
| 布局继承 | 需中间件 express-ejs-layouts |
原生 extends + block |
| 局部引入 | <%- include('partial') %> |
include partial |
| 传参给子模板 | <%- include('x', {data}) %> |
通过父级数据传递 |
| 多 block 支持 | 有限 | 原生支持多个 block |
7. 7 模板引擎对比
(1) EJS vs Pug vs Handlebars
| 特性 | EJS | Pug | Handlebars |
|---|---|---|---|
| 语法风格 | HTML + JS 标签 | 缩进式,无闭合标签 | Mustache {{}} |
| 学习曲线 | 最低 | 中等 | 低 |
| 逻辑能力 | 完整 JS | 完整 JS | 受限(无任意 JS) |
| 布局/继承 | 需中间件 | 原生支持 | 需额外配置 |
| 输出转义 | <%= %> |
= 或 #{} |
{{}} |
| 原始输出 | <%- %> |
!{} |
{{{}}} |
| 适用场景 | 快速上手 / 内部工具 | 简洁优先 / 全栈项目 | 安全优先 / 多端渲染 |
| 社区热度 | 高 | 高 | 中 |
8. 8 SSR vs CSR 对比
(1) 核心差异表
| 维度 | SSR(服务端渲染) | CSR(客户端渲染) |
|---|---|---|
| 渲染位置 | 服务器 | 浏览器 |
| 首屏速度 | 快(HTML 直出) | 慢(需等 JS 加载执行) |
| SEO 友好度 | 高 | 低(爬虫可能看不到内容) |
| 交互复杂度 | 低(每次请求刷新页面) | 高(SPA 无刷新) |
| 服务器压力 | 较高 | 较低 |
| 典型技术 | EJS / Pug / PHP | React / Vue / Angular |
| 适用场景 | 内部工具 / 博客 / SEO 页面 | 复杂交互 / SPA 应用 |
▶ 示例:SSR 与 CSR 的响应差异
SSR 响应——浏览器直接拿到完整 HTML:
TEXT
📖 仅展示
GET /about
→ 服务器渲染 HTML 完整返回
→ 浏览器直接展示,无需等 JS
CSR 响应——浏览器拿到空壳 + JS bundle:
TEXT
📖 仅展示
GET /about
→ 服务器返回 <div id="app"></div> + bundle.js
→ 浏览器下载 JS,执行渲染
→ 用户看到内容(有白屏时间)
9. 9 综合示例:EJS 博客页面
构建一个完整的 Express + EJS 博客应用,包含布局复用、文章列表、文章详情和局部导航。
▶ 示例:(1) 项目结构
TEXT
📖 仅展示
blog/
views/
partials/
header.ejs
footer.ejs
nav.ejs
index.ejs
post.ejs
app.js
▶ 示例:应用入口 app.js
JAVASCRIPT
const express = require('express');
const expressLayouts = require('express-ejs-layouts');
const app = express();
app.set('view engine', 'ejs');
app.set('views', './views');
app.use(expressLayouts);
app.set('layout', 'layout');
const posts = [
{ id: 1, title: 'Getting Started with Node.js', author: 'Charlie', date: '2026-06-20', excerpt: 'Learn the basics of Node.js runtime.' },
{ id: 2, title: 'Understanding Express Middleware', author: 'Alice', date: '2026-06-25', excerpt: 'Deep dive into middleware patterns.' },
{ id: 3, title: 'Template Engines Compared', author: 'Bob', date: '2026-07-01', excerpt: 'EJS vs Pug vs Handlebars showdown.' }
];
app.get('/', (req, res) => {
res.render('index', { title: 'Blog Home', posts: posts });
});
app.get('/post/:id', (req, res) => {
const post = posts.find(p => p.id === parseInt(req.params.id));
if (!post) return res.status(404).send('Post not found');
res.render('post', { title: post.title, post: post });
});
app.listen(3000, () => {
console.log('Blog running on http://localhost:3000');
});
▶ 示例:局部导航 partials/nav.ejs
JAVASCRIPT
<nav class="blog-nav">
<a href="/">All Posts</a>
<% posts.forEach(function(p) { %>
<a href="/post/<%= p.id %>"><%= p.title %></a>
<% }); %>
</nav>
▶ 示例:局部头部 partials/header.ejs
JAVASCRIPT
<header class="site-header">
<h1><%= title %></h1>
</header>
▶ 示例:局部底部 partials/footer.ejs
JAVASCRIPT
<footer class="site-footer">
<p>© 2026 My Blog. Powered by Express + EJS.</p>
</footer>
▶ 示例:文章列表页 views/index.ejs
JAVASCRIPT
<h2>Latest Posts</h2>
<div class="post-list">
<% posts.forEach(function(post) { %>
<article class="post-card">
<h3><a href="/post/<%= post.id %>"><%= post.title %></a></h3>
<p class="meta">By <%= post.author %> on <%= post.date %></p>
<p><%= post.excerpt %></p>
</article>
<% }); %>
</div>
▶ 示例:文章详情页 views/post.ejs
JAVASCRIPT
<h2><%= post.title %></h2>
<p class="meta">By <%= post.author %> on <%= post.date %></p>
<div class="post-body">
<p><%= post.excerpt %></p>
<p>Full article content goes here...</p>
</div>
<a href="/">← Back to all posts</a>
启动应用后访问效果:
TEXT
📖 仅展示
http://localhost:3000 → 文章列表页
http://localhost:3000/post/1 → 文章详情页
http://localhost:3000/post/2 → 第二篇文章
❓ 常见问题
Q 现在还需要 SSR 吗?
A 内部管理工具、SEO 需求高的页面(博客、电商商品页)、内容优先的简单站点仍然非常适合 SSR,无需引入前端框架的复杂度。
Q EJS 和 Pug 堿个更好?
A EJS 更接近原生 HTML,上手零门槛,适合快速开发;Pug 语法更简洁但需要适应缩进规则,适合追求代码简洁的团队。初学者推荐 EJS。
Q 如何在模板中复用布局?
A EJS 使用
express-ejs-layouts 中间件实现布局继承,或手动用 <%- include() %> 引入局部视图;Pug 原生支持 extends + block 实现布局继承。Q res.render 的第二个参数是什么?
A 它是一个数据对象,其属性在模板中可直接作为变量使用,如
res.render('index', {title: 'Hi'}),模板中写 <%= title %>。Q 模板引擎支持热重载吗?
A 开发时可用 nodemon 监听文件变化自动重启服务,也可结合
livereload 中间件实现浏览器自动刷新,无需手动刷新页面。Q <%- 和 <%= 有什么区别?
A
<%= 输出经过 HTML 转义的值,防止 XSS 攻击,是安全选择;<%- 输出原始内容不转义,仅在信任内容或引入子模板时使用。Q 模板中可以使用 async/await 吗?
A EJS 模板内不支持顶层 await,应在路由中完成所有异步数据获取,再通过
res.render() 将结果传给模板。📖 小节
- 1 故事:Charlie 的管理工具界面的核心概念与使用方法
- 2 模板渲染流程的核心概念与使用方法
- 3 EJS 基础语法的核心概念与使用方法
- 4 Express + EJS 配置的核心概念与使用方法
- 5 Pug 基础语法的核心概念与使用方法
- 6 局部视图与布局复用的核心概念与使用方法
- 7 模板引擎对比的核心概念与使用方法
- 8 SSR vs CSR 对比的核心概念与使用方法
📝 作业
- 完成本课所有代码示例,确保每个示例都能正确运行
- 修改综合示例,添加自己的扩展功能
- 查阅官方文档,找出本课未涉及的1-2个API并编写测试代码
- 思考:在实际项目中,你会如何应用本课学到的知识?
- 尝试将本课知识与前面课程的内容结合,构建一个小项目