Appearance
主题系统架构总览
本文档描述 DJAOD 平台主题系统的整体架构,帮助开发者理解各模块之间的分工与数据流动方式。
三层架构
主题系统由三个相对独立的模块组成,分别负责服务端渲染、后端逻辑控制、前端交互增强:
┌─────────────────────────────────────────────────────────────────┐
│ 请求生命周期 │
├─────────────────────────────────────────────────────────────────┤
│ 浏览器请求 │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ backend-admin/src/app/index │ │
│ │ ┌────────────┐ ┌───────────┐ ┌─────────┐ │ │
│ │ │ middleware │→│ controller│→│ routes │ │ │
│ │ │ (鉴权拦截) │ │ (业务逻辑) │ │ (路由表) │ │ │
│ │ └────────────┘ └─────┬─────┘ └─────────┘ │ │
│ └────────────────────────┼────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ backend-admin/src/theme/<theme_name> │ │
│ │ ┌──────────┐ ┌────────┐ ┌──────────────┐ │ │
│ │ │ view/ │ │ data/ │ │ config/ │ │ │
│ │ │ (EJS模板) │ │(运行时 │ │ (主题配置) │ │ │
│ │ │ │ │ 数据) │ │ │ │ │
│ │ └──────────┘ └────────┘ └──────────────┘ │ │
│ └──────────────────────────────────────────────┘ │
│ │ │
│ 渲染 HTML(含 <script> 标签) │
│ │ │
│ 浏览器解析 DOM、加载 CSS、执行 JS │
└─────────────────────────────────────────────────────────────────┘三个模块的分工
1. 服务端控制器 (backend-admin/src/app/index)
Fastify 框架下的 MVC 控制器层,负责接收请求、调用数据模型、组装视图变量、渲染 EJS 模板。
| 目录/文件 | 职责 |
|---|---|
BaseController.js | 空基类(当前为空,保留扩展) |
controller/Base.js | 真正的核心基类(~728 行),包含全部模板渲染与请求处理逻辑 |
controller/*.js | 各业务控制器(如 Music.js、User.js、Ajax.js 等),继承自 Base |
middleware/ | Fastify 中间件(UserAuthTokenValidation 登录鉴权) |
routes/index.js | 自动生成的路由配置表,描述 URL → 控制器方法 的映射 |
核心机制:
- 每个控制器通过
noNeedLogin/noNeedRight数组声明哪些方法不需要登录/权限 Base.assign(name, value)向模板注入变量Base.fetch(templatePath)渲染模板,自动装配 layout、theme、design 等上下文- 通过
AsyncLocalStorage在异步上下文中安全传递request/reply
2. 服务端模板 (backend-admin/src/theme/<theme_name>)
EJS 模板文件集合,在服务端被执行,生成 HTML 响应。
| 目录/文件 | 职责 |
|---|---|
info.json | 主题元数据(名称、版本、作者、类型等) |
config/ | 主题级配置 |
config/design.json | 页面装修组件定义(header、banner、footer 等) |
config/index.json | 简单配置项(默认语言、默认颜色、加载封面等) |
data/ | 运行时数据 |
data/Design.json | 实际保存的装修布局数据(sidebar、index 页面的 subject/right 组件实例) |
data/config.js | 主题能力声明(侧边栏支持、可用组件清单、可用链接组、后台可视化表单配置) |
data/DynamicPage.js | 动态页面数据(定义哪些模型数据可作为动态页面使用) |
data/design_bak.json | 装修数据的备份 |
utils/common.js | 模板中可调用的工具函数集合 |
view/ | 全部 EJS 模板文件 |
view/layout/ | 布局模板(决定 HTML 骨架结构) |
view/common/ | 公共组件(header、footer、sidebar、pagination、public 资源引入、script 加载) |
view/<module>/ | 各业务模块的页面模板 |
3. 前端 JS 交互层
主题的交互行为(按钮点击、表单提交、弹窗等)通过在 EJS 模板中嵌入 <script> 标签实现。可以使用任意方式编写:jQuery、原生 JS、或任何构建工具(Webpack / Vite / esbuild)。没有强制性框架依赖。
当前 default_pc 使用的 JS 入口是 view/common/script.ejs,通过 <script type="module" src="..."> 引入,但主题开发者完全可以改为 <script src="theme.js"> 的方式。
详见 交互约定文档。
数据流:一次完整的页面请求
以用户访问 /song/:id(音乐详情页)为例:
1. 浏览器发起 GET /song/abc123
│
2. Fastify 路由匹配 (routes/index.js)
path: /song/:id → controller: Music, handler: detail
│
3. Middleware 检查 (UserAuthTokenValidation)
检查 session 中的 user,若未登录且不在 noNeedLogin 列表中则重定向
│
4. Fastify preHandler 钩子 (Base.setupRequestHooks)
- 初始化 request._viewCtx(layout、data、themeType)
- 解析 multipart body
- 加载站点配置
- 设置默认 SEO 变量
- 刷新用户数据缓存
- 生成 signature cookie
│
5. Controller 方法 (Music.detail)
- 调用 this.fastify.getModel("music_song").findById(id)
- 调用 this.assign('obj', songData)
- 调用 this.assign('title', songData.title + ' - ' + site_name)
- 返回 this.fetch('music/detail')
│
6. Base.fetch() → Base.view()
- 获取当前主题名(从配置 redis 或默认 'default_pc')
- 构建模板路径: <theme>/view/music/detail.ejs
- 读取 design 数据(从 redis 或文件)
- 组装最终 data 对象(含 design、user、site、theme、环境变量等)
- 注入 templateUtils[theme] 中的工具函数
- 返回 [template, data, { layout: 'default' }]
│
7. Fastify View 引擎渲染
- 先渲染 layout/default.ejs (HTML 骨架)
- layout 中 <%- body %> 处插入 music/detail.ejs 的渲染结果
- EJS 模板中使用 model() 查询数据库、config() 读取配置等
- 最终输出完整 HTML
│
8. reply.type('text/html').send(html)
若 html_compress 开启,先压缩再发送
│
9. 浏览器收到 HTML
- 解析 DOM、加载 CSS、执行 JS
- main.js 中 DOMContentLoaded 触发
- window.AppCommon() 初始化全局交互
- pageName === "music/detail" → AppRegistrar["music/detail"]() 执行页面特定 JS
- Swiper 初始化、波形图绑定、下载按钮绑定等技术栈(default_pc 参考)
以下为 default_pc 模板使用的技术栈,主题开发者可自由替换。系统本身只依赖 Fastify + EJS + MongoDB + Redis。
| 层级 | default_pc 选型 | 可替换 |
|---|---|---|
| 后端框架 | Fastify v5 | 系统固定 |
| 模板引擎 | EJS (@fastify/view) | 系统固定 |
| 数据库 | MongoDB (Mongoose) | 系统固定 |
| 缓存 | Redis | 系统固定 |
| 异步上下文 | AsyncLocalStorage | 系统固定 |
| JS 编写方式 | Vite 6 + ES Module | 可用任何方式(jQuery / 原生 JS / Webpack / 手工) |
| CSS 框架 | Bulma 1.0.4 | Tailwind / Bootstrap / 手写 |
| 轮播图 | Swiper 11 | 任意轮播库 |
| 图标 | Font Awesome 6 | 任意图标库 |
主题识别与加载机制
系统根据请求设备类型自动切换主题:
js
// Base.detectThemeType()
detectThemeType(request) {
const userAgent = request.headers['user-agent'] || '';
const isMobile = /mobile|android|iphone|ipad|ipod/i.test(userAgent);
return isMobile ? 'wap' : 'pc';
}主题名通过以下逻辑决定:
js
// Base.view() 中
const themeConfig = await this.fastify.getConfig(['pc_theme', 'wap_theme', 'mob_status']);
let theme = themeConfig.pc_theme || 'default_pc';
if (themeConfig.mob_status == 2) {
theme = themeConfig[`${ctx.themeType}_theme`] || theme;
}最终模板路径由 theme + "/view/" + templatePath + ".ejs" 拼接,布局路径由 theme + "/view/layout/" + layout + ".ejs" 拼接。
静态资源前缀
| 变量 | 含义 | 示例值 |
|---|---|---|
__TPL_STATIC__ | 主题静态资源路径 | /default_pc/ |
__STATIC__ | 全局静态资源路径 | / |
这两个变量在模板中用于构建 CSS、JS、图片等资源 URL:
ejs
<link rel="stylesheet" href="<%= __TPL_STATIC__ %>bulma@1.0.4/bulma.min.css">
<script type="module" src="<%= __TPL_STATIC__ %>main-abc123.js"></script>
<img src="<%= __TPL_STATIC__ %>images/logo.svg">开发模式判断
模板中通过 dev 变量判断是否为开发环境(APP_DEBUG === 'true'),可用于切换本地/生产资源路径:
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>
<% } %>主题包格式与安装
主题以 ZIP 包形式分发,解压后目录必须包含 info.json:
json
{
"name": "default_pc",
"title": "默认PC端模板",
"description": "默认PC端模板",
"version": "1.0.0",
"author": {
"title": "大图网络",
"avatar": "https://picsum.photos/100/100",
"website": "http://www.maccms.la"
},
"type": [{ "label": "自适应", "value": "Adaptive" }],
"module": [{ "name": "FullFunction", "label": "全功能" }],
"dev": 1,
"install": 1,
"state": 1
}info.json 字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
name | String | 主题唯一标识,即目录名 |
title | String | 主题显示名称(中文) |
description | String | 主题简介 |
image | String | 主题预览图 URL |
version | String | 版本号(语义化版本) |
author | Object | 作者信息(title/avatar/website) |
type | Array | 支持的设备类型(Adaptive: 自适应) |
module | Array | 功能模块标签 |
dev | Number | 是否为开发版(1=是) |
install | Number | 是否已安装(1=已安装) |
state | Number | 状态(1=启用) |
快速上手:创建一个新主题
最小可工作主题结构
my_theme/
├── info.json # 主题元数据
├── config/
│ ├── design.json # 装修组件定义
│ └── index.json # 简单配置项
├── data/
│ ├── Design.json # 装修布局数据
│ ├── config.js # 主题能力声明
│ └── DynamicPage.js # 动态页面
├── utils/
│ └── common.js # 工具函数
└── view/
├── layout/
│ └── default.ejs # 主布局
├── common/
│ ├── header.ejs # 头部导航
│ ├── footer.ejs # 页脚
│ ├── public.ejs # CSS 引入
│ ├── script.ejs # JS 引入
│ └── aside.ejs # 侧边栏
├── index/
│ └── index.ejs # 首页
└── error/
├── 404.ejs # 404 页面
└── index.ejs # 通用错误页第一步:创建 info.json
json
{
"name": "my_theme",
"title": "我的主题",
"description": "自定义主题",
"version": "1.0.0",
"author": { "title": "开发者名称" },
"state": 1
}第二步:创建核心布局 view/layout/default.ejs
ejs
<!DOCTYPE html>
<html lang="zh" data-theme="light">
<head>
<meta charset="UTF-8">
<title><%= title %></title>
<meta name="keywords" content="<%= keywords %>">
<meta name="description" content="<%= description %>">
<%- await include('../common/public'); %>
</head>
<body>
<%- await include('../common/header'); %>
<main>
<%- body %>
</main>
<%- await include('../common/footer'); %>
<%- await include('../common/script'); %>
</body>
</html>第三步:创建 data/config.js
js
export default {
is_sidebar: false,
is_right: false,
components: [],
links: [],
formBuilder: []
}第四步:创建 data/Design.json
json
{
"config": { "theme_mode": "light" },
"sidebar": [],
"index": { "subject": [], "right": [] }
}第五步:创建首页模板 view/index/index.ejs
ejs
<h1><%= site_name %></h1>
<p><%= site_description %></p>第六步:放置到正确目录并启用
将主题目录放到 backend-admin/src/theme/my_theme/,在后台管理 → 系统配置 → 基础配置中,将 pc_theme 设置为 my_theme。
JS 引入方式
前端 JS 通过在 EJS 模板中直接嵌入 <script> 标签加载,可使用任何方式编写。view/common/script.ejs 是约定俗成的 JS 入口文件,在 layout 底部引入:
ejs
<!-- view/layout/default.ejs -->
...
<%- await include('../common/script'); %>
</body>
</html>ejs
<!-- view/common/script.ejs -->
<script src="<%= __TPL_STATIC__ %>js/jquery.min.js"></script>
<script src="<%= __TPL_STATIC__ %>js/theme.js"></script>也可直接在页面模板中写内联脚本。pageName、data-link="ajax" 等系统级交互约定详见 交互约定文档。