Appearance
前端交互约定
本文档列出主题系统中 真正影响模板写作的系统级约定 — 即后端/模板引擎注入到前端的关键变量和机制。具体的 JS 实现(用 jQuery 还是原生 JS、事件委托用什么 class 名)完全由开发者自行决定。
JS 加载方式
JS 在 EJS 模板中通过 <script> 标签引入,通常入口统一放在 view/common/script.ejs,由 layout 底部引用:
ejs
<!-- view/layout/default.ejs -->
<body>
...
<%- await include('../common/script'); %>
</body>ejs
<!-- view/common/script.ejs — 可以引入任意 JS 文件或写内联脚本 -->
<script src="<%= __TPL_STATIC__ %>js/jquery.min.js"></script>
<script src="<%= __TPL_STATIC__ %>js/theme.js?v=<%= global.DJ_VERSION %>"></script>开发/生产模式切换可利用 dev 变量:
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>
<% } %>全局 JS 对象 AOD
view/common/public.ejs 自动注入一个全局 JS 对象,将服务端变量暴露给前端:
ejs
<script>
var AOD = {
__TPL_STATIC__: "<%= __TPL_STATIC__ %>", // 主题静态资源路径前缀
plugins: <%- JSON.stringify(plugins) %>, // 已启用的插件列表
progressColor: '<%- progressColor %>' // 播放进度条颜色
}
</script>前端 JS 通过 window.AOD.__TPL_STATIC__ 等获取这些值。
页面初始化标识 pageName
每个 EJS 模板可在末尾声明 pageName,供 JS 入口根据当前页面执行对应的初始化逻辑:
ejs
<script>
var pageName = "music/detail";
</script>JS 入口的典型写法:
js
const pages = {
"index/index": initIndex,
"music/detail": initMusicDetail,
"user/login": initLogin,
};
document.addEventListener("DOMContentLoaded", () => {
if (window.pageName && pages[window.pageName]) {
pages[window.pageName]();
}
});约定:pageName 格式为 "模块名/页面名",与控制器方法的模板路径对应。这是 JS 与模板之间的唯一命名约定,其余 class 名、选择器等由开发者自定。
内置 AJAX 导航 (data-link="ajax")
系统内置了一个 AJAX 页面导航机制(由 singlePageApplication 函数提供)。链接添加 data-link="ajax" 属性后,点击不刷新整页,而是通过 AJAX 加载目标页并替换内容区:
ejs
<a href="<%= url('Music/detail', { id: song._id }) %>" data-link="ajax">
<%= song.title %>
</a>工作原理:
- 拦截
a[data-link="ajax"]的 click 事件 fetch(url, { headers: { 'x-theme-layout': 'off' } })— 该头部告诉服务端不渲染 layout,只返回#app内容区- 服务端返回纯内容 HTML(不含
<html><head>等外层结构) - 提取并执行其中的
<script>标签 document.getElementById('app').innerHTML = htmlhistory.pushState(url)更新 URL
不使用此特性:不加 data-link="ajax",所有链接走传统整页刷新。
x-theme-layout 请求头
AJAX 导航依赖这个请求头。当请求携带 x-theme-layout: off 时,服务端 Base 基类的 preHandler 会自动禁用 layout 渲染:
js
// Base.setupRequestHooks() 中
const layoutHeader = request.headers['x-theme-layout'];
if (layoutHeader === 'off') {
request._viewCtx.layout = false;
request._viewCtx.isLayoutLocked = true;
}这意味着服务端只返回 EJS 页面模板的内容部分(<%- body %>),不包裹 layout 的 HTML 骨架。
弹窗类接口约定
登录、注册、VIP 购买等弹窗通常通过 AJAX 加载 HTML 片段。服务端应设置 layout(false) 仅返回表单内容:
js
// controller/User.js
async ajax_login(request, reply) {
this.layout(false); // 不渲染 layout,只返回表单 HTML
return this.fetch('user/ajax_login');
}对应的模板文件通常命名为 ajax_*.ejs(如 user/ajax_login.ejs),只包含表单结构,不含 <html> <head> <body> 等外层标签。
__TPL_STATIC__ 与 global.DJ_VERSION
| 变量 | 用途 |
|---|---|
__TPL_STATIC__ | 主题静态资源路径前缀(如 /default_pc/),用于构建 JS/CSS/图片的 URL |
global.DJ_VERSION | 系统版本号,附加在资源 URL 后用于缓存破坏 |
ejs
<link rel="stylesheet" href="<%= __TPL_STATIC__ %>css/theme.css?v=<%= global.DJ_VERSION %>">
<script src="<%= __TPL_STATIC__ %>js/theme.js?v=<%= global.DJ_VERSION %>"></script>
<img src="<%= __TPL_STATIC__ %>images/logo.svg">CSS 变量与主题模式
主题色在模板中注入为 CSS 变量:
ejs
<style>
:root {
--theme-color: <%= themeColor %>;
}
</style>主题模式(light/dark/system)通过 <html> 的 data-theme 属性控制:
ejs
<html data-theme="<%= design?.config?.theme_mode || 'light' %>">可用 API 接口
交互行为需要调用后端 API。完整接口参考见 API 接口文档,核心接口速查:
| 功能 | 接口 | 方法 | 鉴权 |
|---|---|---|---|
| 数据列表加载 | /ajax/index | GET | 否 |
| 关注/取消 | /ajax/follow | POST | 是 |
| 收藏/取消 | /ajax/collect | POST | 是 |
| 加入购物车 | /cart/add | POST | 是 |
| 点赞/踩 | /ajax/reactions | POST | 是 |
| 购买下载 | /ajax/buyDownload | POST | 是 |
| 提交订单 | /order/submit | POST | 是 |
| 查询订单状态 | /order/status | GET | 是 |
| 生成下载签名 | /ajax/signature | POST | 是 |
| 发送短信验证码 | /ajax/sendSms | POST | 否(需 captcha) |
| 发送邮件验证码 | /ajax/sendEmail | POST | 否(需 captcha) |
| 获取点选验证码 | /ajax/captcha | GET | 否 |
| 未读通知数量 | /ajax/getNotifyCount | GET | 是 |
| 购物车数量 | /ajax/getCartCount | GET | 是 |
| 音频播放数据 | /ajax/audioplayer | GET | 否(WASM 加密) |
| 分享 | /ajax/share | POST | 否 |
| 文件上传 | /ajax/uploadConfig | GET | 是(签名字段) |