文章目录
- 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. 循环迭代
支持for、forEach等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模板片段,实现代码复用(如公共导航、页脚):
- 公共组件(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>
- 主模板(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),但可通过“布局模板+内容插入”实现类似功能:
- 布局模板(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>
- 页面模板(views/home.ejs):
<%- include('layout', {
title: '首页',
body: `
<h2>欢迎来到首页</h2>
<p>这是首页的具体内容</p>
`
}) %>
替代方案:使用第三方模块(如ejs-mate)扩展布局功能,支持block和extend语法。
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.ejs、product.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动态网页开发提供了简单高效的解决方案。其核心优势在于:
- 低学习成本:保留完整HTML结构,仅通过少量标签嵌入JavaScript,前端开发者可无缝迁移。
- 灵活性高:支持原生JavaScript逻辑,无需学习新语法,适合快速实现复杂动态效果。
- 易于集成:与Express等框架无缝配合,支持组件复用和布局管理,满足中小型项目需求。
掌握EJS的关键在于理解其标签体系(<%= %>、<%- %>、<% %>),并通过合理的模板结构设计提升代码可维护性。在实际使用中,需注意转义输出的安全风险,控制模板中的逻辑复杂度,平衡开发效率与安全性。
对于需要快速构建动态网页且团队熟悉HTML/JavaScript的场景,EJS是一个理想选择——它不追求语法的极简,而是通过“最小侵入”的方式让开发者专注于内容与数据的结合,是Node.js生态中兼具实用性和友好性的模板引擎。
581

被折叠的 条评论
为什么被折叠?



