Skip to content

主题系统架构总览

本文档描述 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.jsUser.jsAjax.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.4Tailwind / 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 字段说明

字段类型说明
nameString主题唯一标识,即目录名
titleString主题显示名称(中文)
descriptionString主题简介
imageString主题预览图 URL
versionString版本号(语义化版本)
authorObject作者信息(title/avatar/website)
typeArray支持的设备类型(Adaptive: 自适应)
moduleArray功能模块标签
devNumber是否为开发版(1=是)
installNumber是否已安装(1=已安装)
stateNumber状态(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>

也可直接在页面模板中写内联脚本。pageNamedata-link="ajax" 等系统级交互约定详见 交互约定文档

Released under the MIT License.