Template engine integration and view rendering
The Role and Selection of Template Engines
Template engines in Express are responsible for dynamically generating HTML content, separating data from views. They allow developers to embed variables, logic, and control structures in HTML, ultimately rendering complete pages. Express supports multiple template engines, each with its own syntax characteristics and applicable scenarios.
Common template engines include:
- EJS: Embedded JavaScript, with syntax close to native HTML
- Pug (formerly Jade): Indentation-based syntax, concise but with a steeper learning curve
- Handlebars: Mustache-style templates, emphasizing the separation of logic and presentation
- Nunjucks: Inspired by Jinja2, feature-rich and flexible
// Example of installing EJS
npm install ejs
Configuring Template Engines in Express
Configuring a template engine requires setting the view engine and view directory. Express uses the app.set()
method to complete these configurations, ensuring template files can be correctly located and rendered.
const express = require('express');
const app = express();
// Set the view engine to EJS
app.set('view engine', 'ejs');
// Set the view directory (defaults to the 'views' folder in the project root)
app.set('views', path.join(__dirname, 'views'));
For the Pug engine, the configuration is similar but the syntax differs:
app.set('view engine', 'pug');
Basic Rendering and Data Passing
The res.render()
method is the core of Express view rendering. It takes a template filename and a data object as parameters. The properties of the data object can be directly accessed in the template.
app.get('/', (req, res) => {
res.render('index', {
title: 'Homepage',
user: {
name: 'John Doe',
age: 28
},
items: ['Apple', 'Banana', 'Orange']
});
});
The corresponding EJS template file views/index.ejs
might look like this:
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
</head>
<body>
<h1>Welcome, <%= user.name %></h1>
<ul>
<% items.forEach(item => { %>
<li><%= item %></li>
<% }); %>
</ul>
</body>
</html>
Template Inheritance and Layouts
Most template engines support layout or inheritance features to avoid code duplication. For example, in EJS, partial views can be included using include
:
<!-- views/header.ejs -->
<header>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>
<!-- views/index.ejs -->
<%- include('header') %>
<main>
<!-- Page content -->
</main>
Pug uses extends
and block
for a more powerful layout system:
//- views/layout.pug
html
head
title My Website
body
block content
//- views/index.pug
extends layout
block content
h1 Welcome Page
p This is the homepage content
Conditional Rendering and Loops
Template engines support basic control structures. In EJS, JavaScript syntax is used for conditionals and loops:
<% if (user.isAdmin) { %>
<button class="admin-control">Admin Panel</button>
<% } %>
<ul>
<% posts.forEach(post => { %>
<li>
<h3><%= post.title %></h3>
<p><%= post.content %></p>
</li>
<% }) %>
</ul>
Handlebars uses {{#if}}
and {{#each}}
helpers for similar functionality:
{{#if user.isAdmin}}
<button class="admin-control">Admin Panel</button>
{{/if}}
<ul>
{{#each posts}}
<li>
<h3>{{this.title}}</h3>
<p>{{this.content}}</p>
</li>
{{/each}}
</ul>
Custom Helper Functions
Advanced scenarios may require extending template engine functionality. For example, in Nunjucks, custom filters can be registered:
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);
});
Using the filter in a template:
<p>{{ username|shorten(8) }}</p>
Performance Optimization Techniques
View rendering can become a performance bottleneck, especially in high-traffic scenarios. The following optimization strategies are worth considering:
- Template Caching: Enable caching in production
app.set('view cache', true);
- Precompiling Templates: Some engines support precompilation
pug --client --no-debug views/*.pug
-
Reduce Complex Logic: Move business logic out of templates
-
Use Partial Views: Break large templates into smaller components
// Express 4.x+ partial view rendering
app.get('/sidebar', (req, res) => {
res.render('partials/sidebar', { links: [...] });
});
Error Handling and Debugging
Template rendering errors need proper handling. Express provides error-handling middleware:
app.use((err, req, res, next) => {
if (err.view) {
// Specifically handle view errors
res.status(500).render('error', { error: err });
} else {
next(err);
}
});
When debugging templates, enable detailed errors:
// EJS configuration example
app.set('view options', {
compileDebug: true,
debug: true
});
Integration with Frontend Frameworks
Modern development often requires combining Express template engines with frontend frameworks. A common pattern is to provide initial state:
app.get('/', (req, res) => {
const initialState = {
user: req.user,
settings: getSettings()
};
res.render('index', {
initialState: JSON.stringify(initialState)
});
});
Embed this state in the template for frontend use:
<script>
window.__INITIAL_STATE__ = <%- initialState %>;
</script>
Security Considerations
Template rendering involves security risks, particularly XSS attack prevention:
- Auto-escaping: Enabled by default in most engines
// Disable auto-escaping (not recommended)
app.set('view options', { autoescape: false });
-
Use Raw Output Cautiously: In EJS,
<%-
is more dangerous than<%=
-
Sanitize User Input: Always validate and sanitize data passed to templates
const sanitizeHtml = require('sanitize-html');
res.render('profile', {
bio: sanitizeHtml(userProvidedBio)
});
Advanced Data Preprocessing
Sometimes data needs transformation before rendering. Express middleware is suitable for this scenario:
app.use('/products', (req, res, next) => {
Product.fetchAll().then(products => {
res.locals.categories = groupByCategory(products);
next();
});
});
app.get('/products', (req, res) => {
// Data in res.locals is automatically available to all templates
res.render('products');
});
Mixing Multiple Engines
Large projects may require mixing multiple template engines. The consolidate
library can help:
const consolidate = require('consolidate');
app.engine('hbs', consolidate.handlebars);
app.engine('pug', consolidate.pug);
app.get('/hybrid', (req, res) => {
// Choose different engines based on conditions
const usePug = req.query.format === 'pug';
res.render(usePug ? 'template.pug' : 'template.hbs', data);
});
Dynamic Template Selection
Technique for dynamically selecting templates based on request characteristics:
app.get('/profile', (req, res) => {
const template = req.device.type === 'desktop'
? 'profile-desktop'
: 'profile-mobile';
res.render(template, { user: req.user });
});
Testing View Rendering
View testing requires special consideration. Example using Supertest and Jest:
const request = require('supertest');
const app = require('../app');
describe('View Tests', () => {
it('should render the homepage', async () => {
const res = await request(app)
.get('/')
.expect('Content-Type', /html/)
.expect(200);
expect(res.text).toContain('<title>Homepage</title>');
});
});
Internationalization Support
Common implementation for multilingual template rendering:
app.use((req, res, next) => {
// Determine language based on request
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]
});
});
Usage in templates:
<h1><%= t.welcome %></h1>
Streaming Rendering
For very large pages, streaming rendering can improve TTFB:
const { Readable } = require('stream');
app.get('/large-data', (req, res) => {
const stream = new Readable({
read() {}
});
res.type('html');
stream.pipe(res);
// Render in chunks
stream.push('<html><head><title>Big Data</title></head><body><ul>');
dataStream.on('data', chunk => {
stream.push(`<li>${chunk}</li>`);
});
dataStream.on('end', () => {
stream.push('</ul></body></html>');
stream.push(null);
});
});
Combining with WebSocket Real-time Updates
Template engines can integrate with real-time technologies:
// Initialize WebSocket
wss.on('connection', ws => {
// Re-render partial views when data changes
db.on('update', async data => {
const html = await renderPartial('updates', { data });
ws.send(JSON.stringify({ type: 'update', html }));
});
});
Client-side handling:
socket.onmessage = event => {
const msg = JSON.parse(event.data);
if (msg.type === 'update') {
document.getElementById('updates').innerHTML = msg.html;
}
};
Static Asset Handling
Proper handling of static assets in templates:
app.use(express.static('public'));
// Use correct paths in templates
<link rel="stylesheet" href="/css/style.css">
For CDN or dynamic assets:
res.locals.assetPath = (file) => {
return process.env.NODE_ENV === 'production'
? `https://cdn.example.com/${file}`
: `/static/${file}`;
};
In templates:
<link rel="stylesheet" href="<%= assetPath('css/style.css') %>">
Environment-Specific Rendering
Adjust output based on environment variables:
app.use((req, res, next) => {
res.locals.isProduction = process.env.NODE_ENV === 'production';
next();
});
Conditional debug output in templates:
<% if (!isProduction) { %>
<div class="debug-info">
Current User: <%= user.id %>
Request Time: <%= responseTime %>ms
</div>
<% } %>
Custom Response Formats
Beyond HTML, template engines can generate other formats:
app.get('/report.csv', (req, res) => {
res.type('csv');
res.render('data-csv', { data }, (err, csv) => {
if (err) return next(err);
res.send(csv);
});
});
Corresponding CSV template:
id,name,value
<% data.forEach(row => { %>
<%= row.id %>,<%= row.name %>,<%= row.value %>
<% }); %>
View Model Pattern
Introduce a view model layer to transform data:
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));
});
Cache Strategy Implementation
Implement simple view caching:
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);
});
});
Performance Monitoring
Add rendering time monitoring:
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
metrics.trackRenderTime(req.path, duration);
});
next();
});
Progressive Enhancement Support
Serve both traditional and multi-page applications:
app.get('/products', (req, res) => {
const data = fetchProducts();
if (req.accepts('json')) {
res.json(data);
} else {
res.render('products', { products: data });
}
});
View Component System
Build reusable view components:
// components/button.js
module.exports = (text, type = 'default') => `
<button class="btn btn-${type}">${text}</button>
`;
// Usage in templates
const button = require('../components/button');
app.get('/', (req, res) => {
res.render('index', {
primaryButton: button('Primary Button', 'primary')
});
});
Combining Server-side and Client-side Rendering
Modern approach to hybrid rendering:
app.get('/hybrid', (req, res) => {
const initialData = fetchInitialData();
res.render('hybrid', {
ssrContent: renderComponent('Widget', { data: initialData }),
initialData: JSON.stringify(initialData)
});
});
Client-side hydration code:
hydrateComponent('Widget', {
node: document.getElementById('widget'),
props: window.__INITIAL_DATA__
});
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:请求与响应对象详解
下一篇:静态文件服务与资源托管