Chapter 5.
Context Object Introduction
ctx.on(eventName: string, handler: Function): void
HydroOJ 的事件系統基於 發布-訂閱(Pub/Sub)模式,允許插件在特定時機執行自定義邏輯,而無需修改核心代碼。 參數說明
- eventName: 事件名稱(字符串)
- handler: 事件處理函數(可以是異步函數)
事件類型
- Handler 生命週期事件
handler/before/<HandlerClassName>:在指定的處理器執行前觸發handler/after/<HandlerClassName>:在指定的處理器執行後觸發handler/before/<routeName>:在指定的路徑載入前觸發handler/after/<routeName>:在指定的路徑載入後觸發
- 應用生命週期事件
app/started:應用啟動完成app/listen:服務器開始監聽app/ready:應用就緒
- 業務邏輯事件
user/login:用戶登入user/register:用戶註冊problem/add:新增題目record/judge:評測記錄判定contest/end:比賽結束
- 其他事件
system/setting:系統設置變更domain/create:域創建 ...
尋找事件名稱請參閱附錄:事件相關附錄
基礎示範
ctx.on('user/login', async (udoc, ip) => {
console.log(`用戶 ${udoc.uname} 從 ${ip} 登錄`);
});
ctx.on('你想查的事件', async (...args) => {
console.log('\n=== 事件參數 ===');
args.forEach((arg, i) => {
console.log(`參數 ${i}:`, arg);
});
});
ctx.on('你想查的事件', async (...args) => {
console.log('參數數量:', args.length);
console.log('參數類型:', args.map(a => typeof a));
console.log('參數內容:', args);
});
const events = ['user/login', 'user/logout'];
events.forEach(event => {
ctx.on(event, async (udoc) => {
console.log(`事件: ${event}, 用戶: ${udoc.uname}`);
});
});
ctx.off(eventName: string, handler: Function): void
用法一樣 但是是移除已註冊的事件監聽器。
ctx.once(eventName: string, handler: Function): void
用法一樣 但是是只會執行一次的事件監聽器。
handler/before(after)
路由名 vs Handler 名稱 事件
路由名即該頁面的data-page屬性(可在瀏覽器開發者工具(按F12)中查看 會在最外層的<html>標籤中)
假設有一個路由是ctx.Route('contest_create', '/contest/create', ContestEditHandler)
- 路由名為
contest_create - Handler 名稱為
ContestEditHandler
可以參閱附錄:Handler 相關附錄來查看各個路由所對應的 Handler 名稱
可以直接傳入
ctx.on('handler/before/problem_detail', async (Arg) => {
console.log(Arg); // Arg 會是一個 Handler 物件 包含所有參數
// 依據範例 Arg 的類型是 ProblemDetailHandler(它繼承自 Handler 基類)
// ===== 可訪問的通用屬性 =====
Arg.user // 當前用戶對象
Arg.domain // 當前域對象
Arg.session // 會話對象
Arg.request // HTTP 請求對象
Arg.response // HTTP 響應對象
Arg.args // 解析後的請求參數
// ===== Handler 特有的屬性 =====
Arg.pdoc // 題目文檔(ProblemDetailHandler 特有)
Arg.tdoc // 比賽文檔(ContestDetailHandler 特有)
Arg.rdoc // 記錄文檔(RecordDetailHandler 特有)
// ===== 可調用的方法 =====
Arg.checkPerm() // 檢查權限
Arg.checkPriv() // 檢查特權
Arg.url() // 生成 URL
Arg.translate() // 翻譯文本
Arg.back() // 返回上一頁
});
ctx.injectUI
ctx.injectUI('<INJECT-POINT>', 'your_route', {
family: 'Properties', // 分組
icon: 'settings', // 圖標
text: 'Your Feature' // 顯示文本
});
interface InjectOptions {
icon?: string; // 圖標名稱
text?: string; // 顯示文本
family?: string; // 分組名稱
args?: any; // 傳遞給路由的參數
displayName?: string; // 覆蓋顯示名稱
checker?: Function; // 顯示條件檢查函數
}
參數說明
-
<INJECT-POINT>: 注入點名稱(字符串),例如 your_route: 你的功能路由(字符串)即點下去後會導向的頁面路由 example:/shop/grass-cat之類的options: 一個包含以下屬性的對象:family: 分組名稱(字符串)icon: 圖標名稱(字符串)text: 顯示文本(字符串)
常用注入點
根據 HydroOJ 源碼整理:
| 注入點 | 位置 | 用途 | 示例 |
|---|---|---|---|
| DomainManage | 域管理頁面 | 域設置、管理功能 | 輪播圖設置、自定義配置 |
| ProblemAdd | 題目添加頁面 | 題目導入方式 | 從其他 OJ 導入 |
| UserDropdown | 用戶下拉菜單 | 用戶相關功能 | 個人統計、設置 |
| Nav | 導航欄 | 全局導航 | 新增頁面入口 |
| ControlPanel | 控制面板區域 | 系統管理主頁 | 側旁列表 |
| Notification | 通知區域 | 系統通知 | 警告、提示 |
- DomainManage - 域管理頁面
- 位置: /domain/dashboard 頁面的側邊欄
- 常見用途:
- 域級別的設置
- 管理工具
- 統計信息
ctx.injectUI('DomainManage', 'domain_swiper', {
family: 'Properties', // 分組:屬性
icon: 'info', // 圖標
text: '顯示文本' // 顯示文本
});
- ProblemAdd - 題目添加頁面
- 位置: /problem/create 頁面的導入選項
- 常見用途:
- 題目導入方式
- 題目模板
ctx.injectUI('ProblemAdd', 'problem_import_qduoj', {
icon: 'copy',
text: 'Import From QDUOJ'
});
- UserDropdown - 用戶下拉菜單
- 位置: 頁面右上角用戶名下拉菜單
- 常見用途:
- 用戶個人功能
- 快捷入口
ctx.injectUI('UserDropdown', 'user_stats', {
icon: 'chart',
text: '個人統計'
});
- Nav - 導航欄
- 位置: 頁面頂部導航欄
- 常見用途:
- 全局頁面入口
- 重要功能入口
ctx.injectUI('Nav', 'custom_page', {
icon: 'star',
text: '自定義頁面'
});
Notification 是一個特殊的注入點,用於顯示系統級通知消息,而不是像其他注入點那樣添加 UI 元素。
ctx.injectUI(
'Notification', // 注入點:固定為 'Notification'
message: string, // 通知消息內容
options: { // 配置選項
args?: any[], // 消息參數(用於格式化)
type?: string, // 通知類型:'info' | 'warn' | 'error' | 'success'
},
priv?: number // 最低權限要求(可選)
)
範例
// packages/hydrooj/src/entry/common.ts
// 插件加載失敗時的通知
app.injectUI(
'Notification',
`${loadType} load fail: {0}`,
{ args: [pluginName], type: 'warn' },
PRIV.PRIV_VIEW_SYSTEM_NOTIFICATION
);
// 模板加載失敗時的通知
app.injectUI(
'Notification',
'Template load fail: {0}',
{ args: [templatePath], type: 'warn' },
PRIV.PRIV_VIEW_SYSTEM_NOTIFICATION
);
// Addon 未找到時的通知
app.injectUI(
'Notification',
'Addon not found: {0}',
{ args: [addonPath], type: 'warn' },
PRIV.PRIV_VIEW_SYSTEM_NOTIFICATION
);
ctx.inject()
用於安全地訪問服務。它確保只有在所需服務可用時才執行代碼,避免服務未就緒導致的錯誤。
ctx.inject(
['service1', 'service2', ...], // 依賴列表
(c) => { // 回調函數
// c.service1 和 c.service2 已就緒
// 可以安全使用
}
)
像是
ctx.inject(['db'], (c) => {
// 確保 db 已初始化
c.db.collection('user').find(); // 安全
});
inject 的返回值和清理
ctx.inject(['db'], (c) => {
// 設置定時任務
const interval = setInterval(() => {
console.log('Periodic task');
}, 1000);
// 返回清理函數
return () => {
clearInterval(interval);
console.log('Cleanup done');
};
});
常用服務可參閱附錄:常用服務列表
ctx.Route()
ctx.Route(
'route_name', // 路由名稱 (即 data-page 屬性)
'/path/:param', // URL 路徑(支持參數)
HandlerClass, // Handler 類
PERM.PERM_XXX, // 權限檢查(可選)
PRIV.PRIV_XXX // 特權檢查(可選)
)
關於路由可參閱附錄:Handler 相關附錄 範例
// 來源: packages/hydrooj/src/handler/problem.ts
export async function apply(ctx: Context) {
// 1. 基本路由(只有權限檢查)
ctx.Route(
'problem_main', // 路由名稱
'/p', // URL: /p
ProblemMainHandler, // Handler 類
PERM.PERM_VIEW_PROBLEM // 需要查看題目權限
);
// 2. 帶 URL 參數的路由
ctx.Route(
'problem_detail', // 路由名稱
'/p/:pid', // URL: /p/1000, /p/P1000
ProblemDetailHandler // Handler 類
// 無權限檢查(在 Handler 內部處理)
);
// 3. 帶操作的路由
ctx.Route(
'problem_submit',
'/p/:pid/submit',
ProblemSubmitHandler,
PERM.PERM_SUBMIT_PROBLEM
);
// 4. 多級路徑
ctx.Route(
'problem_file_download',
'/p/:pid/file/:filename',
ProblemFileDownloadHandler
);
}
至於 Handler 類的實作 是下一章節的內容
ctx.withHandlerClass(handlerName: string, callback: (HandlerClass) => void)
參數說明
- handlerName: 要擴展的 Handler 類名(字符串)
- callback: 回調函數,接收 Handler 類作為參數 可以擴展現有 Handler 的方法甚至覆蓋
- 擴展(或覆蓋)方法
ctx.withHandlerClass('ContestDetail', (ContestDetailHandler) => {
const originalGet = ContestDetailHandler.prototype.get;
// 包裝原方法
ContestDetailHandler.prototype.get = async function(...args) {
console.log('Before contest detail get');
const result = await originalGet.apply(this, args);
console.log('After contest detail get');
return result;
};
});
- 新增方法
ctx.withHandlerClass('ProblemDetail', (ProblemDetailHandler) => {
// 添加新方法到 ProblemDetailHandler
ProblemDetailHandler.prototype.customMethod = async function() {
// 可以訪問 this (Handler 實例)
const { domainId } = this.args;
return `Custom logic for domain ${domainId}`;
};
});
可以參閱附錄:Handler 相關附錄來查看各個路由所對應的 Handler 名稱 和剛剛的小節一樣的概念
