mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
添加附加组件开发文档
This commit is contained in:
1 parent
f33690650f
commit
0f1f5f26e1
7 files changed
+242
-232
No files matched your search
Vendored
+1
-2
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"files.autoGuessEncoding": false,
|
||||
"files.autoSave": "onFocusChange",
|
||||
"files.encoding": "utf8",
|
||||
"files.eol": "\n",
|
||||
"editor.detectIndentation": true,
|
||||
"editor.detectIndentation": false,
|
||||
"editor.tabSize": 4,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.renderWhitespace": "boundary"
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
# Hydro
|
||||
|
||||
Hydro是一个高效的信息学在线测评系统。
|
||||
特点: 易于部署,轻量,功能强大。
|
||||
Hydro是一个高效的信息学在线测评系统。特点:易于部署,轻量,功能强大且易于扩展。
|
||||
|
||||
[中文文档](docs/deploy.md)
|
||||
[部署说明](docs/deploy.md)
|
||||
[附加组件开发说明](docs/addon.md)
|
||||
[Hydro开发说明](docs/development.md)
|
||||
[Hydro UI 传送门](https://github.com/hydro-dev/ui-default)
|
||||
|
||||
如果您认为本项目有价值,欢迎 star 。
|
||||
相关文档若说明的不够详细,请提交 Pull Request或联系开发组说明。
|
||||
bug和功能建议请在 Issues 提出。
|
||||
|
||||
[在 Gitpod 尝试本项目](https://gitpod.io/#https://github.com/hydro-dev/Hydro)
|
||||
[在 Gitpod 打开已配置完成的测试环境](https://gitpod.io/#https://github.com/hydro-dev/Hydro)
|
||||
|
||||
## 联系我们
|
||||
|
||||
QQ [3402182471](https://wpa.qq.com/msgrd?v=3&uin=3402182471&site=qq&menu=yes)
|
||||
hydro-dev 群:709572015
|
||||
Hydro 用户群:1085853538
|
||||
Telegram [@webpack_exports_undefined](https://t.me/webpack_exports_undefined)
|
||||
|
||||
## 鸣谢
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
@requireCsrfToken
|
||||
为model.system添加缓存
|
||||
修复discussion前端权限节点显示异常
|
||||
web端运行脚本似乎异常
|
||||
discussion前端权限节点显示异常?
|
||||
web端运行脚本异常?
|
||||
讨论回复通知
|
||||
比赛举办者发布通知
|
||||
举报功能
|
||||
SETTINGS_PRIVACY.allowRegisteredUsers
|
||||
model.setting.ui.name
|
||||
pdoc.secretConfig?
|
||||
在线IDE 更换颜色主题, fix resize
|
||||
move to @hydrooj/core?
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
# 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: 可能会被用户多次使用到的脚本(如重新计算rating)
|
||||
- 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 文件夹格式相同。
|
||||
@@ -36,6 +36,37 @@ Hydro 会自行初始化并监听 8888 端口(可使用 `--port=1234` 指定
|
||||
|
||||
之后的进阶配置可在 管理 面板进行。
|
||||
|
||||
## 附加组件
|
||||
|
||||
警告:附加组件对站点所有内容具有完全的访问权限。请不要安装来历不明的组件。
|
||||
|
||||
#### 安装附加组件:
|
||||
|
||||
先全局安装所需模块,再向 hydrooj 注册即可。 例:安装 @hydrooj/geoip
|
||||
|
||||
```sh
|
||||
yarn global add @hydrooj/geoip
|
||||
hydrooj addon add @hydrooj/geoip
|
||||
```
|
||||
|
||||
#### 附加组件列表
|
||||
|
||||
Hydro官方目前提供了以下附加组件:
|
||||
|
||||
| ID | 描述 | 大小 |
|
||||
| ---------------------- | ------------------------------ | ----- |
|
||||
| @hydrooj/ui-default | Hydro的默认用户界面 | ~10MB |
|
||||
| @hydrooj/geoip | GeoIP 支持,用于显示用户登录地 | ~60MB |
|
||||
| @hydrooj/migrate-vijos | 从vijos4的自动升级工具 | <1MB |
|
||||
| @hydrooj/hydrojudge | 评测组件 | ~2MB |
|
||||
|
||||
#### 卸载附加组件
|
||||
|
||||
```sh
|
||||
yarn global remove @hydrooj/geoip
|
||||
hydrooj addon remove @hydrooj/geoip
|
||||
```
|
||||
|
||||
## 杂项
|
||||
|
||||
[OAuth 配置](./oauth.md)
|
||||
+3
-205
@@ -1,206 +1,4 @@
|
||||
# Basic | basic
|
||||
# Hydro 开发
|
||||
|
||||
Hydro 后端使用 NodeJS 编写,前端使用 JQuery + React。
|
||||
代码风格遵循 airbnb 标准(详见 .eslintrc.js)。
|
||||
|
||||
由于模块中不能使用 require() 引入 Hydro 的文件,因此需要从 global.Hydro 中取出需要的模块。
|
||||
例:
|
||||
|
||||
```js
|
||||
const { db } = global.Hydro.service;
|
||||
const { problem } = global.Hydro.model;
|
||||
```
|
||||
|
||||
# Module | module
|
||||
|
||||
## 文件架构
|
||||
|
||||
Hydro 的模块由以下几个部分组成:
|
||||
|
||||
service: 服务
|
||||
script: 脚本
|
||||
handler: 访问路由
|
||||
lib: 库
|
||||
model: 数据库模型
|
||||
file: 额外文件
|
||||
locale: 多国化
|
||||
template: UI 模板
|
||||
README.md: 介绍
|
||||
hydro.json: 声明文件(必须)
|
||||
|
||||
## 模块编译
|
||||
|
||||
使用 `hydro-build` 进行编译。
|
||||
安装:`yarn add hydro-build -D`
|
||||
编译:`hydro-build`
|
||||
如果命运的齿轮没有出差错,您应该可以找到生成的.hydro文件了。
|
||||
|
||||
# Hydro.json | hydro
|
||||
|
||||
Hydro.json 是模块的声明文件,格式如下。
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "模块ID",
|
||||
"version": "模块版本",
|
||||
"description": "模块描述"
|
||||
}
|
||||
```
|
||||
|
||||
# Handler | handler
|
||||
|
||||
通常用于提供页面路由。
|
||||
|
||||
例:注册新路由:
|
||||
|
||||
```js
|
||||
const { Handler, Route } = global.Hydro.service.server;
|
||||
const { user, builtin } = global.Hydro.model;
|
||||
|
||||
class CustomHandler extends Handler {
|
||||
async prepare() {
|
||||
this.checkPriv(builtin.PRIV.PRIV_USER_PROFILE);
|
||||
// this.checkPerm(), this.user.hasPerm(), this.user.hasPriv(), etc.
|
||||
}
|
||||
|
||||
async get(){
|
||||
this.response.template = 'user_login.html';
|
||||
}
|
||||
|
||||
async post({ username, password }) {
|
||||
const udoc = await user.getByUname(username);
|
||||
udoc.checkPassword(password);
|
||||
this.response.body = { udoc };
|
||||
}
|
||||
|
||||
async postConfirm() {
|
||||
// 当提交表单并存在 operation 值为 confirm 时执行。
|
||||
}
|
||||
}
|
||||
|
||||
async function apply() {
|
||||
Route('/route/:username', CustomHandler);
|
||||
}
|
||||
|
||||
global.Hydro.handler.handlerName = apply;
|
||||
```
|
||||
|
||||
在路由中定义所有的函数应均为异步函数,支持的函数如下:
|
||||
|
||||
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` 函数将在初始化阶段被调用。
|
||||
|
||||
### 表单验证
|
||||
|
||||
若使用 Typescript 开发插件,可使用 Hydro 提供的验证工具。
|
||||
|
||||
`@param` 会修改 arguments,首个参数为请求所在的 domainId,剩余参数为指定的内容。
|
||||
|
||||
```ts
|
||||
const { Handler, Route, Types, param } = global.Hydro.service.server;
|
||||
const { user, builtin } = global.Hydro.model;
|
||||
|
||||
class CustomHandler extends Handler {
|
||||
async prepare() {
|
||||
this.checkPriv(builtin.PRIV.PRIV_USER_PROFILE);
|
||||
}
|
||||
|
||||
async get(){
|
||||
this.response.template = 'user_login.html';
|
||||
}
|
||||
|
||||
@param('username', Types.String)
|
||||
@param('password', Types.String)
|
||||
async post(domainId:string, username:string, password:string) {
|
||||
const udoc = await user.getByUname(username);
|
||||
udoc.checkPassword(password);
|
||||
this.response.body = { udoc };
|
||||
}
|
||||
}
|
||||
|
||||
export async function apply() {
|
||||
Route('/route/:username', CustomHandler);
|
||||
}
|
||||
|
||||
global.Hydro.handler.handlerName = apply;
|
||||
```
|
||||
|
||||
若使用 Javascript 开发插件,则可使用 `global.Hydro.lib.validator` 中提供的相关工具。
|
||||
|
||||
# Service | service
|
||||
|
||||
通常用于提供与其他程序对接的接口或启动其他外部程序。(如内置的 MongoDB / 外置的沙箱模块等)
|
||||
|
||||
# Lib | lib
|
||||
|
||||
库文件。通常用于提供一些功能(废话)。
|
||||
|
||||
# File | file
|
||||
|
||||
file 文件夹下的所有文件将被自动解压到 `$TMPDIR/hydro/模块ID/路径` 的位置(权限755,通常用于启动子进程)。
|
||||
|
||||
# Locale | locale
|
||||
|
||||
用于提供多国翻译。格式与 Hydro 的 locale 文件夹格式相同。
|
||||
|
||||
# Template | template
|
||||
|
||||
页面模板,使用 [nunjucks](https://mozilla.github.io/nunjucks/cn/templating.html) 语法。
|
||||
传入了 _ 翻译函数与 model 等。
|
||||
|
||||
```html
|
||||
{% set page_name = "page_name" %}
|
||||
{% extends "layout/basic.html" %}
|
||||
{% block content %}
|
||||
<div class="error__container clearfix">
|
||||
<div class="error__icon-container">
|
||||
<div class="error__twd2"></div>
|
||||
</div>
|
||||
<div class="error__text-container">
|
||||
<h1>{{ _('Oops!') }}</h1>
|
||||
<p>{{ _(error.message).format(error.params) }}</p>
|
||||
<p>{{ _('Technical Information') }}:</p>
|
||||
<p>{{ _('Type') }}: {{ error.code }}</p>
|
||||
<p>{{ _('Arguments') }}:
|
||||
<ol>
|
||||
{% for param in error.params %}
|
||||
<li>{{ param }}</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
<blockquote class="note">请不要覆盖已有模板。</blockquote>
|
||||
|
||||
# README.md | readme
|
||||
|
||||
项目的说明文件。
|
||||
Hydro 使用 Typescript 开发。代码风格遵循 airbnb 标准,indent=4spaces。
|
||||
详见 .eslintrc.js
|
||||
@@ -1,17 +0,0 @@
|
||||
const zlib = require('zlib');
|
||||
const fs = require('fs');
|
||||
|
||||
async function install() {
|
||||
if (!global.Hydro) throw new Error('Data missing');
|
||||
if (!fs.existsSync('hydro')) fs.mkdirSync('hydro');
|
||||
const hydro = JSON.parse(zlib.gunzipSync(global.Hydro).toString());
|
||||
fs.writeFileSync('app.js', hydro.app);
|
||||
for (const i in hydro.modules) {
|
||||
fs.writeFileSync(`${i}.hydro`, Buffer.from(hydro.modules[i], 'base64'));
|
||||
}
|
||||
}
|
||||
|
||||
install().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in new issue
Block a user