模板引擎集成与视图渲染
模板引擎的作用与选择
模板引擎在Express中负责动态生成HTML内容,将数据与视图分离。它允许开发者在HTML中嵌入变量、逻辑和控制结构,最终渲染出完整的页面。Express支持多种模板引擎,每种引擎有各自的语法特点和适用场景。
常见的模板引擎包括:
- EJS:嵌入式JavaScript,语法接近原生HTML
- Pug(原名Jade):缩进式语法,简洁但学习曲线较陡
- Handlebars:Mustache风格的模板,强调逻辑与表现分离
- Nunjucks:受Jinja2启发,功能丰富且灵活
// 安装EJS示例
npm install ejs
Express中配置模板引擎
配置模板引擎需要设置视图引擎和视图目录。Express通过app.set()
方法完成这些配置,确保模板文件能够被正确找到和渲染。
const express = require('express');
const app = express();
// 设置视图引擎为EJS
app.set('view engine', 'ejs');
// 设置视图目录(默认为项目根目录下的views文件夹)
app.set('views', path.join(__dirname, 'views'));
对于Pug引擎,配置方式类似但语法不同:
app.set('view engine', 'pug');
基本渲染与数据传递
res.render()
方法是Express视图渲染的核心,它接受模板文件名和数据对象作为参数。数据对象中的属性可以在模板中直接访问。
app.get('/', (req, res) => {
res.render('index', {
title: '首页',
user: {
name: '张三',
age: 28
},
items: ['苹果', '香蕉', '橙子']
});
});
对应的EJS模板文件views/index.ejs
可能如下:
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
</head>
<body>
<h1>欢迎, <%= user.name %></h1>
<ul>
<% items.forEach(item => { %>
<li><%= item %></li>
<% }); %>
</ul>
</body>
</html>
模板继承与布局
大多数模板引擎支持布局或继承功能,避免重复代码。以EJS为例,可以通过include
引入部分视图:
<!-- views/header.ejs -->
<header>
<nav>
<a href="/">首页</a>
<a href="/about">关于</a>
</nav>
</header>
<!-- views/index.ejs -->
<%- include('header') %>
<main>
<!-- 页面内容 -->
</main>
Pug则使用extends
和block
实现更强大的布局系统:
//- views/layout.pug
html
head
title 我的网站
body
block content
//- views/index.pug
extends layout
block content
h1 欢迎页面
p 这里是首页内容
条件渲染与循环
模板引擎都支持基本的控制结构。在EJS中,使用JavaScript语法进行条件判断和循环:
<% if (user.isAdmin) { %>
<button class="admin-control">管理面板</button>
<% } %>
<ul>
<% posts.forEach(post => { %>
<li>
<h3><%= post.title %></h3>
<p><%= post.content %></p>
</li>
<% }) %>
</ul>
Handlebars使用{{#if}}
和{{#each}}
助手实现类似功能:
{{#if user.isAdmin}}
<button class="admin-control">管理面板</button>
{{/if}}
<ul>
{{#each posts}}
<li>
<h3>{{this.title}}</h3>
<p>{{this.content}}</p>
</li>
{{/each}}
</ul>
自定义辅助函数
高级场景下可能需要扩展模板引擎功能。以Nunjucks为例,可以注册自定义过滤器:
const nunjucks = require('nunjucks');
const env = nunjucks.configure('views', {
express: app,
autoescape: true
});
env.addFilter('shorten', function(str, count) {
return str.slice(0, count || 5);
});
模板中使用这个过滤器:
<p>{{ username|shorten(8) }}</p>
性能优化技巧
视图渲染可能成为性能瓶颈,特别是在高流量场景下。以下优化策略值得考虑:
- 模板缓存:生产环境下启用缓存
app.set('view cache', true);
- 预编译模板:某些引擎支持预编译
pug --client --no-debug views/*.pug
-
减少复杂逻辑:将业务逻辑移出模板
-
使用局部视图:拆分大模板为小组件
// Express 4.x+ 局部视图渲染
app.get('/sidebar', (req, res) => {
res.render('partials/sidebar', { links: [...] });
});
错误处理与调试
模板渲染错误需要适当处理。Express提供了错误处理中间件:
app.use((err, req, res, next) => {
if (err.view) {
// 专门处理视图错误
res.status(500).render('error', { error: err });
} else {
next(err);
}
});
调试模板时,可以启用详细错误:
// EJS配置示例
app.set('view options', {
compileDebug: true,
debug: true
});
与前端框架集成
现代开发常需要将Express模板引擎与前端框架结合。一种常见模式是提供初始状态:
app.get('/', (req, res) => {
const initialState = {
user: req.user,
settings: getSettings()
};
res.render('index', {
initialState: JSON.stringify(initialState)
});
});
模板中嵌入这个状态供前端使用:
<script>
window.__INITIAL_STATE__ = <%- initialState %>;
</script>
安全注意事项
模板渲染涉及安全风险,特别是XSS攻击防护:
- 自动转义:大多数引擎默认开启
// 禁用自动转义(不推荐)
app.set('view options', { autoescape: false });
-
谨慎使用原始输出:EJS中
<%-
比<%=
更危险 -
清理用户输入:始终验证和清理传入模板的数据
const sanitizeHtml = require('sanitize-html');
res.render('profile', {
bio: sanitizeHtml(userProvidedBio)
});
高级数据预处理
有时需要在渲染前对数据进行转换。Express中间件适合这种场景:
app.use('/products', (req, res, next) => {
Product.fetchAll().then(products => {
res.locals.categories = groupByCategory(products);
next();
});
});
app.get('/products', (req, res) => {
// res.locals中的数据自动对所有模板可用
res.render('products');
});
多引擎混合使用
大型项目可能需要混合多种模板引擎。通过consolidate
库可以实现:
const consolidate = require('consolidate');
app.engine('hbs', consolidate.handlebars);
app.engine('pug', consolidate.pug);
app.get('/hybrid', (req, res) => {
// 根据条件选择不同引擎
const usePug = req.query.format === 'pug';
res.render(usePug ? 'template.pug' : 'template.hbs', data);
});
动态模板选择
基于请求特征动态选择模板的技术:
app.get('/profile', (req, res) => {
const template = req.device.type === 'desktop'
? 'profile-desktop'
: 'profile-mobile';
res.render(template, { user: req.user });
});
测试视图渲染
视图测试需要特殊考虑。使用Supertest和Jest的测试示例:
const request = require('supertest');
const app = require('../app');
describe('视图测试', () => {
it('应渲染主页', async () => {
const res = await request(app)
.get('/')
.expect('Content-Type', /html/)
.expect(200);
expect(res.text).toContain('<title>首页</title>');
});
});
国际化支持
多语言模板渲染的常见实现:
app.use((req, res, next) => {
// 根据请求确定语言
res.locals.lang = req.acceptsLanguages('en', 'zh') || 'en';
next();
});
app.get('/', (req, res) => {
const messages = {
en: { welcome: 'Welcome' },
zh: { welcome: '欢迎' }
};
res.render('index', {
t: messages[res.locals.lang]
});
});
模板中使用:
<h1><%= t.welcome %></h1>
流式渲染
对于超大页面,流式渲染可以提高TTFB:
const { Readable } = require('stream');
app.get('/large-data', (req, res) => {
const stream = new Readable({
read() {}
});
res.type('html');
stream.pipe(res);
// 分块渲染
stream.push('<html><head><title>大数据</title></head><body><ul>');
dataStream.on('data', chunk => {
stream.push(`<li>${chunk}</li>`);
});
dataStream.on('end', () => {
stream.push('</ul></body></html>');
stream.push(null);
});
});
与WebSocket实时更新结合
模板引擎可以与实时技术结合:
// 初始化WebSocket
wss.on('connection', ws => {
// 当数据变化时重新渲染部分视图
db.on('update', async data => {
const html = await renderPartial('updates', { data });
ws.send(JSON.stringify({ type: 'update', html }));
});
});
客户端处理:
socket.onmessage = event => {
const msg = JSON.parse(event.data);
if (msg.type === 'update') {
document.getElementById('updates').innerHTML = msg.html;
}
};
静态资源处理
正确处理模板中的静态资源:
app.use(express.static('public'));
// 模板中使用正确的路径
<link rel="stylesheet" href="/css/style.css">
对于CDN或动态资源:
res.locals.assetPath = (file) => {
return process.env.NODE_ENV === 'production'
? `https://cdn.example.com/${file}`
: `/static/${file}`;
};
模板中:
<link rel="stylesheet" href="<%= assetPath('css/style.css') %>">
环境特定渲染
根据环境变量调整输出:
app.use((req, res, next) => {
res.locals.isProduction = process.env.NODE_ENV === 'production';
next();
});
模板中条件输出调试信息:
<% if (!isProduction) { %>
<div class="debug-info">
当前用户: <%= user.id %>
请求耗时: <%= responseTime %>ms
</div>
<% } %>
自定义响应格式
除了HTML,模板引擎可以生成其他格式:
app.get('/report.csv', (req, res) => {
res.type('csv');
res.render('data-csv', { data }, (err, csv) => {
if (err) return next(err);
res.send(csv);
});
});
对应的CSV模板:
id,name,value
<% data.forEach(row => { %>
<%= row.id %>,<%= row.name %>,<%= row.value %>
<% }); %>
视图模型模式
引入视图模型层转换数据:
class ProductViewModel {
constructor(product) {
this.name = product.name;
this.price = `$${product.price.toFixed(2)}`;
this.inStock = product.quantity > 0;
}
}
app.get('/product/:id', (req, res) => {
const product = getProduct(req.params.id);
res.render('product', new ProductViewModel(product));
});
缓存策略实现
实现简单的视图缓存:
const viewCache = new Map();
app.get('/cached-page', (req, res) => {
const cacheKey = req.originalUrl;
if (viewCache.has(cacheKey)) {
return res.send(viewCache.get(cacheKey));
}
res.render('page', (err, html) => {
if (err) return next(err);
viewCache.set(cacheKey, html);
res.send(html);
});
});
性能监控
添加渲染时间监控:
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
metrics.trackRenderTime(req.path, duration);
});
next();
});
渐进式增强支持
同时服务传统和多页应用:
app.get('/products', (req, res) => {
const data = fetchProducts();
if (req.accepts('json')) {
res.json(data);
} else {
res.render('products', { products: data });
}
});
视图组件系统
构建可复用的视图组件:
// components/button.js
module.exports = (text, type = 'default') => `
<button class="btn btn-${type}">${text}</button>
`;
// 在模板中使用
const button = require('../components/button');
app.get('/', (req, res) => {
res.render('index', {
primaryButton: button('主要按钮', 'primary')
});
});
服务端与客户端渲染结合
混合渲染的现代方法:
app.get('/hybrid', (req, res) => {
const initialData = fetchInitialData();
res.render('hybrid', {
ssrContent: renderComponent('Widget', { data: initialData }),
initialData: JSON.stringify(initialData)
});
});
客户端激活代码:
hydrateComponent('Widget', {
node: document.getElementById('widget'),
props: window.__INITIAL_DATA__
});
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:请求与响应对象详解
下一篇:静态文件服务与资源托管