You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Hydro/docs/addon.md

196 lines
6.4 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# Hydro 附加组件开发
前置条件NodeJS>10.10
此教程将以编写剪贴板插件为例进行说明。
## Step1 初始化项目
在一个空文件夹中运行 `yarn init` 并按照提示填写相关信息。
```sh
/workspace/hydro-plugin $ yarn init
yarn init v1.22.4
question name (hydro-plugin): @hydrooj/pastebin
question version (1.0.0): 0.0.1
question description: HydroOJ的剪贴板组件
question entry point (index.js): package.json
question repository url: https://github.com/hydro-dev/pastebin.git
question author: undefined <masnn0@outlook.com>
question license (MIT): MIT
question private:
success Saved package.json
```
## Step2 准备编写组件
分析:剪贴板组件需要以下功能:
- 与数据库交互来存储/检索相应文档。
- 提供 /paste/create 路由以创建新文档。
- 提供 /paste/show/:ID 来查看已创建的文档。
- 根据用户ID进行鉴权允许将文档设置为私密以防止他人查看。
Hydro的推荐架构如下
- handler.js: 用于处理路由
- model.js: 数据库模型
- lib.js: 不依赖于数据库等的库如md5函数
- script.js: 可能会被用户多次使用到的脚本如重新计算rp
- locale/: 翻译文件
- template/: 页面模板
- setting.yaml: 模块所用到的设置,格式在下方说明
## Step3 model.js
提示:由于模块中不便于使用 require() 引入 Hydro 的文件,可以从 global.Hydro 中取出需要的模块。
```js
const { db } = global.Hydro.service; // 数据库连接
const coll = db.collection('paste');
/**
* 添加一个文档
* @param {number} userId
* @param {string} content
* @param {boolean} isPrivate
* @return {Promise<string>}
*/
async function add(userId, content, isPrivate) {
const pasteId = String.random(16); // Hydro提供了此方法创建一个长度为16的随机字符串
// 使用 mongodb 为数据库驱动,相关操作参照其文档
const result = await coll.insertOne({
_id: pasteId,
owner: userId,
content,
isPrivate,
});
return result.insertedId; // 返回插入的文档ID
}
/**
* 查询一个文档
* @param {string} pasteId
* @return {Promise<any>}
*/
async function get(pasteId) {
return await coll.findOne({ _id: pasteId });
}
// 暴露这些接口
global.Hydro.model.pastebin = { add, get };
```
## Step4 handler.js
在路由中定义所有的函数应均为异步函数支持的函数有prepare, get, post, post[Operation], cleanup
具体流程如下:
```
先执行 prepare(args) (如果存在)
args 为传入的参数集合(包括 QueryString, Body, Path中的全部参数
再执行 prepare(args) (如果存在)
检查请求类型:
为 GET
-> 执行 get(args)
为 POST ?
-> 执行 post(args)
-> 含有 operation 字段?
-> 执行 post[Operation]
```
执行 cleanup()
如果在 this.response.template 指定模板则渲染,否则直接返回 this.response.body 中的内容。
* 在表单提交时的 operation 字段使用下划线,函数名使用驼峰命名。
`<input type="hidden" name="operation" value="confirm_delete">` 对应 `postConfirmDelete` 函数。
应当提供 `apply` 函数,并与定义的 Handler 一同挂载到 `global.Hydro.handler[模块名]` 位置。
`apply` 函数将在初始化阶段被调用。
```js
const { Route, Handler } = global.Hydro.service.server; // 注册路由所用工具
const { PRIV } = global.Hydro.model.builtin; // 内置 Privilege 权限节点
const pastebin = global.Hydro.model.pastebin; // 刚刚编写的pastebin模型
const { checkContent } = global.Hydro.lib.validator; // 用于检查用户输入是否合法
const { NotFoundError } = global.Hydro.error;
// 创建新路由
class PasteCreateHandler extends Handler {
// Get请求时触发该函数
async get() {
// 检查用户是否登录,此处为多余(因为底部注册路由时已声明所需权限)
// 此方法适用于权限的动态检查
// this.checkPriv(PRIV.PRIV_USER_PROFILE);
this.response.template = 'paste_create.html'; // 返回此页面
}
async post({ content, private = false }) { // 从用户提交的表单中取出content和private字段
checkContent(content); // 检查输入
// 在HTML表单提交的多选框中选中值为 'on',未选中则为空,需要进行转换
await pastebin.add(this.user._id, content, !!private);
// 将用户重定向到创建完成的url
this.response.redirect = this.url('paste_show', { id: pasteid });
}
}
class PasteShowHandler extends Handler {
async get({ id }) {
const doc = await pastebin.get(id);
if (!doc) throw new NotFoundError(id);
if (doc.isPrivate) {
if (this.user._id !== doc.owner) throw new PermissionError();
}
this.response.body = { doc };
this.response.template = 'paste_show.html';
}
async postDelete({ id }){
// 当提交表单并存在 operation 值为 delete 时执行。
// 本例中未实现删除功能,仅作为说明。
}
}
// Hydro会在服务初始化完成后调用该函数。
async function apply(){
// 注册一个名为 paste_create 的路由,匹配 '/paste/create'
// 使用PasteCreateHandler处理访问改路由需要PRIV.PRIV_USER_PROFILE权限
// 提示:路由匹配基于 path-to-regexp
Route('paste_create', '/paste/create', PasteCreateHandler, PRIV.PRIV_USER_PROFILE);
Route('paste_show', '/paste/show/:id', PasteShowHandler);
}
global.Hydro.handler.pastebin = apply;
```
对于 Typescript 用户的额外说明Hydro为Typescript提供了表单验证API。
`@param` 会修改 arguments首个参数为请求所在的 domainId剩余参数为指定的内容。
使用如下:
```ts
const { Handler, Route, Types, param } = global.Hydro.service.server;
const { user } = global.Hydro.model;
const { isPassword } = global.Hydro.lib.validator;
class UserLoginHandler extends Handler {
@param('username', Types.String)
@param('password', Types.String, isPassword)
async post(domainId:string, username:string, password:string) {
const udoc = await user.getByUname(username);
udoc.checkPassword(password);
this.response.body = { udoc };
}
}
```
## Step5 template
TODO
## Step6 Locale
用于提供多国翻译。格式与 Hydro 的 locale 文件夹格式相同。