阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 模板引擎集成与视图渲染

模板引擎集成与视图渲染

作者:陈川 阅读数:44875人阅读 分类: Node.js

模板引擎的作用与选择

模板引擎在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则使用extendsblock实现更强大的布局系统:

//- 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>

性能优化技巧

视图渲染可能成为性能瓶颈,特别是在高流量场景下。以下优化策略值得考虑:

  1. 模板缓存:生产环境下启用缓存
app.set('view cache', true);
  1. 预编译模板:某些引擎支持预编译
pug --client --no-debug views/*.pug
  1. 减少复杂逻辑:将业务逻辑移出模板

  2. 使用局部视图:拆分大模板为小组件

// 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攻击防护:

  1. 自动转义:大多数引擎默认开启
// 禁用自动转义(不推荐)
app.set('view options', { autoescape: false });
  1. 谨慎使用原始输出:EJS中<%-<%=更危险

  2. 清理用户输入:始终验证和清理传入模板的数据

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

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌