模板引擎与静态文件
本教程共 76 篇 · 第 41 篇 · 更新于 2026-07-25 · 约 5 分钟阅读
41. 模板引擎与静态文件
本节目标:EJS 模板渲染、express.static 静态服务和 multer 文件上传。
不是每个接口都要返回 JSON。管理后台、落地页、邮件模板,这些场景需要服务端渲染 HTML。Express 支持多种模板引擎,配合 express.static 托管静态资源,能搭出完整的前后端不分离项目。
这一节我们讲 EJS(Embedded JavaScript),它语法简单、与 Express 集成最成熟。同时也讲静态文件托管和文件上传,这三件事构成了传统 Web 页面的基础能力。
配置模板引擎
先安装 EJS:
npm install ejs
Express 不需要显式 require('ejs'),只需在 app.js 里指定视图目录和引擎:
import express from 'express'
import path from 'path'
const app = express()
app.set('views', path.join(process.cwd(), 'views'))
app.set('view engine', 'ejs')
然后在 views/ 目录下放模板文件,扩展名是 .ejs。写一个简单的首页:
views/index.ejs:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title><%= title %></title>
</head>
<body>
<h1><%= title %></h1>
<p><%= message %></p>
<ul>
<% items.forEach(item => { %>
<li><%= item %></li>
<% }) %>
</ul>
</body>
</html>
路由里用 res.render() 渲染:
app.get('/', (req, res) => {
res.render('index', {
title: 'Express + EJS',
message: 'Hello from server-side rendering',
items: ['Node.js', 'Express', 'EJS']
})
})
res.render('index', data) 会做三件事:找到 views/index.ejs,把数据填进去,将生成的 HTML 作为响应发送。Content-Type 自动设为 text/html。
EJS 语法速查
EJS 标签有三种,别搞混:
| 标签 | 作用 | 示例 |
|---|---|---|
<%= value %> | 输出值(HTML 转义) | <%= user.name %> |
<%- value %> | 输出原始 HTML(不转义) | <%- htmlContent %> |
<% code %> | 执行 JS 语句,不输出 | <% if (x) { %> |
Warning
<%- %>不转义 HTML,如果输出的是用户输入的内容,会导致 XSS 漏洞。只有当你确实要插入可信的 HTML(比如富文本编辑器生成的内容)时才用它,否则一律用<%= %>。
条件渲染和循环都是普通 JavaScript:
<% if (user) { %>
<p>Welcome, <%= user.name %></p>
<% } else { %>
<p>Please <a href="/login">login</a></p>
<% } %>
<% products.forEach(p => { %>
<div class="product">
<h3><%= p.name %></h3>
<span>¥<%= p.price %></span>
</div>
<% }) %>
EJS 支持局部模板(partials),用来抽取公共片段:
<!-- views/partials/head.ejs -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/css/style.css">
在页面里引入:
<head>
<title><%= title %></title>
<%- include('partials/head') %>
</head>
静态文件托管
CSS、JS、图片这些不常变的资源,用 express.static 直接映射到目录:
app.use(express.static('public'))
假设目录结构:
public/
├── css/
│ └── style.css
├── js/
│ └── app.js
└── images/
└── logo.png
前端就可以通过 /css/style.css、/images/logo.png 访问。注意路径里不带 public,express.static 已经把虚拟根目录指向了 public/ 内部。
如果你想加个前缀,比如所有静态资源走 /assets:
app.use('/assets', express.static('public'))
此时访问路径变成 /assets/css/style.css。生产环境建议用绝对路径:
app.use('/assets', express.static(path.join(process.cwd(), 'public')))
Tip
express.static内部会根据文件扩展名自动推断 Content-Type,并且默认开启缓存。如果要调整缓存策略,可以传选项:
app.use(express.static('public', { maxAge: '1d' }))。
文件上传:multer
表单上传文件时,enctype 必须是 multipart/form-data,这种格式 express.json() 和 express.urlencoded() 解析不了,需要专门的中间件。最常用的是 multer。
npm install multer
基础用法:
import multer from 'multer'
const upload = multer({ dest: 'uploads/' })
// 单文件上传,字段名 "avatar"
app.post('/upload', upload.single('avatar'), (req, res) => {
console.log(req.file) // 文件信息
res.json({
filename: req.file.originalname,
size: req.file.size,
path: req.file.path
})
})
req.file 的结构:
{
fieldname: 'avatar',
originalname: 'photo.jpg',
encoding: '7bit',
mimetype: 'image/jpeg',
destination: 'uploads/',
filename: 'a1b2c3d4e5f6...',
path: 'uploads/a1b2c3d4e5f6...',
size: 12345
}
multer 默认把文件名改成随机哈希,防止重名覆盖。如果你希望保留原始文件名,自定义存储引擎:
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/')
},
filename: (req, file, cb) => {
const unique = Date.now() + '-' + Math.round(Math.random() * 1e9)
cb(null, unique + '-' + file.originalname)
}
})
const upload = multer({ storage })
多文件上传用 upload.array('photos', 5),最多允许 5 张。混合字段用 upload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'gallery', maxCount: 8 }])。
Warning上传功能最容易出安全问题。
multer不会校验文件类型,只信任前端传来的mimetype,而mimetype可以被伪造。生产环境务必做后缀白名单校验,并且不要把上传目录放在public下直接暴露给用户访问。
模板引擎与前后端分离
EJS 这类服务端渲染适合管理后台、SEO 页面、邮件模板。如果是重交互的 C 端应用,前后端分离(React/Vue 做前端,Express 纯当 API 网关)是更主流的选择。
Express 并不强制你用哪种方案。需要 SSR 就配 EJS/Pug,需要 API 就 res.json(),两者甚至可以共存——同一个项目里,管理后台走模板渲染,App 接口走 JSON。