Skip to content

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() 注入:

业务数据变量

变量类型来源说明
titleStringBase.setDefaultVariables()页面标题(格式:页面标题 - 站点名
keywordsStringBase.setDefaultVariables()SEO 关键词(逗号分隔)
descriptionStringBase.setDefaultVariables()SEO 描述
siteObjectBase.initConfig()完整站点配置对象
site_nameStringsite.site_name站点名称
site_titleStringsite.site_title站点标题
site_keywordsStringsite.site_keywords站点默认关键词
site_descriptionStringsite.site_description站点默认描述
site_logoStringsite.site_logo站点 Logo URL
site_logo_blackStringsite.site_logo_black站点 Logo(深色版本)
site_faviconStringsite.site_faviconFavicon URL
site_copyrightStringsite.site_copyright版权信息
site_icpStringsite.site_icpICP 备案号
site_statisticsStringsite.site_statistics统计代码
contact_phoneStringsite.contact_phone联系电话
contact_emailStringsite.contact_email联系邮箱
contact_addressStringsite.contact_address联系地址
site_gzqrcodeStringsite.site_gzqrcode公众号二维码
site_wxqrcodeStringsite.site_wxqrcode微信二维码
html_compressBooleansite.html_compress是否启用 HTML 压缩

请求上下文变量

变量类型说明
userObject当前用户信息(见下方结构)
cookieObject当前请求的 Cookie 对象
requestObjectFastify 请求对象
isWechatBoolean请求是否来自微信内置浏览器
isMobileBoolean是否为移动端
isAjaxBoolean是否为 AJAX 请求
timestampString当前时间 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>
<% } %>

系统配置变量

变量类型说明
devBoolean是否为开发模式(APP_DEBUG === 'true'
__TPL_STATIC__String主题静态资源路径(如 /default_pc/
__STATIC__String全局静态资源路径(如 /
currentThemeString当前主题名
themeTypeString设备类型(pcwap
designObject当前主题的装修数据(来自 data/Design.json
page_modelString当前页面的数据模型名(如 music_song
global.DJ_VERSIONString系统版本号(用于静态资源缓存破坏)
source_typeString资源来源类型

EJS 上下文函数

以下函数在模板中作为全局函数可用,由 template/index.jsdefaultContext 注入。

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=不缓存
}); %>

参数说明

参数类型必需默认值说明
nameString-数据模型名:music_songmusic_albummusic_authorvideo_moviestore_productcircle_contentmusic_specialdynamic_pageblockordercommentusercoupon
options.orderbyStringcreatetime排序字段:createtimeupdatetimepriceviewslikessortplaysdowns
options.orderwayNumber-1排序方向:-1=降序,1=升序
options.limitString/Number20每页数量
options.pageNumNumber1页码
options.pagingBooleanfalse是否返回分页信息。true 时返回 { list: [], page: {...} }
options.queryObject{}MongoDB 查询条件
options.cachetimeNumber0Redis 缓存时间(秒),0 表示不缓存
options.cache_nameString自动生成自定义缓存键名
funcNameString""调用模型的指定方法而非 getList
requestObject自动-由 AsyncLocalStorage 自动注入,无需手动传入

返回值

  • paging: falseArray — 数据列表
  • paging: trueObject{ 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 = [];
        }
    }
}
%>
参数类型说明
nameString系统配置项的键名

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 %>
参数类型说明
_pathString控制器路径,格式 ControllerName/methodName(如 Music/detail
optObject路由参数(如 { 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) %>">
参数类型说明
urlString原始文件/图片 URL
isThumbBoolean是否生成缩略图
optObject额外参数(保留)

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。

参数类型说明
templatePathString模板路径(相对于 view/ 目录,不含 .ejs 后缀)
dataObject传递给子模板的数据对象

countDocuments(modelName, query, cachetime)

统计数据条数。返回集合中符合条件的文档数量。

ejs
<% const songCount = await countDocuments("music_song", { status: 1 }); %>
<p>共有 <%= songCount %> 首歌曲</p>
参数类型默认值说明
modelNameString-数据模型名
queryObject{}MongoDB 查询条件
cachetimeNumber300(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 }); %>

Released under the MIT License.