Documentation
¶
Overview ¶
Package app 定义模块契约与应用注册器。
v1 不做运行时插件加载,但所有功能模块必须以「编译期插件」形态组织: 核心不得直接依赖模块内部实现。这样 v2 接入 WASM 或 gRPC 宿主时,核心无需重写。
Index ¶
- Constants
- func ApplyFilter[T any](ctx context.Context, e Events, filter string, value T) T
- type App
- func (a *App) Close(ctx context.Context) error
- func (a *App) Config() config.Config
- func (a *App) DB() *database.DB
- func (a *App) Events() Events
- func (a *App) Frontend() Frontend
- func (a *App) Hooks() []Hook
- func (a *App) Logger() *slog.Logger
- func (a *App) Lookup(name string) (any, bool)
- func (a *App) Migrations() []NamedFS
- func (a *App) Modules() []string
- func (a *App) Navigation() Navigation
- func (a *App) Permissions() []Permission
- func (a *App) Provide(name string, value any)
- func (a *App) Register(modules ...Module) error
- func (a *App) Router() Router
- func (a *App) SetEvents(e Events)
- func (a *App) SetFrontend(f Frontend)
- func (a *App) Settings() []SettingGroup
- func (a *App) Start(ctx context.Context) error
- type Closer
- type Events
- type Frontend
- type Hook
- type HookFunc
- type HookProvider
- type Migrator
- type Module
- type NamedFS
- type NavGroup
- type NavItem
- type Navigation
- type NavigationProvider
- type Options
- type Permission
- type PermissionProvider
- type RouteProvider
- type Router
- type SettingGroup
- type SettingsProvider
- type Starter
Constants ¶
const ( )
Console 侧边栏的分组标识(七组页面地图)。
分组定义在核心而不是各模块各写一份:分组是**版面的骨架**, 谁往里放东西是模块的事,骨架长什么样只该有一处说了算。 插件要新开一组时自己声明一个 NavGroup,名称不冲突即可。
Variables ¶
This section is empty.
Functions ¶
Types ¶
type App ¶
type App struct {
// contains filtered or unexported fields
}
App 是模块注册与共享依赖的中心。
模块只通过 App 获取依赖并注册能力,不直接互相引用——这是编译期插件形态 的关键约束,也是 v2 换成进程外插件时唯一需要替换的接缝。
生命周期:New → Register(装配)→ 迁移 → Start(播种、后台任务)→ 对外服务 → Close。
func (*App) Navigation ¶
func (a *App) Navigation() Navigation
Navigation 汇总全部模块声明的菜单,按分组顺序与组内顺序排好。
每次调用都重新问一遍各模块,而不是在注册期收集它们的返回值: 设置分组要等 Start 时才登记(见 internal/settings), 而设置项对应的菜单入口正是从那份分组列表推导出来的。注册期收集会漏掉它们。
菜单是给人看的,顺序错了顶多难看;但标识别扭了会让前端静默丢项, 故对声明做一遍校验,把问题在测试与启动日志里说清楚。
func (*App) Provide ¶
Provide 登记一个供其他模块使用的共享服务(如设置读取器)。
这是模块之间唯一的依赖注入点:先注册的模块在 Register 里 Provide, 后注册的模块经 Lookup 取用,依赖方向由 modules.go 的装配顺序决定。
func (*App) SetFrontend ¶ added in v0.2.0
SetFrontend 接上前台扩展的实现,由插件模块在装配时调用。
type Events ¶ added in v0.2.0
type Events interface {
// Emit 通知一个动作。实现须异步执行,不拖慢调用方,也不向调用方报错。
Emit(ctx context.Context, action string, data any)
// Subscribed 判断有没有插件订阅了某个过滤器;没有时调用方可以连编码都省掉。
Subscribed(filter string) bool
// Filter 把值依次交给订阅了过滤器的插件,返回最终的值;任何一环失败都跳过那一环。
Filter(ctx context.Context, filter string, value json.RawMessage) json.RawMessage
}
Events 是内核向插件派发动作与过滤器的总线。
内核模块不认识插件:它们只在事情发生之后 Emit,在要把数据交给别人改一遍时 Filter。 插件模块装配时经 SetEvents 接上实现;没接的时候总线是空的——Emit 什么也不做, Filter 原样返回,内核照常工作。
type Frontend ¶ added in v0.2.0
type Frontend interface {
// Slot 返回插件放进某个插槽的内容。
Slot(ctx context.Context, slot string, page *hooks.Page) template.HTML
// Widget 渲染一个插件小组件,id 形如 <插件>/<小组件>;插件没启用或没产出内容时 ok 为假。
Widget(ctx context.Context, id string, page *hooks.Page) (label string, html template.HTML, ok bool)
// Shortcodes 展开正文里的短代码,返回新的正文;没有可展开的短代码时 changed 为假。
Shortcodes(ctx context.Context, html string, page *hooks.Page) (out string, changed bool)
// StripShortcodes 去掉纯文字里认得的短代码,摘要与页面描述里用。
StripShortcodes(text string) string
}
Frontend 是插件往前台页面里放东西的入口:插槽、侧栏小组件与短代码。
主题模块在渲染时调用它,插件模块是它的实现;返回的 HTML 已按插件的规则净化过, 主题原样放进页面。没接上实现时插槽为空、小组件不显示、短代码原样保留。
type Hook ¶
type Hook struct {
// Name 是钩子点标识。
Name string
// Priority 决定执行顺序,数值小者先执行。
Priority int
// Handler 是钩子实现。
Handler HookFunc
}
Hook 是一条钩子声明。
type Migrator ¶
Migrator 声明模块自带数据库迁移。
返回的文件系统根目录直接包含 goose SQL 文件;每个模块拥有独立的版本表, 迁移编号只需在模块内递增(见 internal/migrate)。
type Module ¶
type Module interface {
// Name 返回模块的唯一标识,用于日志、迁移版本表名与依赖诊断。
// 须为小写字母开头的短标识(字母、数字、下划线、连字符)。
Name() string
// Register 把模块的能力挂载到应用上。
//
// 只做装配,不得访问数据库:migrate 命令也会走同一条注册链,而那时业务表
// 可能尚不存在。需要读写库的启动逻辑放到 Starter.Start。
Register(app *App) error
}
Module 是每个功能模块必须实现的最小接口。
type NavGroup ¶
NavGroup 是 Console 侧边栏的一个分组。
分组由模块声明而不是前端写死:插件的页面要能出现在侧边栏里, 而它归哪一组只有插件自己知道。前端因此只负责把收到的东西画出来。
type NavItem ¶
type NavItem struct {
Key string
Label string
//
// 必须是绝对路径:前端以 basename=/console 挂载 SPA,
// 相对路径会被解析到当前页之下。
Path string
//
// 后端只能给出名字,而两侧之间没有编译期约束:写了登记表里没有的名字,
// 前端只会画一个默认图标,界面上不会有任何提示。故由 cmd/lumo 的契约测试盯着。
Icon string
Group string
Order int
//
// 这是**显示条件而非安全边界**:没有权限就不显示入口,真正的拦截在服务端。
// 前端隐藏按钮只是少让人白跑一趟。
Permission string
Keywords string
Description string
End bool
Hidden bool
}
NavItem 是 Console 侧边栏的一个菜单项。
type Navigation ¶
type Navigation struct {
}
Navigation 是一批菜单声明。
func CoreNavigation ¶
func CoreNavigation() Navigation
CoreNavigation 返回核心自身的菜单声明。
用户、角色、关于、日志这几页由核心实现,没有对应的功能模块,故在这里声明—— 与 app.CorePermissions 同一个理由。其余各页由各自的功能模块声明。
type NavigationProvider ¶
type NavigationProvider interface {
}
NavigationProvider 声明模块贡献的 Console 菜单。
与 SettingsProvider 并列:模块有什么页面,就该由它自己说, 而不是让前端维护一份必须与后端能力保持同步的硬编码清单。
type Options ¶
type Options struct {
Config config.Config
// DB 可为 nil:不需要数据库的命令(如仅渲染帮助)也应能构造 App。
DB *database.DB
Logger *slog.Logger
// Router 可为 nil:migrate 等非 HTTP 场景不注册接口。
Router Router
}
Options 是构造 App 的依赖。
type Permission ¶
type Permission struct {
// Key 是权限串,如 posts:write。
Key string
// Label 是显示名,走 i18n key。
Label string
// Description 是权限说明。
Description string
}
Permission 是一条权限声明,格式为 <资源>:<动作>。
func CorePermissions ¶
func CorePermissions() []Permission
CorePermissions 返回核心自身引入的权限声明。
与模块的 Permissions() 并列:用户、角色、主题与站点级操作由核心实现, 没有对应的功能模块,故在这里声明。角色编辑器据此拿到中文名, 缺了它这些权限会以「有权限串、没名字」的形态出现在界面上。
type PermissionProvider ¶
type PermissionProvider interface {
Permissions() []Permission
}
PermissionProvider 声明模块引入的权限串。
type RouteProvider ¶
type RouteProvider interface {
Routes(r Router)
}
RouteProvider 声明模块挂载 HTTP 接口。
接口注册通过三平面注册面进行,模块不能直接接触根路由, 以保证鉴权与前缀约定不被绕过。
type Router ¶
type Router interface {
// Console 挂载 /api/v1/console/** :已解析凭据、CSRF 校验、强制已认证;
// 细粒度权限由各操作用 auth.RequirePermission 或 Principal.Allows 声明。
Console() huma.API
// ConsolePublic 挂载 Console 平面下**免认证**的端点,仅供登录一类极少数场景。
ConsolePublic() huma.API
// Public 挂载 /api/v1/public/** :解析凭据但不强制,匿名可读已发布内容。
Public() huma.API
// Extension 挂载 /apis/{group}/{version}/{kind} :强制已认证。
Extension() huma.API
// Raw 在站点根路径上挂一个直接写响应的处理器,供 robots.txt、sitemap.xml、
// 订阅源一类**非 JSON 且必须位于根路径**的资源使用。
//
// 这是三平面之外唯一的注册口子,刻意开得很窄:它不经过任何鉴权与内容协商,
// 挂上去的处理器对匿名请求可见。业务接口一律走上面四个注册面。
Raw(method, path string, handler http.HandlerFunc)
}
Router 是暴露给模块的接口注册面。
四个注册面都是 huma.API:模块用 huma.Register 声明操作,请求校验、错误格式与 OpenAPI 文档随之自动生成。前缀与鉴权中间件挂在各平面的分组上,模块无需也不能自行处理。
type SettingGroup ¶
type SettingGroup struct {
// Name 是分组标识(DNS-1123),如 site、seo、mail。
Name string
// Label 是分组显示名。
Label string
// Description 是分组说明。
Description string
// Order 决定 Console 中的显示顺序,数值小者在前。
Order int
// Icon 是该分组的图标名,取值来自 console/src/lib/icons.ts 的登记表。
//
// 分组自己声明图标,设置页的区块标题与命令面板入口因此都从分组列表推导——
// 少一处需要手工同步的清单,也就不会再出现「后端有这一组、界面上没有入口」。
Icon string
// Toggle 是本组主开关的字段名,可留空。
//
// 设置页把所有分组铺在一页上,各组默认收起;声明了主开关的组,
// 那个开关会提到区块标题栏上——「开放评论」「启用邮件发送」「开放注册」
// 这类决定「这一整块要不要生效」的开关,是站长最常动的东西,
// 让它需要先展开一层才够得着,就是把最常用的动作放到最深的地方。
//
// 字段必须存在于 Form 中且为布尔,否则注册时报错:写错一个名字的代价
// 是标题栏上少个开关,而那在界面上没有任何提示。
Toggle string
// Form 是表单声明。模块用 internal/form 的 DSL 构建;
// 主题包从 settings.yaml 读入后经 form.Build 得到。
//
// 值 = Form 的缺省值被已保存值按顶层键覆盖;Schema、缺省值与条件依赖
// 三者同出一处,不再有「stuct 改了 Schema 忘了改」这种缝。
Form *form.Form
// Public 列出可经 Public 平面读取的字段名(如站点标题);其余字段仅 Console 可见。
Public []string
// Check 是 Schema 之外的 Go 侧校验(如时区名是否真实存在),入参为合并默认值后的有效值;可为 nil。
Check func(values map[string]any) error
}
SettingGroup 是一组设置项声明,走统一表单 Schema。
分组是设置的读写单位:Console 按分组渲染表单,接口按分组整体替换值。
type SettingsProvider ¶
type SettingsProvider interface {
Settings() []SettingGroup
}
SettingsProvider 声明模块注册设置项分组,走统一表单 Schema。