Node.js EJS模板引擎全解析:从语法基础到动态视图渲染实践

文章目录

Node.js EJS模板引擎全解析:从语法基础到动态视图渲染实践

EJS(Embedded JavaScript)是一款轻量、灵活的模板引擎,以“在HTML中嵌入JavaScript”为核心设计理念,成为Node.js生态中构建动态网页的热门选择。与Pug等缩进式模板引擎不同,EJS保留了完整的HTML结构,仅通过简单标签嵌入动态逻辑,降低了前端开发者的学习门槛。本文系统梳理EJS的核心功能、语法规则、实战案例、最佳实践及注意事项,帮助开发者快速掌握这一“类HTML”模板引擎的使用。

一、EJS模板引擎的核心价值与特点

EJS的设计哲学是“简单至上”,通过最小化的语法扩展实现HTML与JavaScript的无缝融合,其核心优势在于:

  • 贴近HTML:保留完整HTML语法结构,仅通过<% %>系列标签嵌入动态逻辑,前端开发者可快速上手。
  • JavaScript原生支持:模板中可直接编写任意JavaScript代码(变量、条件、循环等),无需学习新语法。
  • 灵活无依赖:不强制绑定特定框架,可独立使用或与Express等Node.js框架集成。
  • 高效渲染:编译为原生JavaScript函数执行,性能优异,支持模板缓存。
  • 扩展性强:支持自定义标签、过滤器和模板包含(include),满足复杂场景需求。

适用场景:动态网页渲染(如博客、电商页面)、邮件模板生成、静态站点生成等需要将后端数据注入HTML的场景,尤其适合熟悉HTML和JavaScript的开发者。

二、EJS的安装与基础配置

1. 环境准备

  • 依赖Node.js(v12+推荐)和npm/yarn包管理器。
  • 支持独立使用或与Express、Koa等Web框架集成(本文以Express为例)。

2. 安装EJS

# 局部安装(项目依赖)
npm install ejs --save

# 与Express结合时
npm install express ejs --save

3. 与Express框架集成

在Express中配置EJS作为视图引擎:

// app.js
const express = require('express');
const app = express();

// 配置视图引擎为EJS
app.set('view engine', 'ejs');
// 配置视图文件目录(默认是项目根目录下的views文件夹)
app.set('views', './views');

// 定义路由,渲染EJS模板
app.get('/', (req, res) => {
  // 向模板传递数据(第二个参数为模板变量)
  res.render('index', {
    title: 'EJS模板示例',
    message: '欢迎使用EJS模板引擎',
    isLogin: true,
    user: { name: 'Alice' }
  });
});

app.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
});

三、EJS核心语法与功能

EJS通过<% %>系列标签区分静态HTML和动态JavaScript代码,核心标签如下:

标签语法功能描述示例
<%= 变量 %>输出变量值(自动转义HTML,防XSS)<%= user.name %>
<%- 变量 %>输出变量值(不转义HTML,用于可信内容)<%- htmlContent %>
<% 代码 %>执行JavaScript代码(无输出)<% if (isLogin) { %>
<%# 注释 %>EJS注释(不会输出到HTML)<%# 这是一条EJS注释 %>
-%>修剪标签后的换行(减少空白输出)<% for (let i=0; i<5; i++) { -%>

1. 变量输出与转义

  • 转义输出(推荐):使用<%= %>,自动将HTML特殊字符(如<>)转为实体编码,防止XSS攻击。
  • 非转义输出:使用<%- %>,直接输出原始HTML(仅用于完全可信的内容)。

示例:变量输出

<!-- views/index.ejs -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title><%= title %></title> <!-- 转义输出变量 -->
</head>
<body>
  <h1><%= message %></h1>

  <!-- 输出对象属性 -->
  <p>当前用户:<%= user.name %></p>

  <!-- 非转义输出(假设htmlContent是可信的) -->
  <div><%- htmlContent %></div> <!-- 如:<strong>加粗文本</strong> 会被渲染为加粗效果 -->
</body>
</html>

编译后(假设htmlContent: '<strong>欢迎</strong>'

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>EJS模板示例</title>
</head>
<body>
  <h1>欢迎使用EJS模板引擎</h1>
  <p>当前用户:Alice</p>
  <div><strong>欢迎</strong></div>
</body>
</html>

2. 条件判断

<% %>标签中直接编写if-else逻辑,控制内容渲染:

<!-- 假设变量:{ isLogin: true, user: { vip: false } } -->
<% if (isLogin) { %>
  <p>欢迎回来,<%= user.name %>!</p>
  <% if (user.vip) { %>
    <p class="vip">您是VIP用户,享受专属权益</p>
  <% } else { %>
    <p>升级为VIP,解锁更多功能</p>
  <% } %>
<% } else { %>
  <p>请<a href="/login">登录</a>后使用</p>
<% } %>

编译后

  <p>欢迎回来,Alice!</p>
  <p>升级为VIP,解锁更多功能</p>

3. 循环迭代

支持forforEach等JavaScript循环语法,遍历数组或对象:

<!-- 假设变量:{ fruits: ['苹果', '香蕉', '橙子'] } -->
<ul>
  <% fruits.forEach((fruit, index) => { %>
    <li><%= index + 1 %>. <%= fruit %></li>
  <% }) %>
</ul>

<!-- 遍历对象 -->
<% const user = { name: 'Bob', age: 30, email: 'bob@example.com' }; %>
<dl>
  <% for (const key in user) { %>
    <dt><%= key %></dt>
    <dd><%= user[key] %></dd>
  <% } %>
</dl>

编译后

<ul>
    <li>1. 苹果</li>
    <li>2. 香蕉</li>
    <li>3. 橙子</li>
</ul>

<dl>
    <dt>name</dt>
    <dd>Bob</dd>
    <dt>age</dt>
    <dd>30</dd>
    <dt>email</dt>
    <dd>bob@example.com</dd>
</dl>

4. 模板包含(Include)

通过<%- include('文件路径') %>引入其他EJS模板片段,实现代码复用(如公共导航、页脚):

  1. 公共组件(views/partials/header.ejs)
<!-- 头部导航 -->
<header>
  <nav>
    <ul>
      <li><a href="/">首页</a></li>
      <li><a href="/about">关于</a></li>
      <li><a href="/contact">联系</a></li>
    </ul>
  </nav>
</header>
  1. 主模板(views/index.ejs)
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title><%= title %></title>
</head>
<body>
  <%- include('partials/header') %> <!-- 引入头部组件 -->
  
  <main>
    <h1><%= message %></h1>
    <!-- 页面内容 -->
  </main>
  
  <%- include('partials/footer') %> <!-- 引入底部组件 -->
</body>
</html>

说明

  • include路径相对于当前模板文件,或相对于views目录(需配置views路径)。
  • 可向被引入模板传递变量:<%- include('partials/user', { user: currentUser }) %>

5. 模板布局(Layout)

EJS本身不直接支持模板继承(如Pug的extends),但可通过“布局模板+内容插入”实现类似功能:

  1. 布局模板(views/layout.ejs)
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title><%= title %></title>
  <%- include('partials/styles') %> <!-- 公共样式 -->
</head>
<body>
  <%- include('partials/header') %>
  
  <main>
    <%- body %> <!-- 内容占位符 -->
  </main>
  
  <%- include('partials/footer') %>
  <%- include('partials/scripts') %> <!-- 公共脚本 -->
</body>
</html>
  1. 页面模板(views/home.ejs)
<%- include('layout', { 
  title: '首页', 
  body: `
    <h2>欢迎来到首页</h2>
    <p>这是首页的具体内容</p>
  ` 
}) %>

替代方案:使用第三方模块(如ejs-mate)扩展布局功能,支持blockextend语法。

6. 自定义标签(可选)

EJS支持通过delimiter配置自定义标签分隔符(默认<%%>),适合避免与其他模板语法冲突:

// 在Express中配置自定义标签
app.set('view engine', 'ejs');
app.engine('ejs', require('ejs').__express);
// 自定义标签为{{和}}
app.locals.delimiter = '?'; // 或在渲染时指定:res.render('index', { delimiter: '?' })
<!-- 使用自定义标签 -->
<p>?= message ?</p> <!-- 等价于<%= message %> -->
<? if (isLogin) { ?>
  <p>已登录</p>
<? } ?>

四、实战案例:构建动态商品列表页

结合Express和EJS构建一个商品列表页面,展示完整开发流程:

1. 项目结构

/product-demo
  /views
    layout.ejs           # 布局模板
    index.ejs            # 商品列表页
    /partials
      header.ejs         # 头部导航
      footer.ejs         # 页脚
      product-card.ejs   # 商品卡片组件
  /public
    /css
      style.css          # 样式文件
  app.js                 # 入口文件
  package.json

2. 核心代码

(1)入口文件(app.js)
const express = require('express');
const app = express();

// 配置EJS
app.set('view engine', 'ejs');
app.set('views', './views');
// 静态资源目录
app.use(express.static('public'));

// 模拟商品数据
const products = [
  { id: 1, name: '无线蓝牙耳机', price: 299, image: 'https://picsum.photos/seed/prod1/300/200' },
  { id: 2, name: '智能手表', price: 899, image: 'https://picsum.photos/seed/prod2/300/200' },
  { id: 3, name: '便携式充电宝', price: 129, image: 'https://picsum.photos/seed/prod3/300/200' }
];

// 商品列表路由
app.get('/', (req, res) => {
  res.render('index', {
    title: '商品列表',
    products: products,
    featured: products[0] // 推荐商品
  });
});

app.listen(3000, () => {
  console.log('商品服务器运行在 http://localhost:3000');
});
(2)布局模板(layout.ejs)
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title><%= title %></title>
  <link rel="stylesheet" href="/css/style.css">
</head>
<body>
  <%- include('partials/header') %>
  <main class="container">
    <%- body %>
  </main>
  <%- include('partials/footer') %>
</body>
</html>
(3)商品卡片组件(partials/product-card.ejs)
<!-- 商品卡片组件,接收product参数 -->
<div class="product-card">
  <img src="<%= product.image %>" alt="<%= product.name %>" class="product-img">
  <h3 class="product-name"><%= product.name %></h3>
  <p class="product-price">¥<%= product.price %></p>
  <button class="add-to-cart">加入购物车</button>
</div>
(4)商品列表页(index.ejs)
<%- include('layout', { 
  title: title,
  body: `
    <h1>商品列表</h1>
    
    <section class="featured">
      <h2>推荐商品</h2>
      <%- include('partials/product-card', { product: featured }) %>
    </section>
    
    <section class="product-list">
      <h2>全部商品</h2>
      <div class="grid">
        <% products.forEach(product => { %>
          <%- include('partials/product-card', { product: product }) %>
        <% }) %>
      </div>
    </section>
  `
}) %>
(5)样式文件(public/css/style.css)
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 20px;
  margin-top: 20px;
}

.product-card {
  border: 1px solid #eee;
  border-radius: 8px;
  padding: 15px;
  text-align: center;
}

.product-img {
  width: 100%;
  height: 200px;
  object-fit: cover;
  border-radius: 4px;
}

.featured {
  margin-bottom: 30px;
  padding: 20px;
  background: #f9f9f9;
  border-radius: 8px;
}

3. 运行效果

启动服务后访问http://localhost:3000,将显示包含推荐商品和商品列表的页面,通过组件复用实现了代码精简。

五、最佳实践

1. 合理组织模板结构

  • 采用“布局模板+页面模板+组件”的三层结构:
    • layout.ejs:全局布局(包含公共头部、底部、样式和脚本)。
    • views/:存放页面模板(如index.ejsproduct.ejs)。
    • views/partials/:存放可复用组件(如导航、卡片、表单)。
  • 通过include复用组件,减少重复代码(如每个页面都需要的导航栏)。

2. 控制模板中的逻辑复杂度

  • 模板职责单一:仅负责“展示数据”,避免包含复杂业务逻辑(如数据过滤、计算、API请求)。
  • 数据预处理:在Express控制器中完成数据加工(如格式化日期、过滤敏感信息),仅传递“ ready-to-render ”的数据到模板。
  • 简化条件与循环:避免嵌套过深的if-else和多层循环(建议不超过2层),复杂逻辑可拆分为多个组件。

3. 安全使用非转义输出(<%- %>

  • 优先使用转义输出<%= %>会自动转义<>&等特殊字符,防止XSS攻击,适合用户输入的内容(如评论、用户名)。
  • 谨慎使用非转义输出<%- %>仅用于完全可信的内容(如服务器生成的HTML、经过净化的Markdown转换结果),例如:
    <!-- 危险:用户提交的内容直接输出 -->
    <div><%- userComment %></div> <!-- 若userComment包含<script>恶意代码,会被执行 -->
    
    <!-- 安全:经过净化的内容 -->
    <div><%- sanitizeHtml(userComment) %></div> <!-- 使用sanitize-html等库净化 -->
    

4. 优化模板性能

  • 启用模板缓存:生产环境中,Express会自动缓存编译后的EJS模板(通过app.enable('view cache')手动开启),减少重复编译开销。
  • 减少模板嵌套:过深的include嵌套会增加渲染时间,建议将频繁使用的组件合并或预编译。
  • 避免在模板中定义大型对象/数组:复杂数据结构应在控制器中定义,模板仅负责引用。

5. 开发效率提升

  • 使用EJS语法高亮:VS Code安装“EJS Language Support”插件,提升代码可读性。
  • 自动刷新:结合nodemon(监控文件变化)和livereload实现开发时自动重启服务并刷新页面。
  • 统一变量命名:模板变量使用驼峰命名法(与JavaScript一致),保持代码风格统一。

六、注意事项

1. 标签与HTML的冲突

  • EJS默认标签<% %>可能与其他模板语法(如Vue、Angular)冲突,可通过delimiter配置自定义标签(如[[ ]]):
    // 配置自定义标签为[[和]]
    app.locals.delimiter = '[';
    
    <p>[[= message ]]</p> <!-- 等价于<%= message %> -->
    

2. 模板路径问题

  • include的路径解析规则:
    • 相对路径:相对于当前模板文件(如<%- include('../partials/header') %>)。
    • 绝对路径:相对于views目录(需确保views配置正确,如<%- include('/partials/header') %>)。
  • 避免使用绝对路径依赖项目结构,推荐相对路径提升可移植性。

3. 空行与空白字符

  • EJS默认会保留标签周围的空白字符,可能导致HTML中出现多余空行,可通过-%>修剪标签后的换行:
    <% for (let i=0; i<3; i++) { -%> <!-- 修剪换行 -->
      <li>项目<%= i %></li>
    <% } -%>
    

4. 与其他模板引擎的选择

  • EJS vs Pug:EJS更接近HTML,适合前端开发者快速上手;Pug语法更简洁,但需要适应缩进规则。
  • EJS vs Handlebars:EJS支持原生JavaScript,灵活性更高;Handlebars语法更严格,适合团队规范统一。
  • 选择依据:团队技术背景(HTML熟悉度)、项目复杂度(简单页面用EJS更高效)、性能需求(两者性能差异极小)。

5. 调试技巧

  • 开发环境中,通过console.log在模板中输出变量(需放在<% %>标签内):
    <% console.log('商品数据:', products) %> <!-- 输出到终端 -->
    
  • 模板语法错误通常会提示“Unexpected token”,需检查标签是否闭合、JavaScript语法是否正确。

七、总结

EJS模板引擎以“HTML为骨,JavaScript为魂”的设计理念,为Node.js动态网页开发提供了简单高效的解决方案。其核心优势在于:

  1. 低学习成本:保留完整HTML结构,仅通过少量标签嵌入JavaScript,前端开发者可无缝迁移。
  2. 灵活性高:支持原生JavaScript逻辑,无需学习新语法,适合快速实现复杂动态效果。
  3. 易于集成:与Express等框架无缝配合,支持组件复用和布局管理,满足中小型项目需求。

掌握EJS的关键在于理解其标签体系(<%= %><%- %><% %>),并通过合理的模板结构设计提升代码可维护性。在实际使用中,需注意转义输出的安全风险,控制模板中的逻辑复杂度,平衡开发效率与安全性。

对于需要快速构建动态网页且团队熟悉HTML/JavaScript的场景,EJS是一个理想选择——它不追求语法的极简,而是通过“最小侵入”的方式让开发者专注于内容与数据的结合,是Node.js生态中兼具实用性和友好性的模板引擎。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值