Node.js: 模板引擎与 SSR

最后更新:2026-08-26

1. 1 故事:Charlie 的管理工具界面

Charlie 是一家初创公司的后端开发者,团队需要一个内部管理工具来查看订单、用户统计和系统状态。同事们建议用 React 或 Vue,但 Charlie 觉得对于这种内部工具太重了——页面不多、交互简单,用模板引擎在服务端渲染就够了。他选择了 EJS,因为它几乎就是 HTML 加上几个标签,上手零门槛。一天之内,他就用 Express + EJS 搭出了带布局复用的完整管理页面。

你将学到



2. 2 模板渲染流程

模板引擎的核心工作是将模板文件数据合并,生成最终的 HTML 字符串返回给浏览器。

100%
flowchart LR
    A[浏览器请求] --> B[Express 路由]
    B --> C[获取数据]
    C --> D["res.render()"]
    D --> E[EJS / Pug 编译]
    E --> F[生成 HTML]
    F --> G[HTTP 响应]

整个流程可以概括为:

  1. 浏览器发起 HTTP 请求
  2. Express 路由匹配到对应处理函数
  3. 处理函数从数据库或 API 获取数据
  4. 调用 res.render() 传入模板名和数据
  5. 模板引擎将模板 + 数据编译为 HTML
  6. 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>&copy; 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>
▶ 试一试
JAVASCRIPT
<footer class="site-footer">
  <p>&copy; 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="/">&larr; 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. 完成本课所有代码示例,确保每个示例都能正确运行
  2. 修改综合示例,添加自己的扩展功能
  3. 查阅官方文档,找出本课未涉及的1-2个API并编写测试代码
  4. 思考:在实际项目中,你会如何应用本课学到的知识?
  5. 尝试将本课知识与前面课程的内容结合,构建一个小项目
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏