跳至主要内容

Chapter 5.

Context Object Introduction

ctx.on(eventName: string, handler: Function): void

HydroOJ 的事件系統基於 發布-訂閱(Pub/Sub)模式,允許插件在特定時機執行自定義邏輯,而無需修改核心代碼。 參數說明

  • eventName: 事件名稱(字符串)
  • handler: 事件處理函數(可以是異步函數)

事件類型

  1. Handler 生命週期事件
  • handler/before/<HandlerClassName>:在指定的處理器執行前觸發
  • handler/after/<HandlerClassName>:在指定的處理器執行後觸發
  • handler/before/<routeName>:在指定的路徑載入前觸發
  • handler/after/<routeName>:在指定的路徑載入後觸發
  1. 應用生命週期事件
  • app/started:應用啟動完成
  • app/listen:服務器開始監聽
  • app/ready:應用就緒
  1. 業務邏輯事件
  • user/login:用戶登入
  • user/register:用戶註冊
  • problem/add:新增題目
  • record/judge:評測記錄判定
  • contest/end:比賽結束
  1. 其他事件
  • 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);
});
監聽多個事件(typescript語法小教室)
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通知區域系統通知警告、提示
常見注入點示例
  1. DomainManage - 域管理頁面
  • 位置: /domain/dashboard 頁面的側邊欄
  • 常見用途:
    • 域級別的設置
    • 管理工具
    • 統計信息
ctx.injectUI('DomainManage', 'domain_swiper', {
family: 'Properties', // 分組:屬性
icon: 'info', // 圖標
text: '顯示文本' // 顯示文本
});
  1. ProblemAdd - 題目添加頁面
  • 位置: /problem/create 頁面的導入選項
  • 常見用途:
    • 題目導入方式
    • 題目模板
ctx.injectUI('ProblemAdd', 'problem_import_qduoj', {
icon: 'copy',
text: 'Import From QDUOJ'
});
  1. UserDropdown - 用戶下拉菜單
  • 位置: 頁面右上角用戶名下拉菜單
  • 常見用途:
    • 用戶個人功能
    • 快捷入口
ctx.injectUI('UserDropdown', 'user_stats', {
icon: 'chart',
text: '個人統計'
});
  1. 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 的方法甚至覆蓋
  1. 擴展(或覆蓋)方法
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;
};
});
  1. 新增方法
ctx.withHandlerClass('ProblemDetail', (ProblemDetailHandler) => {
// 添加新方法到 ProblemDetailHandler
ProblemDetailHandler.prototype.customMethod = async function() {
// 可以訪問 this (Handler 實例)
const { domainId } = this.args;
return `Custom logic for domain ${domainId}`;
};
});

可以參閱附錄:Handler 相關附錄來查看各個路由所對應的 Handler 名稱 和剛剛的小節一樣的概念

草貓
第十四屆進階教學 aka.架網站的那個