Appearance
EJS 模板语法与上下文函数完整参考
本文档提供在 DJAOD 主题模板中可使用的 EJS 标签语法、所有全局变量、上下文函数的完整参考。
EJS 标准标签
EJS 支持以下三种基本标签:
| 标签 | 含义 | 输出转义 | 示例 |
|---|---|---|---|
<%= %> | 输出变量(HTML 转义) | 是 | <%= title %> → 首页 - 网站 |
<%- %> | 输出变量(不转义 HTML) | 否 | <%- body %> → 直接渲染 HTML |
<% %> | 执行 JS 代码(无输出) | N/A | <% let x = 1; %> |
使用规则
<%= %> — 用于普通文本输出(自动转义 & < > " ' 等字符,防 XSS):
ejs
<title><%= title %></title>
<meta name="keywords" content="<%= keywords %>">
<input value="<%= request.query.wd %>"><%- %> — 用于 HTML 片段输出(不做转义,注意安全):
ejs
<%# 渲染布局中的 body 内容 %>
<%- body %>
<%# 渲染子模板的 HTML 输出 %>
<%- await include('../components/comment/index', { id: obj._id }) %>
<%# 渲染富文本内容 %>
<%- obj.content %>
<%# 输出 JSON 到 JS 变量 %>
<script>
var plugins = <%- JSON.stringify(plugins) %>;
</script><% %> — 用于控制流和变量声明:
ejs
<%# 条件判断 %>
<% if (user._id) { %>
<a href="/user">已登录</a>
<% } else { %>
<a href="/login">请登录</a>
<% } %>
<%# 循环遍历 %>
<% navbar.forEach((item) => { %>
<a href="<%= item.url %>"><%= item.title %></a>
<% }); %>
<%# for...of 循环 %>
<% for (let item of list) { %>
<div><%= item.title %></div>
<% } %>
<%# try/catch(在模板中使用 model() 时) %>
<% try { %>
<% const data = await model("music_song", { limit: "10" }); %>
<% } catch(e) { %>
<p>加载失败</p>
<% } %>
<%# 变量声明 %>
<% const { themeColor } = design.config; %>
<% const navbar = await model("dynamic_page", { query: { diyname: "header" } }); %>全局模板变量
以下变量在模板上下文(defaultContext)中自动可用,无需通过控制器 assign() 注入:
业务数据变量
| 变量 | 类型 | 来源 | 说明 |
|---|---|---|---|
title | String | Base.setDefaultVariables() | 页面标题(格式:页面标题 - 站点名) |
keywords | String | Base.setDefaultVariables() | SEO 关键词(逗号分隔) |
description | String | Base.setDefaultVariables() | SEO 描述 |
site | Object | Base.initConfig() | 完整站点配置对象 |
site_name | String | site.site_name | 站点名称 |
site_title | String | site.site_title | 站点标题 |
site_keywords | String | site.site_keywords | 站点默认关键词 |
site_description | String | site.site_description | 站点默认描述 |
site_logo | String | site.site_logo | 站点 Logo URL |
site_logo_black | String | site.site_logo_black | 站点 Logo(深色版本) |
site_favicon | String | site.site_favicon | Favicon URL |
site_copyright | String | site.site_copyright | 版权信息 |
site_icp | String | site.site_icp | ICP 备案号 |
site_statistics | String | site.site_statistics | 统计代码 |
contact_phone | String | site.contact_phone | 联系电话 |
contact_email | String | site.contact_email | 联系邮箱 |
contact_address | String | site.contact_address | 联系地址 |
site_gzqrcode | String | site.site_gzqrcode | 公众号二维码 |
site_wxqrcode | String | site.site_wxqrcode | 微信二维码 |
html_compress | Boolean | site.html_compress | 是否启用 HTML 压缩 |
请求上下文变量
| 变量 | 类型 | 说明 |
|---|---|---|
user | Object | 当前用户信息(见下方结构) |
cookie | Object | 当前请求的 Cookie 对象 |
request | Object | Fastify 请求对象 |
isWechat | Boolean | 请求是否来自微信内置浏览器 |
isMobile | Boolean | 是否为移动端 |
isAjax | Boolean | 是否为 AJAX 请求 |
timestamp | String | 当前时间 ISO 字符串 |
用户对象结构 (user)
js
user = {
_id: '', // 用户 ID
id: '', // 用户 ID(别名)
username: '', // 用户名
nickname: '访客', // 昵称
avatar: '', // 头像 URL
email: '', // 邮箱
mobile: '', // 手机号
isLogin: false, // 是否已登录
isGuest: true, // 是否为游客
group_id: 0, // 用户组 ID
role: 'guest' // 角色
}模板中判断登录状态:
ejs
<% if (user._id) { %>
欢迎,<%= user.nickname || user.username %>
<% } else { %>
<a href="<%= url('User/login') %>">请登录</a>
<% } %>系统配置变量
| 变量 | 类型 | 说明 |
|---|---|---|
dev | Boolean | 是否为开发模式(APP_DEBUG === 'true') |
__TPL_STATIC__ | String | 主题静态资源路径(如 /default_pc/) |
__STATIC__ | String | 全局静态资源路径(如 /) |
currentTheme | String | 当前主题名 |
themeType | String | 设备类型(pc 或 wap) |
design | Object | 当前主题的装修数据(来自 data/Design.json) |
page_model | String | 当前页面的数据模型名(如 music_song) |
global.DJ_VERSION | String | 系统版本号(用于静态资源缓存破坏) |
source_type | String | 资源来源类型 |
EJS 上下文函数
以下函数在模板中作为全局函数可用,由 template/index.js 的 defaultContext 注入。
model(name, options, [funcName])
最核心的数据查询函数。从 MongoDB 查询数据,支持缓存。
ejs
<% const songs = await model("music_song", {
orderby: "month_downs",
orderway: -1, // -1=降序, 1=升序
limit: "20",
pageNum: 1,
paging: false,
query: { status: 1, type_ids: "abc123" },
cachetime: 600 // 缓存时间(秒),0=不缓存
}); %>参数说明:
| 参数 | 类型 | 必需 | 默认值 | 说明 |
|---|---|---|---|---|
name | String | 是 | - | 数据模型名:music_song、music_album、music_author、video_movie、store_product、circle_content、music_special、dynamic_page、block、order、comment、user、coupon 等 |
options.orderby | String | 否 | createtime | 排序字段:createtime、updatetime、price、views、likes、sort、plays、downs |
options.orderway | Number | 否 | -1 | 排序方向:-1=降序,1=升序 |
options.limit | String/Number | 否 | 20 | 每页数量 |
options.pageNum | Number | 否 | 1 | 页码 |
options.paging | Boolean | 否 | false | 是否返回分页信息。true 时返回 { list: [], page: {...} } |
options.query | Object | 否 | {} | MongoDB 查询条件 |
options.cachetime | Number | 否 | 0 | Redis 缓存时间(秒),0 表示不缓存 |
options.cache_name | String | 否 | 自动生成 | 自定义缓存键名 |
funcName | String | 否 | "" | 调用模型的指定方法而非 getList |
request | Object | 自动 | - | 由 AsyncLocalStorage 自动注入,无需手动传入 |
返回值:
paging: false→Array— 数据列表paging: true→Object—{ list: Array, page: { total, pageSize, pageNum, pageCount } }
使用示例:
ejs
<%# 简单查询 — 获取最新 10 首歌曲 %>
<% const latestSongs = await model("music_song", {
orderway: -1,
orderby: "createtime",
limit: "10",
cachetime: 300
}); %>
<%# 带条件查询 %>
<% const albums = await model("music_album", {
query: { author_id: author._id, status: 1 },
limit: "20"
}); %>
<%# 分页查询 %>
<%
let { list, page } = await model("music_song", {
paging: true,
pageNum: pageNum || 1,
limit: 20,
query: { status: 1 },
cachetime: 600
});
%>
<p>共 <%= page.total %> 条,当前第 <%= page.pageNum %>/<%= page.pageCount %> 页</p>
<% list.forEach(item => { %>
<div><%= item.title %></div>
<% }); %>
<%# 调用模型特定方法 (funcName) %>
<% const stats = await model("music_song", { startDate: '2024-01-01' }, 'getMonthlyStats'); %>
<%# 查询 block(配置数据块) %>
<%
const [web_links] = await model("block", {
query: { config_name: "web_links" },
cachetime: 3600
});
const { data: web_links_data = [] } = web_links || {};
%>
<%# 查询 dynamic_page(动态页面/导航) %>
<% const navbar = await model("dynamic_page", {
query: { diyname: "header", status: 1 },
orderway: -1,
orderby: "sort",
limit: "20",
cachetime: 600
}); %>可用的数据模型名称列表:
| 模型名 | MongoDB Collection | 说明 |
|---|---|---|
music_song | 音乐歌曲 | 单曲/作品 |
music_album | 音乐专辑 | 专辑 |
music_author | 音乐作者 | 创作者 |
music_playlist | 播放列表 | 歌单 |
music_special | 音乐歌单 | 精选歌单 |
music_channel | 音乐频道 | 频道 |
video_movie | 视频电影 | 视频/MV |
store_product | 商城商品 | 商品 |
circle_content | 圈子内容 | 社区帖子 |
dynamic_page | 动态页面 | 自定义页面 |
block | 配置数据块 | 友链、广告位等 |
order | 订单 | 用户订单 |
comment | 评论 | 用户评论 |
user | 用户 | 用户信息 |
coupon | 优惠券 | 优惠券 |
seckill | 秒杀 | 秒杀商品 |
presale | 预售 | 预售商品 |
combination | 团购 | 团购商品 |
config(name)
读取系统配置项。从系统配置中获取指定键的值。
ejs
<%= await config('site_name') %>
<%= await config('js_tongji') %>
<%
let certificate = await config("sys_certificate");
let certificate_list = [];
if (certificate) {
if (typeof certificate === 'object') {
certificate_list = certificate;
} else if (typeof certificate === 'string' && certificate.trim() !== '') {
try {
certificate_list = JSON.parse(certificate);
} catch (e) {
certificate_list = [];
}
}
}
%>| 参数 | 类型 | 说明 |
|---|---|---|
name | String | 系统配置项的键名 |
url(path, params)
生成前台路由 URL。将控制器路径和参数转换为实际 URL。
ejs
<%= url('User/login') %>
<%# 输出: /user/login %>
<%= url('Music/detail', { id: 'abc123' }) %>
<%# 输出: /song/abc123 %>
<%= url('Album/detail', { id: 'def456' }) %>
<%# 输出: /album/def456 %>
<%= url('Author/detail', { id: author._id }) %>
<%# 输出: /author/ghi789 %>
<%= url('Search/index', { wd: keyword }) %>
<%# 输出: /search/index?wd=keyword %>
<%= url('User/ajax_login', { redirect: request.raw.url }) %>
<%# 输出: /user/ajax_login?redirect=/当前页面路径 %>
<%= url('Index/sitemap') %>
<%# 输出: /index/sitemap %>
<%= url('Dynamic/activity') %>
<%# 输出: /dynamic/activity %>
<%= url('Notify/index') %>
<%# 输出: /notify/index %>| 参数 | 类型 | 说明 |
|---|---|---|
_path | String | 控制器路径,格式 ControllerName/methodName(如 Music/detail) |
opt | Object | 路由参数(如 { id: 'abc123' }),undefined 值的键会被自动过滤 |
cdnurl(url, isThumb, options)
处理 CDN/图片 URL。将相对路径或原始 URL 转换为 CDN 加速 URL,可选生成缩略图。
ejs
<%# 直接输出 CDN URL %>
<img src="<%= await cdnurl(user.avatar, true) %>">
<%# 不转义 HTML(用于属性值中的动态内容) %>
<img src="<%- await cdnurl(site_logo, true) %>" alt="Logo">
<%# 普通文件 URL %>
<a href="<%= await cdnurl(file.url, false) %>">| 参数 | 类型 | 说明 |
|---|---|---|
url | String | 原始文件/图片 URL |
isThumb | Boolean | 是否生成缩略图 |
opt | Object | 额外参数(保留) |
include(templatePath, data)
加载子模板。将另一个 EJS 文件渲染后插入当前位置。
ejs
<%# 加载公共组件 %>
<%- await include('../common/header') %>
<%- await include('../common/footer') %>
<%- await include('../common/pagination') %>
<%# 加载业务组件,传入数据 %>
<%- await include('../components/comment/index', {
id: obj._id,
type: 'music',
source_uid: obj.author.user_id
}) %>
<%# 加载排行榜组件 %>
<%- await include('../components/grid-hot/index', {
modelName: 'music_song',
condition: {
is_tabs: false,
title: "排行榜",
orderway: "desc",
orderby: "week_downs",
limit: "20",
query: { type_ids: obj.type._id }
}
}) %>
<%# 加载预售/正常销售组件 %>
<% if (obj.is_presale) { %>
<%- await include('./presale_sales', { obj, request, user }) %>
<% } else { %>
<%- await include('./normal_sales', { obj, request, user }) %>
<% } %>
<%# 加载装修组件 %>
<%- await include(item.component, { ...item.param, title: item.title, model: model }) %>重要:使用 <%- 而不是 <%=,因为子模板渲染结果是 HTML。
| 参数 | 类型 | 说明 |
|---|---|---|
templatePath | String | 模板路径(相对于 view/ 目录,不含 .ejs 后缀) |
data | Object | 传递给子模板的数据对象 |
countDocuments(modelName, query, cachetime)
统计数据条数。返回集合中符合条件的文档数量。
ejs
<% const songCount = await countDocuments("music_song", { status: 1 }); %>
<p>共有 <%= songCount %> 首歌曲</p>| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
modelName | String | - | 数据模型名 |
query | Object | {} | MongoDB 查询条件 |
cachetime | Number | 300(5 分钟) | 缓存时间(秒) |
app — Fastify 应用实例
app 是 Fastify 实例的直接引用,在模板中可以调用其拥有的全部方法和属性。
常用场景:
ejs
<%# 检查插件是否安装 %>
<% if (await app.is_plugin_installed('vip')) { %>
<a class="vip-btn" href="<%= url('Vip/index') %>">开通VIP</a>
<% } %>
<% if (await app.is_plugin_installed('recharge')) { %>
<a href="<%= url('Recharge/index') %>">充值</a>
<% } %>
<%# 获取已启用的插件列表 %>
<% var plugins = await app.get_enabled_plugins('name'); %>
<%# 获取配置项 %>
<% const site_name = await app.getConfig("site_name"); %>
<%# 获取模型实例 %>
<% const songModel = app.getModel("music_song"); %>
<% const song = await songModel.findById(id); %>布局中的特殊变量
body
仅在 layout 文件中可用。代表子模板渲染后的 HTML 内容。
ejs
<%# view/layout/default.ejs %>
<!DOCTYPE html>
<html>
<head>...</head>
<body>
<header>...</header>
<main>
<%# 子模板的内容会渲染在这里 %>
<%- body %>
</main>
<footer>...</footer>
</body>
</html>模板中的 CSS 与 JS 约定
注入全局 JS 变量
view/common/public.ejs 中注入 AOD 全局对象:
ejs
<script>
var AOD = {
__TPL_STATIC__: "<%= __TPL_STATIC__ %>",
plugins: <%- JSON.stringify(plugins) %>,
progressColor: '<%- progressColor %>',
}
</script>CSS 变量
主题色通过 CSS 自定义属性注入:
ejs
<style>
:root {
--theme-color: <%= themeColor %>;
}
</style>开发/生产模式资源切换
ejs
<% if (dev) { %>
<script src="http://127.0.0.1:4000/js/theme.js"></script>
<% } else { %>
<script src="<%= __TPL_STATIC__ %>js/theme.js?v=<%= global.DJ_VERSION %>"></script>
<% } %>dev 值由环境变量 APP_DEBUG === 'true' 决定。具体 URL 由开发者根据自身开发环境配置。
版本号缓存破坏
ejs
<link rel="stylesheet" href="<%= __TPL_STATIC__ %>bulma@1.0.4/bulma.min.css?v=<%= global.DJ_VERSION %>">
<link rel="stylesheet" href="<%= __TPL_STATIC__ %>index.css?v=<%= global.DJ_VERSION %>">
<script src="<%= __TPL_STATIC__ %>bulma-toast/bulma-toast.min.js?v=<%= global.DJ_VERSION %>"></script>模板中的内联样式
EJS 模板支持在 <style> 标签中编写 CSS:
ejs
<style>
.settings {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
padding: 10px 0;
}
</style>完整模板示例
详情页模板 (music/detail.ejs)
ejs
<div class="columns">
<div class="column is-12-mobile is-12-tablet is-12-desktop is-9-fullhd">
<%# 面包屑导航 %>
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li><a href="/"><%= site_name %></a></li>
<li><a href="<%= url('Type/detail', { id: obj.type._id })%>"><%= obj.type.title %></a></li>
<li><a href="<%= request.url %>"><%= obj.title %></a></li>
</ul>
</nav>
<div class="box columns detail-box">
<div class="column is-one-quarter">
<figure class="cover-box image play" data-id="<%= obj._id%>" data-type="music">
<img src="<%= obj.image %>" alt="<%= obj.title %>" />
</figure>
</div>
<div class="column">
<div class="info-box">
<h1 class="title is-3">
<%= obj.title %>
<% if (Number(obj?.vip || 0) !== 0 ) { %>
<a class="tag is-warning ajax-modal"
href="javascript:;"
data-url="<%= url('Vip/ajax_vip')%>">
<%= obj.vipInfo?.title %>
</a>
<% } %>
</h1>
<div class="audio-info is-flex">
<p>时长: <%= common.formatTime(obj.duration) %></p>
<p>比特率: <%= common.bytesToKbps(obj.bitrate) %> kbps</p>
<p>格式: <%= obj.format %></p>
<p>大小: <%= common.formatBytes(obj.size) %></p>
</div>
<%# 条件加载不同销售组件 %>
<% if (obj.is_presale) { %>
<%- await include('./presale_sales', { obj, request, user }) %>
<% } else { %>
<%- await include('./normal_sales', { obj, request, user }) %>
<% } %>
</div>
</div>
</div>
<%# 特别推荐区块 %>
<div class="block fixed-grid">
<h2 class="title is-5">特别推荐</h2>
<div class="swiper-container swiper-index-hot">
<div class="columns swiper-wrapper">
<%
const lrs = await model("music_song", {
orderby: "month_downs",
limit: "20",
query: { type_ids: obj.type._id },
cachetime: 3600
}) || [];
%>
<% for (let item of lrs) { %>
<div class="swiper-slide column is-3-tablet is-2-desktop">
<figure class="image is-1by1">
<a href="<%= url('Music/detail', { id: item._id }) %>"
title="<%= item.title %>">
<img src="<%= item.image %>" alt="<%= item.title %>" />
</a>
</figure>
<p class="title is-6"><%= item.title %></p>
</div>
<% } %>
</div>
</div>
</div>
<%# 评论组件 %>
<%- await include('../components/comment/index', {
id: obj._id,
type: 'music',
source_uid: obj.author.user_id
}) %>
</div>
<%# 右侧栏 - 排行榜 %>
<div class="column is-12-mobile is-12-tablet is-12-desktop is-3-fullhd">
<%- await include('../components/grid-hot/index', {
modelName: 'music_song',
condition: {
is_tabs: false,
title: obj.type.title + "排行榜",
orderway: "desc",
orderby: "week_downs",
limit: "20",
query: { type_ids: obj.type._id }
}
}) %>
</div>
</div>
<script>
var pageName = "music/detail";
</script>常见模式与最佳实践
1. 避免模板错误导致的空白页
在调用 model() 时始终考虑可能的空结果:
ejs
<% const data = await model("music_song", { limit: "20" }) || []; %>
<% if (Array.isArray(data) && data.length > 0) { %>
<% data.forEach(item => { %>
<div><%= item.title %></div>
<% }); %>
<% } else { %>
<p>暂无数据</p>
<% } %>2. 安全输出用户生成的内容
富文本内容使用 <%- %> 并考虑配合 htmlFilter:
ejs
<%- common.htmlFilter(obj.content) || '暂无介绍' %>3. 条件渲染优化
在循环中直接检查属性存在性,避免模板报错:
ejs
<% item.category?.forEach((cat) => { %>
<a href="<%= url('Type/detail', { id: cat._id }) %>"><%= cat.title %></a>
<% }); %>4. 避免重复查询(使用 try/catch 提升健壮性)
ejs
<%
let songs = [];
try {
songs = await model("music_song", {
query: { status: 1 },
limit: "10",
cachetime: 300
}) || [];
} catch(e) {
// 查询失败静默处理
}
%>5. layout 中预取跨页面数据
在 layout 中查询一次,传递给所有子页面:
ejs
<%# view/layout/default.ejs %>
<%
const agreement = await model("dynamic_page", {
query: { diyname: "agreement", status: 1 },
orderway: -1,
orderby: "sort",
limit: "20",
cachetime: 600
});
%>
...
<%- await include('../common/footer', { agreement }); %>