diff --git a/docusaurus.config.js b/docusaurus.config.js index 601cdfd0..7c0505f0 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -83,8 +83,6 @@ const config = { changefreq: null, ignorePatterns: [ "/next/**", - "/2.20/**", - "/2.21/**", "/2.22/**", "/2.23/**", "/2.24/**", diff --git a/versioned_docs/version-2.20/about.md b/versioned_docs/version-2.20/about.md deleted file mode 100644 index d0eb7313..00000000 --- a/versioned_docs/version-2.20/about.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: 关于文档 -description: 关于本文档站点的一些说明 ---- - -:::note -此文档使用 [Docusaurus](https://docusaurus.io/) 搭建,感谢 [Docusaurus](https://github.com/facebook/docusaurus) 社区所做的贡献。 -::: - -## 参与贡献 - -:::tip -如果你发现文档中有不正确或者需要添加的内容,非常欢迎参与到文档编辑当中。 -::: - -当前文档的仓库地址为 [halo-dev/docs](https://github.com/halo-dev/docs) ,所以你可以 fork 此仓库,修改之后提交 `Pull request` 等待我们合并即可。 diff --git a/versioned_docs/version-2.20/contribution/issue.md b/versioned_docs/version-2.20/contribution/issue.md deleted file mode 100644 index f1184855..00000000 --- a/versioned_docs/version-2.20/contribution/issue.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: 问题反馈 -description: 问题反馈渠道及指南 ---- - -:::info -如果您在使用过程中遇到了什么问题,您可以通过下面的方式反馈,但请尽量按照要求提出反馈。 -::: - -## GitHub Issues - -链接:[https://github.com/halo-dev/halo/issues](https://github.com/halo-dev/halo/issues) - -如果你在使用过程中,遇到了一些 bug 或者需要添加某些新特性,请尽量在 GitHub Issues 进行反馈,这非常有助于我们跟踪解决此问题,您也可以很方便的接收到处理状态。 - -建议步骤: - -1. 在 [Issues 列表](https://github.com/halo-dev/halo/issues) 搜索相关问题,看看是否有其他人已经提到了此问题。 -2. 如果当前还没有人遇到您类似的问题,那么请点击右上角的 `New issue` 按钮创建新的 issue。 -3. 选择正确的反馈类型。 -4. 请尽可能详细的按照模板填写内容。 -5. 点击 `Submit new issue` 提交 issue。 - -## Halo 官方社区 - -链接:[https://bbs.halo.run](https://bbs.halo.run) - -此平台主要目的用于与其他 Halo 用户进行交流。但如果您对 GitHub 不是很熟悉或者没有账号,您也可以在此平台进行反馈。 diff --git a/versioned_docs/version-2.20/contribution/pr.md b/versioned_docs/version-2.20/contribution/pr.md deleted file mode 100644 index 5f3b7e8a..00000000 --- a/versioned_docs/version-2.20/contribution/pr.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -title: 代码贡献 -description: 代码贡献指南 ---- - -欢迎关注并有想法参与 Halo 的开发,以下是关于如何参与到 Halo 项目的指南,仅供参考。 - -## 发现 Issue - -所有的代码尽可能都有依据(Issue),不是凭空产生。 - -### 寻找一个 Good First Issue - -> 这个步骤非常适合首次贡献者。 - -在 [halo-dev](https://github.com/halo-dev) 和 [halo-sigs](https://github.com/halo-sigs) 组织下,有非常多的仓库。每个仓库下都有可能包含一些“首次贡献者”友好的 Issue,主要是为了给贡献者提供一个友好的体验。该类 Issue -一般会用 `good-first-issue` 标签标记。标签 `good-first-issue` 表示该 Issue 不需要对 Halo 有深入的理解也能够参与。 - -请点击:[good-first-issue](https://github.com/issues?q=org%3Ahalo-dev+is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22+no%3Aassignee+) -查看关于 Halo 的 Good First Issue。 - -### 认领 Issue - -若对任何一个 Issue 感兴趣,请尝试在 Issue 进行回复,讨论解决 Issue 的思路。确定后可直接通过 `/assign` 或者 `/assign @GitHub 用户名` 认领这个 -Issue。这样可避免两位贡献者在同一个问题上花时间。 - -## 代码贡献步骤 - -1. Fork 此仓库 - - 点击 Halo 仓库主页右上角的 `Fork` 按钮即可。 - -2. Clone 仓库到本地 - - ```bash - git clone https://github.com/{YOUR_USERNAME}/halo --recursive - # 或者 git clone git@github.com:{YOUR_USERNAME}/halo.git --recursive - ``` - -3. 添加主仓库 - - 添加主仓库方便未来同步主仓库最新的 commits 以及创建新的分支。 - - ```bash - git remote add upstream https://github.com/halo-dev/halo.git - # 或者 git remote add upstream git@github.com:halo-dev/halo.git - git fetch upstream main - ``` - -4. 创建新的开发分支 - - 我们需要从主仓库的主分支创建一个新的开发分支。 - - ```bash - git checkout upstream/main - git checkout -b {BRANCH_NAME} - ``` - -5. 提交代码 - - ```bash - git add . - git commit -s -m "Fix a bug king" - git push origin {BRANCH_NAME} - ``` - -6. 合并主分支 - - 在提交 Pull Request 之前,尽量保证当前分支和主分支的代码尽可能同步,这时需要我们手动操作。示例: - - ```bash - git fetch upstream/main - git merge upstream/main - git push origin {BRANCH_NAME} - ``` - -## Pull Request - -进入此阶段说明已经完成了代码的编写,测试和自测,并且准备好接受 Code Review。 - -### 创建 Pull Request - -回到自己的仓库页面,选择 `New pull request` 按钮,创建 `Pull request` 到原仓库的 `main` 分支。 -然后等待我们 Review 即可,如有 `Change Request`,再本地修改之后再次提交即可。 - -提交 Pull Request 的注意事项: - -- 提交 Pull Request 请充分自测。 -- 每个 Pull Request 尽量只解决一个 Issue,特殊情况除外。 -- 应尽可能多的添加单元测试,其他测试(集成测试和 E2E 测试)可看情况添加。 -- 不论需要解决的 Issue 发生在哪个版本,提交 Pull Request 的时候,请将主仓库的主分支设置为 `main`。例如:即使某个 Bug 于 Halo 2.0.x 被发现,但是提交 Pull Request 仍只针对 - `main` 分支,等待 Pull Request 合并之后,我们会通过 `/cherrypick release-2.0` 或者 `/cherry-pick release-2.1` 指令将此 Pull Request - 的修改应用到 `release-2.0` 和 `release-2.1` 分支上。 - -### 更新 commits - -Code Review 阶段可能需要 Pull Request 作者重新修改代码,请直接在当前分支 commit 并 push 即可,无需关闭并重新提交 Pull Request。示例: - -```bash -git add . -git commit -s -m "Refactor some code according code review" -git push origin bug/king -``` - -同时,若已经进入 Code Review 阶段,请不要强制推送 commits 到当前分支。否则 Reviewers 需要从头开始 Code Review。 - -### 开发规范 - -请参考 [https://docs.halo.run/developer-guide/core/code-style](https://docs.halo.run/developer-guide/core/code-style) -,请确保所有代码格式化之后再提交。 diff --git a/versioned_docs/version-2.20/contribution/sponsor.md b/versioned_docs/version-2.20/contribution/sponsor.md deleted file mode 100644 index eede34e3..00000000 --- a/versioned_docs/version-2.20/contribution/sponsor.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: 赞助我们 -description: 如果 Halo 对你有帮助,不妨赞助我们 ---- - -## 为什么需要赞助 - -我们花费了大量的精力来维护这个软件,并且也提供了不少资金来维护服务器,域名等。因此我们需要赞助来节省部分开发成本。你的赞助不仅仅会被我们用来支付一些开发成本(比如服务器,OSS,域名等),还会让我们有更多的信心和精力投入到这个开源项目的开发中。从而让项目保持更加健康的成长以及迭代。 - -## 赞助形式 - -:::info -你可以通过任何形式对我们赞助,不限于资金。 -::: - -### 资金赞助 - -- 爱发电:[https://afdian.com/a/halo-dev](https://afdian.com/a/halo-dev) - -### 通过我们的推广链接购买服务器 - -如果你当前还没有购买服务器,可以考虑通过以下链接购买,这会为我们带来一部分收益。 - -- 阿里云:[https://www.aliyun.com/daily-act/ecs/activity_selection?userCode=j57gyupo](https://www.aliyun.com/daily-act/ecs/activity_selection?userCode=j57gyupo) -- 阿里云新人专享:[https://www.aliyun.com/minisite/goods?userCode=j57gyupo](https://www.aliyun.com/minisite/goods?userCode=j57gyupo) -- 腾讯云:[https://curl.qcloud.com/9Ogon25Y](https://curl.qcloud.com/9Ogon25Y) diff --git a/versioned_docs/version-2.20/developer-guide/annotations-form.md b/versioned_docs/version-2.20/developer-guide/annotations-form.md deleted file mode 100644 index b4ff90a1..00000000 --- a/versioned_docs/version-2.20/developer-guide/annotations-form.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -title: 元数据表单定义 ---- - -在 Halo 2.0,所有的模型都包含了 `metadata.annotations` 字段,用于存储元数据信息。元数据信息可以用于存储一些自定义的信息,可以等同于扩展字段。此文档主要介绍如何在 Halo 中为具体的模型定义元数据编辑表单,至于如何在插件或者主题模板中使用,请看插件或者主题的文档。 - -定义元数据编辑表单同样使用 `FormKit Schema`,但和主题或插件的定义方式稍有不同,其中输入组件类型可参考 [表单定义](./form-schema.md)。 - -:::info[提示] -因为 `metadata.annotations` 是一个键值都为字符串类型的对象,所以表单项的值必须为字符串类型。这就意味着,FormKit 的 `number`、`group`、`repeater` 等类型的输入组件都不能使用。 -::: - -## AnnotationSetting 资源定义方式 - -```yaml title="annotation-setting.yaml" -apiVersion: v1alpha1 -kind: AnnotationSetting -metadata: - name: my-annotation-setting -spec: - targetRef: - group: content.halo.run - kind: Post - formSchema: - - $formkit: "text" - name: "download" - label: "下载地址" - - $formkit: "text" - name: "version" - label: "版本" -``` - -以上定义为文章模型添加了两个元数据字段,分别为 `download` 和 `version`,分别对应了下载地址和版本号,最终效果: - -![Annotation Setting Preview](/img/annotation-setting/annotation-setting-preview.png) - -字段说明: - -1. `metadata.name`:唯一标识,命名规范可参考 [metadata name](./plugin/api-reference/server/extension.md#naming-spec-for-metadata-name),为了尽可能避免冲突,建议自定义前缀以及追加随机字符串,如:`theme-earth-post-wanfs5`。 -2. `spec.targetRef`:模型的关联,即为哪个模型添加元数据表单,目前支持的模型可查看下方的列表。 -3. `spec.formSchema`:表单的定义,使用 FormKit Schema 来定义。虽然我们使用的 YAML,但与 FormKit Schema 完全一致。 - -targetRef 支持列表: - -| 对应模型 | group | kind | -| ---------- | ---------------- | ---------- | -| 文章 | content.halo.run | Post | -| 自定义页面 | content.halo.run | SinglePage | -| 文章分类 | content.halo.run | Category | -| 文章标签 | content.halo.run | Tag | -| 菜单项 | `""` | MenuItem | -| 用户 | `""` | User | - -## 为多个模型定义表单 - -考虑到某些情况可能会同时为多个模型添加元数据表单,推荐在一个 `yaml` 文件中使用 `---` 来分割多个资源定义,如下: - -```yaml title="annotation-setting.yaml" -apiVersion: v1alpha1 -kind: AnnotationSetting -metadata: - name: my-annotation-setting -spec: - targetRef: - group: content.halo.run - kind: Post - formSchema: - - $formkit: "text" - name: "download" - label: "下载地址" - - $formkit: "text" - name: "version" - label: "版本" - ---- - -apiVersion: v1alpha1 -kind: AnnotationSetting -metadata: - name: my-annotation-setting -spec: - targetRef: - group: "" - kind: MenuItem - formSchema: - - $formkit: "text" - name: "icon" - label: "图标" -``` diff --git a/versioned_docs/version-2.20/developer-guide/core/build.md b/versioned_docs/version-2.20/developer-guide/core/build.md deleted file mode 100644 index ea87e21f..00000000 --- a/versioned_docs/version-2.20/developer-guide/core/build.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -title: 构建 -description: 构建为可执行 JAR 和 Docker 镜像的文档 ---- - -:::info -在此之前,我们推荐你先阅读[《准备工作》](./prepare),检查本地环境是否满足要求。 -::: - -一般情况下,为了保证版本一致性和可维护性,我们并不推荐自行构建和二次开发。 - -## 克隆项目 - -如果你已经 Fork 了相关仓库,请将以下命令中的 `halo-dev` 替换为你的 GitHub 用户名。 - -```bash -git clone https://github.com/halo-dev/halo - -# 或者使用 ssh 的方式 clone(推荐) -# git clone git@github.com:halo-dev/halo.git - -# 或者使用 GitHub CLI 克隆(推荐) -# gh repo clone halo-dev/halo - -# 或者使用 GitHub CLI Fork(推荐) -# gh repo fork halo-dev/halo - -cd halo - -# 切换到特定的分支或标签,请替换 ${branch_name} -git checkout ${branch_name} -``` - -## 构建 Fat Jar - -构建之前需要修改 `gradle.properties` 中的 `version` 属性(推荐遵循 [SemVer 规范](https://semver.org/)),例如:`version=2.20.0` - -```bash -cd path/to/halo -``` - -下载预设插件(可选): - -```bash -# Windows -./gradlew.bat downloadPluginPresets - -# macOS / Linux -./gradlew downloadPluginPresets -``` - -构建: - -```bash -# Windows -./gradlew.bat clean build -x check - -# macOS / Linux -./gradlew clean build -x check -``` - -构建完成之后,在 Halo 项目下产生的 `application/build/libs/halo-${version}.jar` 即为构建完成的文件。 - -最终部署文档可参考:[使用 JAR 文件部署](../../getting-started/install/jar-file.md)。 - -## 构建 Docker 镜像 - -在此之前,请确认已经构建好了 Fat Jar。 - -```bash -cd path/to/halo -``` - -```bash -# 请替换 ${tag_name} -docker build -t halo-dev/halo:${tag_name} . -``` - -```bash -# 插件构建完成的版本 -docker images | grep halo -``` - -最终部署文档可参考:[使用 Docker Compose 部署](../../getting-started/install/docker-compose.md)。 diff --git a/versioned_docs/version-2.20/developer-guide/core/code-style.md b/versioned_docs/version-2.20/developer-guide/core/code-style.md deleted file mode 100644 index c23fbc01..00000000 --- a/versioned_docs/version-2.20/developer-guide/core/code-style.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: 代码风格 -description: 代码风格的相关配置说明 ---- - -Halo 添加了 checkstyle 插件,来保证每位提交者代码的风格保持一致,减少无效代码的修改。本篇文章主要讲解如何在 IDEA 中添加 CheckStyle 插件,并引入项目所提供的 checkstyle.xml 配置。 - -## 安装 CheckStyle-IDEA - -- 进入 IDEA 插件市场。 -- 搜索 CheckStyle-IDEA,点击安装即可。 - -## 配置 CheckStyle - -- 进入 CheckStyle 配置(File | Settings | Tools | Checkstyle)。 -- 选择 Checkstyle 版本:9.3(以文件 `application/build.gradle` 中指定的版本为准)。 -- 在配置文件中点击添加按钮,配置描述可随便填写(推荐 Halo Checks),选择 ./config/checkstyle/checkstyle.xml,点击下一步和完成。 -- 勾选刚刚创建的配置文件。 - -## 配置 Editor - -- 进入编辑器配置(File | Settings | Editor | Code Style) - -- 导入 checkstyle.xm 配置: - -![image.png](https://www.halo.run/upload/2020/2/image-0c7a018e73f74634a534fa3ba8806628.png) - -- 选择 `./config/checkstyle/checkstyle.xml` 配置文件,点击确定即可。 - -至此,有关代码风格检查工具和格式化配置已经完成。 diff --git a/versioned_docs/version-2.20/developer-guide/core/framework.md b/versioned_docs/version-2.20/developer-guide/core/framework.md deleted file mode 100644 index 8196c276..00000000 --- a/versioned_docs/version-2.20/developer-guide/core/framework.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -title: Halo 架构概览 -description: Halo 架构概览 ---- - -Halo 是一个基于 Spring Boot 的 Java Web 应用,Web 层不再使用 Servlet 技术,而是充分向异步和非阻塞的反应式编程靠拢,使用 Netty 作为 Web 服务器,使用 [Reactor](https://projectreactor.io/) 作为异步编程框架,使用 R2DBC 作为数据库访问框架,使用 WebFlux 作为 Web 层框架。 - -Halo 由以下几个核心模块组成: - -- 安全模块:提供用户认证、授权、用户管理等功能。 -- 插件模块:提供插件管理、插件加载、插件通信、扩展点等功能。 -- 主题模块:提供主题管理、模板渲染、主题配置等功能。 -- 内置内容管理模块:提供文章、分类、标签、评论、附件、页面、菜单、设置等功能。 - -## Halo 核心概念和 Extension - -### 自定义模型 {#extension} - -Extension 自定义模型提供了一种声明和管理数据模型的方法,它是 Halo 的核心概念之一。Halo 中的所有数据模型都是通过 Extension 来定义的,包括文章、分类、标签、评论、附件、页面、菜单、设置等,这便于插件系统可以灵活的进行数据模型的扩展,设计文档参考:[自定义模型设计](https://github.com/halo-dev/rfcs/tree/main/extension)。 - -每个自定义模型都有三大类属性:metadata、spec、和 status。 - -1. metadata 用于标识自定义模型,每个自定义模型都至少有三个 metadata 属性:name、creationTimestamp、version,除此之外还有 labels 用于标识自定义模型的标签,annotations 用于存放扩展信息,deletionTimestamp 用于标识自定义模型是否被删除,finalizers 用于标识自定义模型的是否可回收。 -2. spec 描述用户期望达到的理想状态(Desired State),比如用户可以配置插件的 `spec.enable` 属性为 `true` 来启用插件或者为 `false` 来停用插件,这就是用户期望达到的理想状态,然后插件控制器会根据用户的期望状态来实现插件的启用或停用,它是声明式的,用户只需要声明期望状态,实际状态由具体的控制器来维护,最终达到用户期望的状态。 -3. status 描述当前实际状态(Actual State),比如用户可以通过 `status.phase` 属性来查看插件启用进行到了哪一步,中间过程可能包含多个步骤,比如插件解析、加载、资源准备等,这些步骤都是由插件控制器来实现的,它是实际状态,只要插件控制器还在运行,它就会一直更新状态,最终达到用户期望的状态。 - -每个自定义模型注册后都会默认生成 CRUD APIs,通过这些 APIs 就可以对自定义模型对象进行增删改查的操作,然后只需要编写控制器来实现自定义模型的业务逻辑即可,这就是 Halo 的异步编程模型。 - -### 控制器 {#controller} - -在 Halo 中,用户通过自定义模型定义资源的期望状态,Controller 负责监视资源的实际状态,当资源的实际状态和“期望状态”不一致时,Controller 则对系统进行必要的更改,以确保两者一致,这个过程被称之为调谐(Reconcile),而实现调谐的逻辑被称之为 Reconciler。Reconciler 获取对象的名称并返回是否需要重试(例如发生一些错误),如果需要重试,则 Controller 会在稍后再次调用 Reconciler,而这个过程会一直重复,直到 Reconciler 返回成功为止,这个过程被称之为调谐循环(Reconciliation Loop)。 - -### 自定义模型生命周期 {#extension-lifecycle} - -所有 Halo 的自定义模型对象都遵循一个共同的生命周期,可以将其视为状态机,尽管某些特定的自定义模型扩展了这一点并提供了更多状态。要编写正确的控制器,了解公共对象生命周期非常重要。 - -所有自定义模型对象都存在以下状态之一: - -- `DOES_NOT_EXIST`:Halo 不知道该对象。该状态不区分“尚未创建”和“已删除”。 -- `ACTIVE`:Halo 知道该对象并且该对象尚未被删除(未设置 `metadata.deletionTimestamp`)。在此状态下,任何更新操作(PUT、PATCH、服务器端处理等)都将导致相同的状态。 -- `DELETING`:Halo 知道该对象,该对象已被删除,但尚未完全删除。这可能是因为对象有一个或多个终结器(在 `metadata.finilizers` 中),客户端仍然可以访问该对象,并且可以看到它正在删除,因为设置了 `metadata.deleteTimestamp` 字段。当最后一个终结器被删除时,该对象将从存储中删除,并真正不存在。 - -下图描述了上述状态: - -```text - +---- object - | updated - v | - +----------+ | - | +----+ - object --------->| ACTIVE | - created | +-----------+ - | +---+------+ | - | | | - | | | -+------------+---+ | | -| | object deleted | -| |<--- without finalizers | -| | object deleted -| DOES_NOT_EXIST | with finalizers -| | | -| |<--- finalizers removed | -| | | | -+----------------+ | | - | | - | | - +---+------+ | - | | | - | DELETING |<----------+ - | | - +----------+ -``` - -总结:自定义模型对象的删除并不是立即生效的,而是需要经过两个步骤,第一步是将对象的 `metadata.deletionTimestamp` 字段设置为当前时间,第二步是将对象的 `metadata.finalizers` 字段设置为空,这样对象才会真正被删除,第一步是由用户发起的,第二步是由 Halo 控制器发起的。 - -### Secret {#secret} - -Secret 用于解决密码、token、密钥等敏感数据的配置问题,而不需要把这些敏感数据暴露到自定义模型的 Spec 中,或 API 响应中。 - -### ConfigMap {#configmap} - -ConfigMap 自定义模型用来保存 key-value pair 配置数据,这个数据可以在 Reconciler 里使用,或者被用来为插件或者主题存储配置数据。 - -虽然 ConfigMap 跟 Secret 类似,但是 ConfigMap 更方便的处理不含敏感信息的字符串。 - -### Setting {#setting} - -Setting 自定义模型用于提供用户配置声明,用户可以通过 Setting 来声明一些模板需要的配置,比如主题设置、插件设置、系统设置等都可以通过 Setting 来声明,就能在 UI 层面提供配置入口,用户可以通过 UI 来配置这些设置,而不需要修改配置文件。 - -### 基于角色的访问控制(RBAC){#rbac} - -Halo 使用基于角色的访问控制(Role-based Access Control,RBAC)来控制用户对资源的访问权限,RBAC 通过将角色分配给用户来实现访问控制,用户可以通过角色来访问资源,角色可以通过权限来访问资源。 - -RBAC 主要引入了角色(Role)和角色绑定(RoleBinding)的抽象概念,插件可以通过定义角色来提供用户对资源的分配入口,用户可以通过角色绑定来获取角色,从而获取资源的访问权限。 - -而对于底层角色,用户分配起来比较麻烦,因此 Halo 提供了**角色模板**的概念,通过将角色标记为模板来使用一组功能相关的角色,如文章查看的角色可能必须包含标签和分类的查看才算是一组完整的功能,因此可以将文章查看的角色标记为模板,并依赖标签和分类的查看角色,这样用户就可以通过角色模板来获取一组功能相关的角色,而不需要一个一个的分配角色。 diff --git a/versioned_docs/version-2.20/developer-guide/core/prepare.md b/versioned_docs/version-2.20/developer-guide/core/prepare.md deleted file mode 100644 index ca917bc2..00000000 --- a/versioned_docs/version-2.20/developer-guide/core/prepare.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: 准备工作 -description: 开发环境的准备工作 ---- - -## 环境要求 - -- [OpenJDK 17 LTS](https://github.com/openjdk/jdk) -- [Node.js 20 LTS](https://nodejs.org) -- [pnpm 9](https://pnpm.io/) -- [IntelliJ IDEA](https://www.jetbrains.com/idea/) -- [Git](https://git-scm.com/) -- [Docker](https://www.docker.com/)(可选) - -## 名词解释 - -### 工作目录 - -指 Halo 所依赖的工作目录,在 Halo 运行的时候会在系统当前用户目录下产生一个 halo-next 的文件夹,绝对路径为 ~/halo-next。里面通常包含下列目录或文件: - -1. `db`:存放 H2 Database 的物理文件,如果你使用其他数据库,那么不会存在这个目录。 -2. `themes`:里面包含用户所安装的主题。 -2. `plugins`:里面包含用户所安装的插件。 -5. `attachments`:附件目录。 -4. `logs`:运行日志目录。 diff --git a/versioned_docs/version-2.20/developer-guide/core/run.md b/versioned_docs/version-2.20/developer-guide/core/run.md deleted file mode 100644 index 6d12ac05..00000000 --- a/versioned_docs/version-2.20/developer-guide/core/run.md +++ /dev/null @@ -1,122 +0,0 @@ ---- -title: 开发环境运行 -description: 开发环境运行的指南 ---- - -:::info -在此之前,我们推荐你先阅读[《准备工作》](./prepare),检查本地环境是否满足要求。 -::: - -## 项目结构说明 - -目前如果需要完整的运行 Halo,总共需要三个部分: - -1. Halo 主项目([halo-dev/halo](https://github.com/halo-dev/halo)) -2. UI,包括 Console 控制台和 UC 个人中心(托管在 Halo 主项目) -3. 主题(Halo 主项目内已包含默认主题) - -:::info[说明] -从 Halo 2.11 开始,Halo 项目的 `ui` 目录同时包含了 Console(管理控制台)和 UC(个人中心),以下统称为 UI。 - -当前 Halo 主项目并不会将 UI 的构建资源托管到 Git 版本控制,所以在开发环境是需要同时运行 UI 项目的。当然,在我们的最终发布版本的时候会在 CI 中自动构建 UI 到 Halo 主项目。 -::: - -## 克隆项目 - -如果你已经 Fork 了相关仓库,请将以下命令中的 `halo-dev` 替换为你的 GitHub 用户名。 - -```bash -git clone https://github.com/halo-dev/halo - -# 或者使用 ssh 的方式 clone(推荐) -# git clone git@github.com:halo-dev/halo.git - -# 或者使用 GitHub CLI 克隆(推荐) -# gh repo clone halo-dev/halo - -# 或者使用 GitHub CLI Fork(推荐) -# gh repo fork halo-dev/halo -``` - -## 运行 UI 服务 - -```bash -cd path/to/halo/ui -pnpm install -pnpm build:packages -pnpm dev -``` - -最终控制台打印了如下信息即代表运行正常: - -```bash -VITE v4.2.3 ready in 638 ms - -# Console 控制台服务 -➜ Local: http://localhost:3000/console/ - -# UC 个人中心服务 -➜ Local: http://localhost:4000/uc/ -``` - -:::info[提示] -请不要直接使用 UI 的运行端口(3000 / 4000)访问,会因为跨域问题导致无法正常登录,建议按照后续的步骤以 dev 的配置文件运行 Halo,在 dev 的配置文件中,我们默认代理了 UI 页面的访问地址,所以后续访问 UI 页面使用 `http://localhost:8090/console` 和 `http://localhost:8090/uc` 访问即可,代理的相关配置: - -```yaml -halo: - console: - proxy: - endpoint: http://localhost:3000/ - enabled: true - uc: - proxy: - endpoint: http://localhost:4000/ - enabled: true -``` - -::: - -## 运行 Halo - -1. 在 IntelliJ IDEA 中打开 Halo 项目,等待 Gradle 初始化和依赖下载完成。 - -2. 下载预设插件(可选) - - ```bash - # Windows - ./gradlew.bat downloadPluginPresets - - # macOS / Linux - ./gradlew downloadPluginPresets - ``` - -3. 修改 IntelliJ IDEA 的运行配置 - - - Windows - - 将 Active Profiles 改为 `dev,win`,如图所示: - - ![IntelliJ IDEA Profiles](/img/developer-run/IntelliJ-IDEA-Profiles-Win.png) - - - macOS / Linux - - 将 Active Profiles 改为 `dev`,如图所示: - - ![IntelliJ IDEA Profiles](/img/developer-run/IntelliJ-IDEA-Profiles-macOS.png) - -4. 点击 IntelliJ IDEA 的运行按钮,等待项目启动完成。 - -5. 或者使用 Gradle 运行 - - ```bash - # macOS / Linux - ./gradlew bootRun --args="--spring.profiles.active=dev" - - # Windows - gradlew.bat bootRun --args="--spring.profiles.active=dev,win" - ``` - -6. 最终提供以下访问地址: - 1. 网站首页:[http://localhost:8090](http://localhost:8090) - 2. Console 控制台:[http://localhost:8090/console](http://localhost:8090/console) - 3. UC 个人中心:[http://localhost:8090/uc](http://localhost:8090/uc) diff --git a/versioned_docs/version-2.20/developer-guide/core/structure.md b/versioned_docs/version-2.20/developer-guide/core/structure.md deleted file mode 100644 index 66084b07..00000000 --- a/versioned_docs/version-2.20/developer-guide/core/structure.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: 系统结构 -description: Halo 项目的构成 ---- - -[Halo](https://github.com/halo-dev/halo) 博客系统分为以下四个部分: - -| 项目名称 | 简介 | -| :------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------- | -| [halo](https://github.com/halo-dev/halo) | 提供整个系统的服务,采用 [Spring Boot](https://spring.io/) 开发 | -| [halo-admin](https://github.com/halo-dev/halo-admin) | 负责后台管理的渲染,采用 [Vue](https://vuejs.org/) 开发,已集成在 Halo 运行包内,无需独立部署。 | -| [halo-comment](https://github.com/halo-dev/halo-comment) | 评论插件,采用 [Vue](https://vuejs.org/) 开发,在主题中运行方式引入构建好的 `JavaScript` 文件即可 | -| [halo-theme-\*](https://github.com/halo-dev) | 主题项目集,采用 [FreeMarker](https://freemarker.apache.org/) 模板引擎编写,需要包含一些特殊的配置才能够被 halo 所使用 | - -## 自定义配置 - -> 为什么要提前讲自定义配置呢?是因为在这里让大家了解到 `Halo` 的`配置方式`,以及`配置优先级`,不至于未来运行项目的时候不知道如何优雅地修改配置。 - -`Halo` 配置目录优先级如下(从上到下优先级越来越小,上层的配置将会覆盖下层): - -- `Halo` 自定义配置 - - file:~/.halo/ - - file:~/.halo-dev/ -- `Spring Boot` 默认配置 - - file:./config/ - - file:./ - - classpath:/config/ - - classpath:/ - -> 参考:[Application Property Files](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-application-property-files) - -在开发的时候,希望大家能够在 `~/halo-dev/application.yml` 中进行添加自定义配置。当然后面也会讲到如何用`运行参数` 和 `VM options` 进行控制配置,届时可根据具体情况进行选择。 - -:::warning -开发的时候,我们不建议直接更改`项目源码`中的所包含的`配置文件`,包括 `application.yml`、`application-dev.yml`、`application-test.yml` 和 `application-user.yml`。 -::: diff --git a/versioned_docs/version-2.20/developer-guide/form-schema.md b/versioned_docs/version-2.20/developer-guide/form-schema.md deleted file mode 100644 index 1a4e46f2..00000000 --- a/versioned_docs/version-2.20/developer-guide/form-schema.md +++ /dev/null @@ -1,588 +0,0 @@ ---- -title: 表单定义 ---- - -在 Halo 2.0,在 Console 端的所有表单我们都使用了 [FormKit](https://github.com/formkit/formkit) 的方案。FormKit 不仅支持使用 Vue 组件的形式来构建表单,同时支持使用 Schema 的形式来构建。因此,我们的 [Setting](https://github.com/halo-dev/halo/blob/87ccd61ae5cd35a38324c30502d4e9c0ced41c6a/src/main/java/run/halo/app/core/extension/Setting.java#L20) 资源中的表单定义,都是使用 FormKit Schema 来定义的,最常用的场景即主题和插件的设置表单定义。当然,如果要在 Halo 2.0 的插件中使用,也可以参考 FormKit 的文档使用 Vue 组件的形式使用,但不需要在插件中引入 FormKit。 - -此文档将不会介绍 FormKit 的具体使用教程,因为我们已经很好的集成了 FormKit,并且使用方式基本无异。此文章将介绍 Halo 2.0 中表单定义的一些规范,以及额外的一些输入组件。 - -FormKit 相关文档: - -- Form Schema: [https://formkit.com/essentials/schema](https://formkit.com/essentials/schema) -- FormKit Inputs: [https://formkit.com/inputs](https://formkit.com/inputs) - -:::tip -目前不支持 FormKit Pro 中的输入组件,但 Halo 额外提供了部分输入组件,将在下面文档列出。 -::: - -## Setting 资源定义方式 - -```yaml title="settings.yaml" -apiVersion: v1alpha1 -kind: Setting -metadata: - name: foo-setting -spec: - forms: - - group: group_1 - label: 分组 1 - formSchema: - - $formkit: radio - name: color_scheme - label: 默认配色 - value: system - options: - - label: 跟随系统 - value: system - - label: 深色 - value: dark - - label: 浅色 - value: light - - - group: group_2 - label: 分组 2 - formSchema: - - $formkit: text - name: username - label: 用户名 - value: "" - - $formkit: password - name: password - label: 密码 - value: "" -``` - -:::tip -需要注意的是,FormKit Schema 本身应该是 JSON 格式的,但目前我们定义一个表单所使用的是 YAML,可能在参考 FormKit 写法时需要手动转换一下。 -::: - -字段说明: - -1. `metadata.name`:设置资源的名称,建议以 `-setting` 结尾。 -2. `spec.forms`:表单定义,可以定义多个表单,每个表单都有一个 `group` 字段,用于区分不同的表单。 -3. `spec.forms[].label`:表单的标题。 -4. `spec.forms[].formSchema`:表单的定义,使用 FormKit Schema 来定义。虽然我们使用的 YAML,但与 FormKit Schema 完全一致。 - -## 组件类型 - -除了 FormKit 官方提供的常用输入组件之外,Halo 还额外提供了一些输入组件,这些输入组件可以在 Form Schema 中使用。 - -### select - -#### 描述 - -自定义的选择器组件,支持静态和动态数据源,支持多选等功能。 - -#### 参数 {#select-params} - -- `options`:静态数据源。当 `action` 存在时,此参数无效。 -- `action`:远程动态数据源的接口地址。 -- `requestOption`:动态数据源的请求参数,可以通过此参数来指定如何获取数据,适配不同的接口。当 `action` 存在时,此参数有效。 -- `remoteOptimize`:是否开启远程数据源优化,默认为 `true`。开启后,将会对远程数据源进行优化,减少请求次数。仅在动态数据源下有效。 -- `allowCreate`:是否允许创建新选项,默认为 `false`。仅在静态数据源下有效,需要同时开启 `searchable`。 -- `clearable`:是否允许清空选项,默认为 `false`。 -- `multiple`:是否多选,默认为 `false`。 -- `maxCount`:多选时最大可选数量,默认为 `Infinity`。仅在多选时有效。 -- `sortable`:是否支持拖动排序,默认为 `false`。仅在多选时有效。 -- `searchable`: 是否支持搜索,默认为 `false`。 -- `autoSelect`:当初始值不存在时,是否自动选择第一个选项,默认为 `true`。仅在单选时有效。 - -#### 参数类型定义 - -```ts -{ - options?: Array< - Record & { - label: string; - value: string; - } - >; - action?: string; - requestOption?: { - method?: "GET" | "POST"; - - /** - * 请求结果中 page 的字段名,默认为 `page`。 - */ - pageField?: PropertyPath; - - /** - * 请求结果中 size 的字段名,默认为 `size`。 - */ - sizeField?: PropertyPath; - - /** - * 请求结果中 total 的字段名,默认为 `total`。 - */ - totalField?: PropertyPath; - - /** - * 从请求结果中解析数据的字段名,默认为 `items`。 - */ - itemsField?: PropertyPath; - - /** - * 从 items 中解析出 label 的字段名,默认为 `label`。 - */ - labelField?: PropertyPath; - - /** - * 从 items 中解析出 value 的字段名,默认为 `value`。 - */ - valueField?: PropertyPath; - - /** - * 使用 value 查询详细信息时,fieldSelector 的查询参数 key,默认为 `metadata.name`。 - */ - fieldSelectorKey?: PropertyPath; - }; - remoteOptimize?: boolean; - allowCreate?: boolean; - clearable?: boolean; - multiple?: boolean; - maxCount?: number; - sortable?: boolean; - searchable?: boolean; -} -``` - -#### 静态数据示例 - -```yaml -- $formkit: select - name: countries - label: What country makes the best food? - sortable: true - multiple: true - clearable: true - searchable: true - placeholder: Select a country - options: - - label: China - value: cn - - label: France - value: fr - - label: Germany - value: de - - label: Spain - value: es - - label: Italy - value: ie - - label: Greece - value: gr -``` - -#### 远程动态数据示例 - -支持远程动态数据源,通过 `action` 和 `requestOption` 参数来指定如何获取数据。 - -请求的接口将会自动拼接 `page`、`size` 与 `keyword` 参数,其中 `keyword` 为搜索关键词。 - -```yaml -- $formkit: select - name: postName - label: Choose an post - clearable: true - action: /apis/api.console.halo.run/v1alpha1/posts - requestOption: - method: GET - pageField: page - sizeField: size - totalField: total - itemsField: items - labelField: post.spec.title - valueField: post.metadata.name - fieldSelectorKey: metadata.name -``` - -:::tip -当远程数据具有分页时,可能会出现默认选项不在第一页的情况,此时 Select 组件将会发送另一个查询请求,以获取默认选项的数据。此接口会携带如下参数: - -```ts -fieldSelector: `${requestOption.fieldSelectorKey}=(value1,value2,value3)` -``` - -其中,value1, value2, value3 为默认选项的值。返回值与查询一致,通过 `requestOption` 解析。 -::: - -### list - -#### 描述 - -列表类型的输入组件,支持动态添加、删除数据项。 - -#### 参数 - -- `item-type`:数据项的数据类型,用于初始化数据。可选参数 `string`, `number`, `boolean`, `object`,默认为 `string` -- `min`:数组最小要求数量,默认为 `0` -- `max`:数组最大容量,默认为 `Infinity`,即无限制 -- `addButton`:是否显示添加按钮 -- `addLabel`:添加按钮的文本 -- `upControl`:是否显示上移按钮 -- `downControl`:是否显示下移按钮 -- `insertControl`:是否显示插入按钮 -- `removeControl`:是否显示移除按钮 - -#### 示例 - -```yaml -- $formkit: list - name: socials - label: 社交账号 - addLabel: 添加账号 - min: 1 - max: 5 - itemType: string - children: - - $formkit: text - index: "$index" - validation: required -``` - -:::tip -`list` 组件有且只有一个子节点,并且必须为子节点传递 `index` 属性。若想提供多个字段,则建议使用 `group` 组件包裹,并将 itemType 改为 object。 -::: - -最终保存表单之后得到的值为以下形式: - -```json -{ - "socials": [ - "GitHub", - "Twitter" - ] -} -``` - -### verificationForm - -#### 描述 - -用于远程验证一组数据是否符合要求的组件。 - -#### 参数 - -- `action`:对目标数据进行验证的接口地址 -- `label`:验证按钮文本 -- `submitAttrs`:验证按钮的额外属性 - -#### 示例 - -```yaml -- $formkit: verificationForm - action: /apis/console.api.halo.run/v1alpha1/verify/verify-password - label: 账户校验 - children: - - $formkit: text - label: "用户名" - name: username - validation: required - - $formkit: password - label: "密码" - name: password - validation: required -``` - -:::tip -尽管 `verificationForm` 本身是一个输入组件,但与其他输入组件不同的是,它仅仅用于包装待验证的数据,所以并不会破坏原始数据的格式。例如上述示例中的值在保存后为: - -```json -{ - "username": "admin", - "password": "admin" -} -``` - -而不是 - -```json -{ - "verificationForm": { - "username": "admin", - "password": "admin" - } -} -``` - -::: - -示例中发送至验证地址的值为如下格式: - -```json -{ - "username": "admin", - "password": "admin" -} -``` - -当验证接口返回成功响应时,则验证通过,否则验证失败。 - -若用户在验证失败时想显示错误信息,可以在验证接口返回错误信息,该错误信息的结构定义需遵循 [RFC 7807 - Problem Details for HTTP APIs](https://datatracker.ietf.org/doc/html/rfc7807.html) 。例如: - -```json -{ - "title": "无效凭据", - "status": 401, - "detail": "用户名或密码错误。" -} -``` - -UI 效果: - - - -### repeater - -#### 描述 - -一组重复的输入组件,可以用于定义一组数据,最终得到的数据为一个对象的数组,可以方便地让使用者对其进行增加、移除、排序等操作。 - -#### 参数 - -- `min`:数组最小要求数量,默认为 `0` -- `max`:数组最大容量,默认为 `Infinity`,即无限制 -- `addButton`:是否显示添加按钮 -- `addLabel`:添加按钮的文本 -- `upControl`:是否显示上移按钮 -- `downControl`:是否显示下移按钮 -- `insertControl`:是否显示插入按钮 -- `removeControl`:是否显示移除按钮 - -#### 示例 - -```yaml -- $formkit: repeater - name: socials - label: 社交账号 - value: [] - max: 5 - min: 1 - children: - - $formkit: select - name: enabled - id: enabled - label: 是否启用 - options: - - label: 是 - value: true - - label: 否 - value: false - - $formkit: text - # 在 Repeater 中进行条件判断的方式,当 enabled 为 true 时才显示 - if: "$value.enabled === true", - name: name - label: 名称 - value: "" - - $formkit: text - if: "$value.enabled === true", - name: url - label: 地址 - value: "" -``` - -:::tip -使用 `repeater` 类型时,一定要设置默认值,如果不需要默认有任何元素,可以设置为 `[]`。 -::: - -其中 `name` 和 `url` 即数组对象的属性,最终保存表单之后得到的值为以下形式: - -```json -{ - "socials": [ - { - "name": "GitHub", - "url": "https://github.com/halo-dev" - } - ] -} -``` - -UI 效果: - - - -### attachment - -#### 描述 - -附件类型的输入框,支持直接调用附件库弹框选择附件。 - -#### 参数 - -- `accepts`:文件类型,数据类型为 `string[]` -- `min`:最小文件数量 -- `max`:最大文件数量 - -#### 示例 - -```yaml -- $formkit: attachment - name: logo - label: Logo - accepts: - - "image/png" - - "video/mp4" - - "audio/*" - value: "" -``` - -### code - -#### 描述 - -代码编辑器的输入组件,集成了 [Codemirror](https://codemirror.net/)。 - -#### 参数 - -- `language`:代码语言,目前支持 `yaml` `html` `javascript` `css` `json`。 -- `height`:代码编辑器的高度。 - -#### 示例 - -```yaml -- $formkit: code - name: footer_code - label: 页脚代码注入 - value: "" - language: yaml -``` - -### menuSelect - -#### 描述 - -菜单选择器,用于选择系统内的导航菜单,支持单选、多选、排序。 - -#### 示例 - -```yaml -- $formkit: menuSelect - name: menus - label: 菜单 - multiple: true - value: [] -``` - -:::info -menuSelect 基于 select,并兼容 select 的[参数](#select-params)。 -::: - -### menuCheckbox - -#### 描述 - -菜单复选框,用于选择系统内的导航菜单。其中选择的值为菜单资源 `metadata.name` 的集合。 - -#### 示例 - -```yaml -- $formkit: menuCheckbox - name: menus - label: 菜单 - value: [] -``` - -### menuRadio - -#### 描述 - -菜单单选框,用于选择系统内的导航菜单。其中选择的值为菜单资源 `metadata.name`。 - -#### 示例 - -```yaml -- $formkit: menuRadio - name: menu - label: 菜单 - value: "" -``` - -### postSelect - -#### 描述 - -文章选择器,用于选择系统内的文章。其中选择的值为文章资源 `metadata.name`。 - -#### 示例 - -```yaml -- $formkit: postSelect - name: post - label: 文章 - value: "" -``` - -### singlePageSelect - -#### 描述 - -单页选择器,用于选择系统内的独立页面。其中选择的值为独立页面资源 `metadata.name`。 - -#### 示例 - -```yaml -- $formkit: singlePageSelect - name: singlePage - label: 单页 - value: "" -``` - -### categorySelect - -#### 描述 - -文章分类选择器,用于选择系统内的文章分类。其中选择的值为文章分类资源 `metadata.name`。 - -#### 示例 - -```yaml -- $formkit: categorySelect - name: category - label: 分类 - value: "" -``` - -### categoryCheckbox - -#### 描述 - -文章分类复选框,用于选择系统内的文章分类。其中选择的值为文章分类资源 `metadata.name` 的集合。 - -#### 示例 - -```yaml -- $formkit: categoryCheckbox - name: categories - label: 分类 - value: [] -``` - -### tagSelect - -#### 描述 - -文章标签选择器,用于选择系统内的文章标签。其中选择的值为文章标签资源 `metadata.name`。 - -#### 示例 - -```yaml -- $formkit: tagSelect - name: tag - label: 标签 - value: "" -``` - -### tagCheckbox - -#### 描述 - -文章标签复选框,用于选择系统内的文章标签。其中选择的值为文章标签资源 `metadata.name` 的集合。 - -#### 示例 - -```yaml -- $formkit: tagCheckbox - name: tags - label: 标签 - value: [] -``` diff --git a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/extension-client.md b/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/extension-client.md deleted file mode 100644 index 0f11141f..00000000 --- a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/extension-client.md +++ /dev/null @@ -1,222 +0,0 @@ ---- -title: 与自定义模型交互 -description: 了解如何通过代码的方式操作数据 ---- - -Halo 提供了两个类用于与自定义模型对象交互 `ExtensionClient` 和 `ReactiveExtensionClient`。 - -它们提供了对自定义模型对象的增删改查操作,`ExtensionClient` 是阻塞式的用于后台任务如控制器中操作数据,而 `ReactiveExtensionClient` 返回值都是 Mono 或 Flux 是反应式非阻塞的,它们由 [reactor](https://projectreactor.io/) 提供。 - -```java -public interface ReactiveExtensionClient { - - // 已经过时,建议使用 listBy 或 listAll 代替 - Flux list(Class type, Predicate predicate, - Comparator comparator); - - // 已经过时,建议使用 listBy 或 listAll 代替 - Mono> list(Class type, Predicate predicate, - Comparator comparator, int page, int size); - - Flux listAll(Class type, ListOptions options, Sort sort); - - Mono> listBy(Class type, ListOptions options, - PageRequest pageable); - - /** - * Fetches Extension by its type and name. - * - * @param type is Extension type. - * @param name is Extension name. - * @param is Extension type. - * @return an optional Extension. - */ - Mono fetch(Class type, String name); - - Mono fetch(GroupVersionKind gvk, String name); - - Mono get(Class type, String name); - - /** - * Creates an Extension. - * - * @param extension is fresh Extension to be created. Please make sure the Extension name does - * not exist. - * @param is Extension type. - */ - Mono create(E extension); - - /** - * Updates an Extension. - * - * @param extension is an Extension to be updated. Please make sure the resource version is - * latest. - * @param is Extension type. - */ - Mono update(E extension); - - /** - * Deletes an Extension. - * - * @param extension is an Extension to be deleted. Please make sure the resource version is - * latest. - * @param is Extension type. - */ - Mono delete(E extension); -} -``` - -### 示例 - -如果你想在插件中根据 name 参数查询获取到 Person 自定义模型的数据,则可以这样写: - -```java -@Service -@RequiredArgsConstructor -public PersonService { - private final ReactiveExtensionClient client; - - Mono getPerson(String name) { - return client.fetch(Person.class, name); - } -} -``` - -或者使用阻塞式 Client - -```java -@Service -@RequiredArgsConstructor -public PersonService { - private final ExtensionClient client; - - Optional getPerson(String name) { - return client.fetch(Person.class, name); - } -} -``` - -注意:非阻塞线程中不能调用阻塞式方法。 - -我们建议你更多的使用响应式的 `ReactiveExtensionClient` 去替代 `ExtensionClient`。 - -### 查询 {#query} - -`ReactiveExtensionClient` 提供了两个方法用于查询数据,`listBy` 和 `listAll`。 - -`listBy` 方法用于分页查询数据,`listAll` 方法用于查询所有数据,它们都需要一个 `ListOptions` 参数,用于传递查询条件: - -```java -public class ListOptions { - private LabelSelector labelSelector; - private FieldSelector fieldSelector; -} -``` - -其中 `LabelSelector` 用于传递标签查询条件,`FieldSelector` 用于传递字段查询条件。 - -`FieldSelector` 支持比自动生成的 APIs 中更多的查询条件,可以通过 `run.halo.app.extension.index.query.QueryFactory` 来构建。 - -```java -ListOptions.builder() - .fieldQuery(QueryFactory.and( - QueryFactory.equal("name", "test"), - QueryFactory.equal("age", 18) - )) - .build(); -``` - -支持的查询条件如下: - -| 方法 | 说明 | 示例 | -| ---------------------------- | ---------------- | ----------------------------------------------------------------------------- | -| equal | 等于 | equal("name", "test"), name 是字段名,test 是字段值 | -| equalOtherField | 等于其他字段 | equalOtherField("name", "otherName"), name 是字段名,otherName 是另一个字段名 | -| notEqual | 不等于 | notEqual("name", "test") | -| notEqualOtherField | 不等于其他字段 | notEqualOtherField("name", "otherName") | -| greaterThan | 大于 | greaterThan("age", 18) | -| greaterThanOtherField | 大于其他字段 | greaterThanOtherField("age", "otherAge") | -| greaterThanOrEqual | 大于等于 | greaterThanOrEqual("age", 18) | -| greaterThanOrEqualOtherField | 大于等于其他字段 | greaterThanOrEqualOtherField("age", "otherAge") | -| lessThan | 小于 | lessThan("age", 18) | -| lessThanOtherField | 小于其他字段 | lessThanOtherField("age", "otherAge") | -| lessThanOrEqual | 小于等于 | lessThanOrEqual("age", 18) | -| lessThanOrEqualOtherField | 小于等于其他字段 | lessThanOrEqualOtherField("age", "otherAge") | -| in | 在范围内 | in("age", 18, 19, 20) | -| and | 且 | and(equal("name", "test"), equal("age", 18)) | -| or | 或 | or(equal("name", "test"), equal("age", 18)) | -| between | 在范围内 | between("age", 18, 20), 包含 18 和 20 | -| betweenExclusive | 在范围内 | betweenExclusive("age", 18, 20), 不包含 18 和 20 | -| betweenLowerExclusive | 在范围内 | betweenLowerExclusive("age", 18, 20), 不包含 18,包含 20 | -| betweenUpperExclusive | 在范围内 | betweenUpperExclusive("age", 18, 20), 包含 18,不包含 20 | -| startsWith | 以指定字符串开头 | startsWith("name", "test") | -| endsWith | 以指定字符串结尾 | endsWith("name", "test") | -| contains | 包含指定字符串 | contains("name", "test") | -| all | 指定字段的所有值 | all("age") | - -在 `FieldSelector` 中使用的所有字段都必须添加为索引,否则会抛出异常表示不支持该字段。关于如何使用索引请参考 [自定义模型使用索引](./extension.md#using-indexes)。 - -可以通过 `and` 和 `or` 方法组合和嵌套查询条件: - -```java -import static run.halo.app.extension.index.query.QueryFactory.and; -import static run.halo.app.extension.index.query.QueryFactory.equal; -import static run.halo.app.extension.index.query.QueryFactory.greaterThan; -import static run.halo.app.extension.index.query.QueryFactory.or; - -Query query = and( - or(equal("dept", "A"), equal("dept", "B")), - or(equal("age", "19"), equal("age", "18")) -); -ListOptions.builder() - .fieldQuery(query) - .build(); -``` - -### 构建 ListOptions - -ListOptions 提供了 `builder` 方法用于构建查询条件,`fieldQuery` 方法用于传递字段查询条件,`labelSelector` 方法用于传递标签查询条件。 - -```java -ListOptions.builder() - .labelSelector() - .eq("key-1", "value-1") - .end() - .fieldQuery(QueryFactory.equal("key-2", "value-2")) - .build(); -``` - -- `labelSelector` 之后使用 `end` 方法结束标签查询条件的构建。 -- `andQuery` 和 `orQuery` 用于组合多个 `FieldSelector` 查询条件。 - -### 排序 - -`listBy` 和 `listAll` 方法都支持传递 `Sort` 参数,用于传递排序条件。 - -```java -import org.springframework.data.domain.Sort; - -Sort.by(Sort.Order.asc("metadata.name")) -``` - -通过 `Sort.by` 方法可以构建排序条件,`Sort.Order` 用于指定排序字段和排序方式,`asc` 表示升序,`desc` 表示降序。 - -排序中使用的字段必须是添加为索引的字段,否则会抛出异常表示不支持该字段。关于如何使用索引请参考 [自定义模型使用索引](./extension.md#using-indexes)。 - -### 分页 - -`listBy` 方法支持传递 `PageRequest` 参数,用于传递分页条件。 - -```java -import run.halo.app.extension.PageRequestImpl; - -PageRequestImpl.of(1, 10); - -PageRequestImpl.of(1, 10, Sort.by(Sort.Order.asc("metadata.name")); - -PageRequestImpl.ofSize(10); -``` - -通过 `PageRequestImpl.of` 方法可以构建分页条件,具有两个参数的方法用于指定页码和每页数量,具有三个参数的方法用于指定页码、每页数量和排序条件。 - -`ofSize` 方法用于指定每页数量,页码默认为 1。 diff --git a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/extension-getter.md b/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/extension-getter.md deleted file mode 100644 index 80f0e348..00000000 --- a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/extension-getter.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: 获取扩展 -description: 了解如何在插件中使用 `ExtensionGetter` 获取扩展 ---- - -`ExtensionGetter` 用于获取和管理 Halo 或其他插件提供的扩展。它提供了多种方法来根据扩展点获取扩展,确保插件能够灵活地集成和使用各种扩展功能。 - -`ExtensionGetter` 接口的定义如下: - -```java -public interface ExtensionGetter { - - /** - * Get only one enabled extension from system configuration. - * - * @param extensionPoint is extension point class. - * @return implementation of the corresponding extension point. If no configuration is found, - * we will use the default implementation from application context instead. - */ - Mono getEnabledExtension(Class extensionPoint); - - /** - * Get the extension(s) according to the {@link ExtensionPointDefinition} queried - * by incoming extension point class. - * - * @param extensionPoint extension point class - * @return implementations of the corresponding extension point. - * @throws IllegalArgumentException if the incoming extension point class does not have - * the {@link ExtensionPointDefinition}. - */ - Flux getEnabledExtensions(Class extensionPoint); - - /** - * Get all extensions according to extension point class. - * - * @param extensionPointClass extension point class - * @param type of extension point - * @return a bunch of extension points. - */ - Flux getExtensions(Class extensionPointClass); -} -``` - -包含以下方法: - -1. `getEnabledExtension(Class extensionPoint)`: 获取一个在扩展设置中已启用的扩展。如果没有找到对应配置,将使用 Halo 中的默认扩展,如果 Halo 没有提供默认实现则找到一个由**已启用插件**提供的可用扩展。 -2. `getEnabledExtensions(Class extensionPoint)`: 根据传入的扩展点类获取所有已启用扩展。如果没有在扩展设置页面配置过则会返回所有可用的扩展。 -3. `getExtensions(Class extensionPointClass)`: 获取所有与扩展点类相关的扩展,无论是否在扩展设置中启用它。 - -:::tip -使用 `getEnabledExtension` 方法或者 `getEnabledExtensions` 方法取决于扩展点声明的 `type` 是 `SINGLETON` 还是 `MULTI_INSTANCE`。 - -通过使用 `ExtensionGetter`,开发者可以轻松地在插件中访问和管理各种扩展点,提升插件的功能和灵活性。 - -如果想了解 Halo 提供的扩展点请参考:[扩展点](../../extension-points/server/index.md)。 -::: - -### 示例 - -如果你想在插件中获取已启用的搜索引擎扩展,可以使用 `ExtensionGetter` 来获取: - -```java -@Service -@RequiredArgsConstructor -public class SearchService { - private final ExtensionGetter extensionGetter; - - Mono getSearchEngine() { - return extensionGetter.getEnabledExtension(SearchEngine.class) - } -} -``` diff --git a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/extension.md b/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/extension.md deleted file mode 100644 index 83d8ed6c..00000000 --- a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/extension.md +++ /dev/null @@ -1,690 +0,0 @@ ---- -title: 自定义模型 -description: 了解什么是自定义模型及如何创建 ---- - -## 概述 - -Halo 自定义模型是参考自 [Kubernetes CRD](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/) 的一种灵活可扩展的数据存储和使用方式,旨在为插件开发者提供自定义数据支持。 -自定义模型遵循 [OpenAPI v3](https://spec.openapis.org/oas/v3.1.0),便于开发者在插件中存储、读取和操作自定义数据。 -详情请参考 [自定义模型设计](https://github.com/halo-dev/rfcs/tree/main/extension)。 - -### 示例 {#person-extension-example} - -以下是一个典型的自定义模型代码示例: - -```java -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.ToString; -import run.halo.app.extension.AbstractExtension; -import run.halo.app.extension.GVK; -import run.halo.app.extension.GroupKind; - -@Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) -@GVK(group = "my-plugin.halo.run", - version = "v1alpha1", - kind = "Person", - plural = "persons", - singular = "person") -public class Person extends AbstractExtension { - - @Schema(requiredMode = Schema.RequiredMode.REQUIRED) - private Spec spec; - - @Data - @Schema(name = "PersonSpec") - public static class Spec { - @Schema(description = "The description on name field", maxLength = 100) - private String name; - - @Schema(description = "The description on age field", maximum = "150", minimum = "0") - private Integer age; - - @Schema(description = "The description on gender field") - private Gender gender; - - private Person otherPerson; - } - - public enum Gender { - MALE, FEMALE, - } -} -``` - -## 创建自定义模型步骤 {#create-extension} - -创建一个自定义模型需要以下三个步骤: - -1. **继承 `AbstractExtension` 类**:创建一个类继承 `run.halo.app.extension.AbstractExtension`。 -2. **使用 `GVK` 注解**:通过 `GVK` 注解定义自定义模型的基本信息,包括 group、version、kind 等。 -3. **注册自定义模型**:在插件的 `start()` 生命周期方法中注册自定义模型。 - -```java -@Autowired -private SchemeManager schemeManager; - -@Override -public void start() { - schemeManager.register(Person.class); -} -``` - -### `GVK` 注解详解 - -- **group**:表示自定义模型所属的组,通常采用域名形式,建议使用你的组织或公司拥有的子域名。例如 `widget.mycompany.com`。 -- **version**:API 的版本,通常用于与 group 组合形成 `apiVersion`,例如`api.halo.run/v1alpha1`。 -- **kind**:标识自定义模型的类型,即资源的 REST 表示形式。 -- **plural**/**singular**:自定义资源的复数和单数名称,用于在 API 路径中标识资源类型。 - - singular: 必须全部小写,通常是将 `kind` 的值转换为小写作为 `singular` 的值。 - - plural:自定义资源在 `/apis///.../` 下提供,必须为全部小写,通常是将 `kind` 的值转换为小写并转为复数形式作为 `plural` 的值。 - -### 自定义模型定义结构 - -一个自定义模型通常包含以下几部分: - -- `apiVersion`:标识 API 版本,由 `GVK` 注解的 `group` 和 `version` 组合而成。 -- `kind`:标识自定义模型类型。 -- `metadata`:[Metadata](#metadata) 类型,用于存储模型的元数据,如名称、创建时间。 -- `spec`:声明自定义模型对象的期望状态。它是声明式的,用户只需要声明期望状态,实际状态由具体的控制器来维护,最终达到用户期望的状态。 -- `status`:描述自定义模型对象资源的实际状态。 - -`apiVersion`、`kind` 和 `metadata` 已包含在 `AbstractExtension` 类中,开发者只需关注 `spec` 和 `status` 即可。 - -#### Metadata - -自定义模型的 Metadata 包含以下属性: - -- `name`: 用于标识自定义模型的名称。 -- `creationTimestamp`: 用于标识自定义模型的创建时间,无法修改,只在创建时自动生成。 -- `version`: 用于标识自定义模型的数据乐观锁版本,无法修改,由更新时自动填充,如果更新时指定了 `version` 且与当前 `version` 不一致则会更新失败。 -- `deletionTimestamp`: 用于标识自定义模型的删除时间,表示此自定义模型对象被声明为删除,此时仍然可以通过 API 访问到此对象,参考 [自定义模型对象生命周期](../../../core/framework.md#extension-lifecycle) -- `finalizers`: 用于标识终结器,它是一个字符串集合,用于标识自定义模型对象是否可回收,参考 [自定义模型对象生命周期](../../../core/framework.md#extension-lifecycle) -- `labels`: 用于标识自定义模型的标签,它是一个字符串键值对集合,用于标识自定义模型对象的标签,可以通过标签来查询自定义模型对象。 -- `annotations`: 用于存放扩展信息,它是一个字符串键值对集合,用于存放自定义模型对象的扩展信息。 - -## 声明自定义模型对象 {#declare-extension-object} - -在创建了自定义模型之后,可以通过在插件项目的 `src/main/resources/extensions` 目录下编写 `yaml` 文件来声明自定义模型对象。示例如下: - -```yaml -apiVersion: my-plugin.halo.run/v1alpha1 -kind: Person -metadata: - name: fake-person -spec: - name: halo - age: 18 - gender: male -``` - -该目录下所有的 `yaml` 文件中声明的自定义模型对象都会**在插件启动后被创建/更新**,文件名是任意的,只需根据 `kind` 和 `apiVersion` 确定类型。 -基于这个特性,开发者可以将一些**初始化资源**的声明放在这个目录下,以便在插件启动时自动创建。但需要注意的是,如果资源如配置等能被用户修改,则不应该放在这个目录下,因为这些资源会在插件启动时被强制覆盖。 - -## 校验自定义模型对象 {#validate-extension-object} - -Halo 使用 [OpenAPI v3](https://spec.openapis.org/oas/v3.1.0) 标准来定义自定义模型。 -OpenAPI 规范定义了自定义模型的数据结构、字段属性及其校验规则,然后将其转换为 JSON Schema,注册到 Halo 的 SchemeManager 中。 - -使用 `@Schema` 注解可以为自定义模型的字段添加校验规则,`@Schema` 是 OpenAPI 提供的一个注解,通过这个注解,我们可以在生成的 OpenAPI 文档中展示字段的详细信息(如名称、描述、类型、是否必填等),同时也可以对字段进行一定的校验,比如限制字段的最大长度、最小值、格式等。 - -### 基本用法 - -`@Schema` 注解中有许多可用的属性,用来对字段进行更加细致的校验和文档说明。下面是一些常用的属性: - -- description:字段的描述信息,用于在文档中展示。 -- example:字段的示例值。 -- requiredMode:是否必填字段。 -- minLength:字符串字段的最小长度。 -- maxLength:字符串字段的最大长度。 -- minimum:数值字段的最小值。 -- maximum:数值字段的最大值。 -- format:字段的格式,常用于指定日期、时间、邮箱等特殊格式。 - -例如,如果我们有一个电子邮件字段,并且想要校验它的格式,可以这样定义: - -```java -@Schema(description = "用户电子邮箱", example = "user@example.com", format = "email") -private String email; -``` - -当用户向 API 提交一个自定义模型对象时,Halo 会根据自定义模型中定义的 OpenAPI `@Schema` 注解对对象进行以下几个步骤的校验: - -1. **基本结构校验**:验证对象的字段结构是否符合定义的 OpenAPI 模式,例如字段类型是否正确、是否存在必填字段等。 -2. **字段约束校验**:针对特定字段的约束条件(如最小值、最大长度、正则表达式等)进行校验,确保字段值符合条件。 -3. **成功或失败**:如果校验通过,Halo 接受并存储该对象;如果校验失败,会返回详细的错误信息,说明哪些字段不符合要求 - -参考示例 [Person](#person-extension-example)。 - -## 使用索引 {#using-indexes} - -为了让插件可以方便的定义自定义模型定义,而不需要考虑操作数据库表的细节且可以切换存储介质如可以使用 `MySQL`,`PostgreSQL`,`H2` 等数据库来来作为存储介质,数据存储使用 `byte[]` 的形式,这使得无法利用数据库的原生索引来提高查询效率。 - -Halo 提供了一套索引机制,开发者可以通过注册自定义模型时声明索引来提高查询效率。 - -示例: - -```java -import static run.halo.app.extension.index.IndexAttributeFactory.multiValueAttribute; -import static run.halo.app.extension.index.IndexAttributeFactory.simpleAttribute; - -@Override -public void start() { - schemeManager.register(Moment.class, indexSpecs -> { - indexSpecs.add(new IndexSpec() - .setName("spec.tags") - // multiValueAttribute 用于得到一个返回多个值的索引函数 - .setIndexFunc(multiValueAttribute(Moment.class, moment -> { - var tags = moment.getSpec().getTags(); - return tags == null ? Set.of() : tags; - })) - // simpleAttribute 用于得到一个返回单个值的索引函数,可以返回 null - indexSpecs.add(new IndexSpec() - .setName("spec.owner") - .setIndexFunc( - simpleAttribute(Moment.class, moment -> moment.getSpec().getOwner()))); - ); -} -``` - -`IndexSpec` 用于声明索引项,它包含以下属性: - -- name:索引名称,在同一个自定义模型的索引中必须唯一,一般建议使用字段路径作为索引名称,例如 `spec.slug`。 -- order:对索引值的排序方式,支持 `ASC` 和 `DESC`,默认为 `ASC`。 -- unique:是否唯一索引,如果为 `true` 则索引值必须唯一,如果创建自定义模型对象时检测到此索引字段有重复值则会创建失败。 -- indexFunc:索引函数,用于获取索引值,接收当前自定义模型对象,返回一个索引值,索引值必须是字符串任意类型,如果不是字符串类型则需要自己转为字符串,可以使用 `IndexAttributeFactory` 提供的静态方法来创建 `indexFunc`: - - `simpleAttribute()`:用于得到一个返回单个值的索引函数,例如 `moment -> moment.getSpec().getSlug()`。 - - `multiValueAttribute()`:用于得到一个返回多个值的索引函数,例如 `moment -> moment.getSpec().getTags()`。 - -当注册自定义模型时声明了索引,Halo 会在插件启动时构建索引,在构建索引期间插件处于未启动状态。 - -Halo 默认会为每个自定义模型建立以下几个索引,因此不需要为下列字段再次声明索引: - -- `metadata.name` 创建唯一索引 -- `metadata.labels` -- `metadata.creationTimestamp` -- `metadata.deletionTimestamp` - -创建了索引的字段可以在查询时使用 `fieldSelector` 参数来查询,参考 [自定义模型 API](#extension-apis)。 - -:::tip - -- 索引是一种存储数据结构,可提供对数据集中字段的高效查找。 -- 索引将自定义模型中的字段映射到数据库行,以便在查询特定字段时不需要完整的扫描。 -- 查询数据之前,必须对需要查询的字段创建索引。 -- 索引可以包含一个或多个字段的值,可以包含唯一值或重复值。索引中的值按照索引中的顺序进行排序。 -- 索引可以提高查询性能,但会占用额外的存储空间,因为它们需要存储索引字段的副本。索引的大小取决于字段的数据类型和索引的类型,因此,创建索引时应该考虑存储成本和性能收益。 - ::: - -## 命名规范 - -### `metadata.name` {#naming-spec-for-metadata-name} - -`metadata.name` 是自定义模型对象的唯一标识名,需遵循以下规则: - -- 不超过 253 个字符。 -- 只能包含小写字母、数字和 `-`,且以字母或数字开头和结尾。 - -### `labels` {#naming-spec-for-labels} - -`labels` 是一个字符串键值对集合,用于标识模型的标签,格式为 `/`。例如,`halo.run/post-slug`。遵循以下规则: - -- 前缀是可选的,通常是反向的域名表示形式,用于避免键名冲突。 -- 名称必须是合法的 DNS 标签,最多 63 个字符,且以字母数字字符开头和结尾。 - -建议保持标签的命名简洁易懂,在整个项目中保持一致性,不包含敏感信息。 - -**需要注意的是**,`metadata.labels` 被用于通过标签查询自定义模型对象。**它会被自动创建索引**,因此使用时需谨慎,避免索引过多导致性能问题,对于不需要索引的额外字段,可以使用 `metadata.annotations`。 - -#### labels 命名规范 - -前缀规则: - -- 如果 label 用于特定于一个组织的资源,建议使用一个前缀,如 `plugin.halo.run/plugin-name`。 -- 前缀必须是一个有效的 DNS 子域名(参考 metadata.name),且最多可包含 253 个字符。 -- 保留了不带前缀的 label 键以及特定前缀(如 halo.run),因此插件不可使用。 - -名称规则: - -- 名称必须是合法的 DNS 标签,最多可包含 63 个字符。 -- 必须以字母数字字符开头和结尾。 -- 可以包含 `-`、`.`、`_` 和`字母数字`字符。 - -通用规范: - -- 避免使用容易引起混淆或误解的键名。 -- 尽量保持简洁明了,易于理解。 -- 使用易于记忆和识别的单词或缩写。 - -一致性和清晰性: - -- 在整个项目或组织中保持一致的命名约定。 -- labels 应直观地反映其代表的信息或用途。 -- 不要在 labels 中包含敏感信息,例如用户凭据或个人识别信息。 - -### `annotations` {#naming-spec-for-annotations} - -`annotations` 是一个字符串键值对集合,用于存放扩展信息,命名规则与 `labels` 相同。 - -可以使用 `metadata.annotations` 存放一些额外的信息,如 JSON 数据、配置信息等。 - -## 自定义模型 API {#extension-apis} - -定义并注册自定义模型后,Halo 会根据 `GVK` 注解自动生成一组 `CRUD` APIs。 - -生成 APIs 的规则为:`/apis////{extensionname}/` - -例如,`Person` 自定义模型将有以下 APIs: - -- `GET /apis/my-plugin.halo.run/v1alpha1/persons`:列出所有对象。 -- `GET /apis/my-plugin.halo.run/v1alpha1/persons/{name}`:根据名称查询对象。 -- `POST /apis/my-plugin.halo.run/v1alpha1/persons`:创建对象。 -- `PUT /apis/my-plugin.halo.run/v1alpha1/persons/{name}`:更新对象。 -- `DELETE /apis/my-plugin.halo.run/v1alpha1/persons/{name}`:删除对象。 - -其中,**列出所有对象**的 API 支持以下参数: - -- **page**:页码,从 1 开始。 -- **size**:每页的数据量。 -- **sort**:排序字段,格式为 `字段名,排序方式`,例如 `name,desc`,可传递多个排序字段,排序使用的字段必须是注册为索引的字段。 -- **labelSelector**:标签选择器,用于筛选特定标签的对象。详见 [标签选择器参数规则](#label-selector-query-params)。 -- **fieldSelector**:字段选择器,用于筛选注册为索引的字段。详见 [字段选择器参数规则](#field-selector-query-params)。 - -示例: - -```shell -GET /apis/my-plugin.halo.run/v1alpha1/persons?page=1&size=10&sort=name,desc&labelSelector=name=halo&fieldSelector=spec.slug=halo -``` - -表示查询 `metadata.labels` 中 `name` 的值等于 `halo` 且 `spec.slug` 的值等于 `halo` 的自定义模型对象,并按照 `name` 字段降序排序,查询第 1 页,每页 10 条数据。 - -### 自定义模型 API 业务逻辑 - -自动生成的 `CRUD` APIs 不仅只是简单的数据操作,你可以通过定义[控制器](../../../core/framework.md#controller) 来实现对数据的业务逻辑处理。 - -自定义模型控制器是专门为自定义模型设计的,它允许用户通过自定义逻辑来响应自定义模型对象的变化,执行自动化操作,从而扩展这组自动生成 APIs 的功能。 - -自定义模型控制器通常会: - -- 监控自定义模型的变化:当某个自定义模型的对象被创建、更新或删除时,控制器会被触发,读取该对象的状态信息。 -- 执行特定的业务逻辑:根据自定义模型的状态和需求,控制器可以执行某些动作,如创建或删除其他资源,或调用外部系统进行处理。 -- 维护资源的期望状态:控制器的目标是确保自定义模型的状态符合期望状态,维护资源的稳定性。 - -参考 [自定义模型控制器](../../../core/framework.md#controller) 文档。 - -### 选择器参数规则 - -#### 标签选择器 {#label-selector-query-params} - -`labelSelector`:标签选择器,用于筛选特定标签的对象,支持以下操作符: - -- `=` 表示等于,例如 `labelSelector=name=halo` 表示查询 `metadata.labels` 中 `name` 的值等于 `halo` 的自定义模型对象。 -- `!=` 表示不等于,例如 `labelSelector=name!=halo` 表示查询 `metadata.labels` 中 `name` 的值不等于 `halo` 的自定义模型对象。 -- `!` 表示不存在 key,例如 `labelSelector=!name` 表示查询 `metadata.labels` 不存在 `name` 这个 key 的自定义模型对象。 -- `存在检查` 表示查询存在 key 的对象,例如 `labelSelector=name` 表示查询 `metadata.labels` 存在 `name` 这个 key 的自定义模型对象。 - -#### 字段选择器 {#field-selector-query-params} - -`fieldSelector`:字段选择器,格式与 `labelSelector` 类似,但需要确保对应的字段是注册为索引的字段。 - -例如 `fieldSelector=spec.name=slug` 表示查询 `spec.slug` 的值等于 `halo` 的自定义模型对象。 - -支持的操作符有 `=`、`!=` 和 `in`: - -- `=` 表示等于,例如 `fieldSelector=spec.slug=halo` 表示查询 `spec.slug` 的值等于 `halo` 的自定义模型对象。 -- `!=` 表示不等于,例如 `fieldSelector=spec.slug!=halo` 表示查询 `spec.slug` 的值不等于 `halo` 的自定义模型对象。 -- `in` 表示在集合中,例如 `fieldSelector=spec.slug=(halo,halo2)` 表示查询 `spec.slug` 的值为 `halo` 或 `halo2` 的自定义模型对象。 - -## 自定义 API - -对于自动生成的 `CRUD` APIs 不能满足的场景,开发者可以通过定义自定义 API 来扩展功能。 - -推荐使用 [Spring Webflux](https://docs.spring.io/spring-framework/reference/web/webflux-functional.html) 的 `Functional Endpoints` 来编写轻量级自定义 APIs: - -```java -RouterFunction route = route() - .GET("/persons/{name}", accept(APPLICATION_JSON), this::getPerson) - .POST("/persons", this::createPerson) - .build(); -``` - -- **HandlerFunction**:用于处理请求,接收 `ServerRequest` 并返回 `ServerResponse`。 -- **RouterFunction**:将请求路由到相应的处理函数。 - -这样开发者可以灵活定义符合业务需求的 APIs,方便地扩展插件的功能。 - -自定义 APIs 与自动生成的 APIs 一样,都应该遵循以下规范: - -`/apis////{extensionname}/` - -路径不超过 7 段,如果超过则应当以参数的形式传递或改进路径设计,否则无法适应角色模板的规则,参考 [角色模板](../../security/role-template.md#resource-rules)。 - -### 自定义 API 的 Group 规则 {#custom-api-group-spec} - -为了确保插件定义的自定义 APIs 不与`其他插件的 APIs / 自动生成的 APIs` 冲突,Halo 约定通过不同的 group 来区分,遵循以下规则: - -- 在 Console 端使用的自定义 API 的 group 规则遵循 `console.api.`。 -- 在个人中心使用的自定义 API 的 group 规则遵循 `uc.api.`,例如 `uc.api.my-plugin.halo.run`。 -- 为主题端提供的公开的自定义 API 的 group 规则建议为 `api.`,例如 `api.my-plugin.halo.run`。 - -其中 `` 为自定义模型的 `GVK` 注解中的 `group`。 - -例如,`Person` 自定义模型需要提供一个在 Console 使用的自定义 API,可以定义如下: - -```java -// my-plugin.halo.run 为 Person 自定义模型的 group -// console.api. 为 Console 端自定义 API 的 group 前缀 -RouterFunction route = route() - .GET("/apis/console.api.my-plugin.halo.run/v1alpha1/persons/{name}", - accept(APPLICATION_JSON), this::getPerson) - .build(); -``` - -### CustomEndpoint 接口 - -根据 [自定义 API 的 Group 规则](#custom-api-group-spec) 约定,开发者需要在自定义 API 的路径中包含 `console.api.`,这样会导致 API 路径变得冗长。 - -为了简化 API 路径写法,Halo 提供了 `run.halo.app.core.extension.endpoint.CustomEndpoint` 接口,开发者可以通过实现该接口来定义自定义 APIs: - -```java -import static org.springframework.http.MediaType.APPLICATION_JSON; - -import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.server.RequestPredicates; -import org.springframework.web.reactive.function.server.RouterFunction; -import org.springframework.web.reactive.function.server.RouterFunctions; -import org.springframework.web.reactive.function.server.ServerRequest; -import org.springframework.web.reactive.function.server.ServerResponse; -import reactor.core.publisher.Mono; -import run.halo.app.core.extension.endpoint.CustomEndpoint; -import run.halo.app.extension.GroupVersion; - -@Component -public class PersonEndpoint implements CustomEndpoint { - - @Override - public RouterFunction endpoint() { - return RouterFunctions.route() - .GET("/persons/{name}", - RequestPredicates.accept(APPLICATION_JSON), this::getPerson) - // more routes - .build(); - } - - private Mono getPerson(ServerRequest request) { - return ServerResponse.ok().bodyValue("Hello, " + request.pathVariable("name")); - } - - @Override - public GroupVersion groupVersion() { - return new GroupVersion("console.api.my-plugin.halo.run", "v1alpha1"); - } -} -``` - -CustomEndpoint 接口包含以下两个方法: - -- `endpoint()`:定义自定义 API 的路由。 -- `groupVersion()`:定义自定义 API 的 group 和 version。 - -实现了 `CustomEndpoint` 接口的类需要添加 `@Component` 注解,Halo 会自动扫描并注册这些自定义 APIs。 -注册时会根据 `groupVersion()` 方法返回的 group 和 version 作为 `endpoint()` 中定义路由的前缀以简化路径写法。 - -本章节相关技术栈参考文档: - -- [Reactor 3 Reference Guide](https://projectreactor.io/docs/core/release/reference/) -- [Webflux](https://docs.spring.io/spring-framework/reference/web/webflux.html)。 - -### 带注解的 MVC 控制器写法 - -如果开发者习惯使用 Spring MVC 的注解风格,也可以使用 `@Controller`、`@RequestMapping` 等注解来定义自定义 APIs: - -唯一的区别是是需要在 MVC 控制器添加 `@ApiVersion` 注解,**没有此注解的 MVC 控制器类无法被注册路由**。 - -示例: - -```java -@ApiVersion("my-plugin.halo.run/v1alpha1") -@RequestMapping("/persons") -@RestController -@RequiredArgsConstructor -public class PersonController { - private final PersonService personService; - - @GetMapping("/{name}") - public Mono getPerson(@PathVariable("name") String name) { - return personService.getPerson(name); - } -} -``` - -这个写法定义的路由与 `CustomEndpoint` 接口的写法是等价的,`@ApiVersion` 等价于 `CustomEndpoint` 接口的 `groupVersion()` 方法。 - -参考 [Spring Framework Web](https://docs.spring.io/spring-framework/reference/web/webflux/new-framework.html) - -### 自定义 API 查询参数定义 - -以 Person 自定义模型为例,列表查询 API 的查询参数可能包括关键词、排序、分页等,可以通过定义一个 DTO 类来封装查询参数: - -```java -@Data -public class PersonQuery { - private String keyword; - private Integer page; - private Integer size; - private String sort; -} -``` - -但排序、分页、标签查询和字段查询等参数通常是通用的,因此 Halo 提供了 `run.halo.app.extension.router.SortableRequest` 类来封装这些参数,开发者可以直接继承该类来定义额外查询参数: - -```java -public class PersonQuery extends SortableRequest { - - public PersonQuery(ServerWebExchange exchange) { - super(exchange); - } - - @Override - public ListOptions toListOptions() { - return super.toListOptions(); - } - - @Override - public PageRequest toPageRequest() { - return super.toPageRequest(); - } -} -``` - -- toListOptions():将查询参数转换为 `ReactiveExtensionClient` 的 list 查询所需参数。 -- toPageRequest():将查询参数转换为 `ReactiveExtensionClient` 的 list 查询所需 page 参数,此方法通常不需要覆盖。 - -当需要添加额外的查询参数时,只需在 `PersonQuery` 类中添加对应的字段即可。 - -```java -public class PersonQuery extends SortableRequest { - - public PersonQuery(ServerWebExchange exchange) { - super(exchange); - } - - public String getKeyword() { - return queryParams.getFirst("keyword"); - } - - @Override - public ListOptions toListOptions() { - return ListOptions.builder(super.toListOptions()) - .fieldQuery(QueryFactory.or( - QueryFactory.equal("metadata.name", getKeyword()), - QueryFactory.contains("spec.name", getKeyword()) - )) - .build(); - } -} -``` - -然后使用它: - -```java -final ReactiveExtensionClient client; - -public Mono> list(ServerRequest request) { - var query = new PersonQuery(request.exchange()); - return client.listBy(Person.class, query.toListOptions(), query.toPageRequest()); -} -``` - -参考 [ReactiveExtensionClient](./extension-client.md#query)。 - -### 使用 Java Bean Validation {#using-java-bean-validation} - -对于自定义 API 的请求体,开发者可以使用 [Java Bean Validation](https://beanvalidation.org/) 来校验请求体参数,可以减少手动校验的代码量。 - -Bean Validation 为应用程序提供了一种通过约束声明和元数据的通用验证方式。 -要使用它,你可以在域模型属性上使用声明性验证约束进行注解,然后在运行时强制执行这些约束。它包含内置的约束,你还可以定义自己的自定义约束。 - -以下示例,展示了一个包含两个属性的简单 PersonParam 模型: - -```java -public class PersonParam { - private String name; - private int age; -} -``` - -Bean Validation 允许您像以下示例所示那样声明约束: - -```java -public class PersonParam { - - @NotNull - @Size(max=64) - private String name; - - @Min(0) - private int age; -} -``` - -要启用 Bean Validation,需要在插件项目中添加一个配置类,如下所示: - -```java -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; - -@Configuration -public class PluginConfig { - - @Bean - public LocalValidatorFactoryBean validator() { - return new LocalValidatorFactoryBean(); - } -} -``` - -然后注入 `Validator` 实例: - -```java -@Component -@RequiredArgsConstructor -public class PersonEndpoint implements CustomEndpoint { - // step 1: 注入 Validator 实例 - private final Validator validator; - - // 省略其他代码 - - private Mono updatePerson(ServerRequest request) { - return request.bodyToMono(PersonParam.class) - // step 3: 调用 validate 方法 - .doOnNext(person -> validate(person, request.exchange())) - .flatMap(person -> ServerResponse.ok().bodyValue(person)); - } - - // step 2: 校验请求体参数 - private void validate(PersonParam person, ServerWebExchange exchange) { - var bindResult = validate(person, "person", validator, exchange); - if (bindResult.hasErrors()) { - throw new RequestBodyValidationException(bindResult); - } - } -} - -// 将此工具方法添加到你的插件中 -public static BindingResult validate(Object target, String objectName, - Validator validator, ServerWebExchange exchange) { - BindingResult bindingResult = new BeanPropertyBindingResult(target, objectName); - try { - // 由于 Halo 可以在登录时设置用户语言环境 - // 设置当前请求的 Locale 使得校验错误信息的语言可以根据请求的 Locale 返回 - LocaleContextHolder.setLocaleContext(exchange.getLocaleContext()); - validator.validate(target, bindingResult); - return bindingResult; - } finally { - LocaleContextHolder.resetLocaleContext(); - } -} -``` - -参考文档: - -- [RequestBodyValidationException](https://github.com/halo-dev/halo/blob/25086ee3e63f0c8b6ed380140a068c44404ef2b2/application/src/main/java/run/halo/app/infra/exception/RequestBodyValidationException.java) -- [Bean Validation](https://beanvalidation.org/) -- [Spring Validation](https://docs.spring.io/spring-framework/reference/core/validation/beanvalidation.html) - -## API 文档 - -Halo 会自动生成 OpenAPI 文档,包括自动生成的 `CRUD` APIs 和自定义 APIs。 - -API 文档可以通过访问 `/swagger-ui.html` 查看,例如:`http://localhost:8090/swagger-ui.html`。 - -API 文档会根据 [自定义 API 的 Group 规则](#custom-api-group-spec)被划分到不同的分组,方便开发者和生成 API Client: - -- `Aggregated API V1alpha1`:所有 APIs 都会被聚合到这个分组中。 -- `Extension API V1alpha1`:自动生成的所有 `CRUD` API。 -- `Console API V1alpha1`:Console 端使用的自定义 API。 -- `User-center API V1alpha1`:个人中心使用的自定义 API。 -- `Public API V1alpha1`:提供给主题端使用的自定义 API。 - -参考 [Swagger Config](http://localhost:8090/v3/api-docs/swagger-config) - -为了能让自定义 API 能够被 Swagger 文档展示,开发者定义 Functional Endpoints 时需要 SpringDoc 包装过的 `SpringdocRouteBuilder` 来代替 `RouterFunctions`。 - -```java -public class PersonEndpoint implements CustomEndpoint { - - @Override - public RouterFunction endpoint() { - final var tag = "PersonV1alpha1Console"; - return SpringdocRouteBuilder.route() - .GET("/persons", this::getPersons, - builder -> builder.operationId("ListPersons") - .description("List all persons") - .tag(tag) - .response(responseBuilder() - .implementation(ListResult.generateGenericClass(Person.class)) - ) - ) - .build(); - } -} -``` - -其中 builder 用于设置 API 文档的元数据: - -- operationId:操作 ID,建议首字母大写,驼峰命名,生成 API Client 时将以此为方法名的一部分。 -- tag:标签,用于分组 API,建议使用 `{自定义模型Kind}{自定义模型Version}{作用域}` 的格式,例如 `PersonV1alpha1Console`,Console 表示其在 Console 端使用。 - -关于生成 API Client 文档参考 [Devtools 生成 API Client](../../basics/devtools.md#how-to-generate-api-client) - -由于 SpringDoc 缺少对 `SpringdocRouteBuilder` 的文档介绍,开发者可参考示例来使用。 - -- [PostEndpoint](https://github.com/halo-dev/halo/blob/25086ee3e63f0c8b6ed380140a068c44404ef2b2/application/src/main/java/run/halo/app/core/endpoint/console/PostEndpoint.java) -- [AttachmentEndpoint](https://github.com/halo-dev/halo/blob/25086ee3e63f0c8b6ed380140a068c44404ef2b2/application/src/main/java/run/halo/app/core/attachment/endpoint/AttachmentEndpoint.java#L48) -- [UserConnectionEndpoint](https://github.com/halo-dev/halo/blob/25086ee3e63f0c8b6ed380140a068c44404ef2b2/application/src/main/java/run/halo/app/core/endpoint/uc/UserConnectionEndpoint.java#L55) -- [构建查询参数](https://github.com/halo-dev/halo/blob/25086ee3e63f0c8b6ed380140a068c44404ef2b2/application/src/main/java/run/halo/app/content/PostQuery.java#L97) diff --git a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/finder-for-theme.md b/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/finder-for-theme.md deleted file mode 100644 index 60fb81f4..00000000 --- a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/finder-for-theme.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: 为主题提供数据 -description: 了解如何为主题提供更多获取和使用数据的方法。 ---- - -当你在插件中创建了自己的自定义模型时,你可能需要在主题模板中使用这些数据。或者,你提供一些额外的数据,以便主题可以使用它们,你可以通过创建一个自定义的 `finder` 来实现这一点。 - -## 创建一个 Finder - -首先,你需要创建一个 `interface`,并在其中定义你需要提供给主题获取数据的方法,方法的返回值可以是 `Mono` 或 `Flux` 类型,例如: - -```java -public interface LinkFinder { - Mono get(String linkName); - - Flux listAll(); -} -``` - -然后写一个实现类,实现这个 `interface`,并在类上添加 `@Finder` 注解,例如: - -```java -import run.halo.app.theme.finders.Finder; - -@Finder("myPluginLinkFinder") -public class LinkFinderImpl implements LinkFinder { - @Override - public Mono get(String linkName) { - // ... - } - - @Override - public Flux listAll() { - // ... - } -} -``` - -`@Finder` 注解的值是你在主题中使用的名称,例如,你可以在主题中使用 `myPluginLinkFinder.get('a-link-name')` 来获取数据,`myPluginLinkFinder` 就是你在 `@Finder` 注解中定义的名称。 - -## Finder 命名 - -为了避免与其他插件的 `finder` 名称冲突,建议在 `@Finder` 注解中添加一个你插件名称的前缀作为 `finder` 名称且名称需要是驼峰式的,不能包含除了 `_` 之外的其他特殊字符。 - -例如,你的插件名称是 `my_plugin`,你需要为主题提供一个获取链接的 `finder`,那么你可以这样定义 `@Finder` 注解: - -```java -@Finder("myPluginLinkFinder") -``` - -## 使用 Finder - -在主题中,你可以通过 `finder` 名称和方法名及对应的参数来获取数据,例如: - -```html -
-
-``` - -模板语法参考:[Thymeleaf](https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#standard-expression-syntax)。 diff --git a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/login-handler-enhancer.md b/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/login-handler-enhancer.md deleted file mode 100644 index c06fe5e8..00000000 --- a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/login-handler-enhancer.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: 登录增强 -description: 了解如何在登录时如何允许 Halo 做登录逻辑的增强切入。 ---- - -## 背景 - -在 Halo 中,插件可以实现多种登录方式,例如 LDAP、第三方登录等。然而,灵活的登录方式也带来了以下问题: - -1. 登录逻辑难以统一:例如登录成功后需要进行额外处理,这需要插件自行实现。 -2. Halo 或其他插件无法知晓登录状态:无法记录登录日志等额外处理。 -3. 新增安全特性适配:Halo 增加了新安全特性,插件需要适配才能使用,如在记住我机制中需要在登录成功后设置 remember-me cookie。 - -为了解决这些问题,Halo 提供了登录增强机制,插件可以在登录成功或失败时调用登录增强器,使 Halo 可以执行额外的处理逻辑。随着 Halo 的版本更新,这些逻辑也会更新,而插件无需做任何修改。 - -### 登录增强器 - -Halo 提供了一个 LoginHandlerEnhancer 的 Bean,插件可以通过依赖注入的方式在合适的位置调用该 Bean 的方法,以便 Halo 可以在登录成功或失败后执行逻辑切入。 - -```java -public interface LoginHandlerEnhancer { - - /** - * Invoked when login success. - * - * @param exchange The exchange. - * @param successfulAuthentication The successful authentication. - */ - Mono onLoginSuccess(ServerWebExchange exchange, Authentication successfulAuthentication); - - /** - * Invoked when login fails. - * - * @param exchange The exchange. - * @param exception the reason authentication failed - */ - Mono onLoginFailure(ServerWebExchange exchange, AuthenticationException exception); -} -``` - -例如在用户密码登录的处理器中,可以这样调用登录增强器: - -```java -public class UsernamePasswordHandler implements ServerAuthenticationSuccessHandler, - ServerAuthenticationFailureHandler { - @Override - public Mono onAuthenticationFailure(WebFilterExchange webFilterExchange, - AuthenticationException exception) { - var exchange = webFilterExchange.getExchange(); - return loginHandlerEnhancer.onLoginFailure(exchange, exception) - .then(handleFailure(exchange, exception)); - } - - @Override - public Mono onAuthenticationSuccess(WebFilterExchange webFilterExchange, - Authentication authentication) { - return loginHandlerEnhancer.onLoginSuccess(webFilterExchange.getExchange(), authentication) - .then(handleSuccess(webFilterExchange.getExchange(), authentication); - } -} -``` - -设备管理、记住我等机制都依赖于登录增强器。插件开发者可以通过在合适的时机调用登录增强器来实现这些功能,确保插件与 Halo 的安全特性无缝集成。 diff --git a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/notification.md b/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/notification.md deleted file mode 100644 index a401de08..00000000 --- a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/notification.md +++ /dev/null @@ -1,475 +0,0 @@ ---- -title: 发送和订阅通知 -description: 了解如何在插件中发送和订阅通知。 ---- - -Halo 的通知功能提供了事件驱动的消息提醒机制,让用户能够及时获取系统内的关键事件。 -开发者可以根据需求定义事件类型和通知方式(如站内消息、邮件等),并支持个性化的推送策略,提升用户体验和系统可扩展性。 - -通知系统通过事件机制将关键消息推送给用户。开发者可以自定义通知类型、消息格式和推送方式,主要应用于以下场景: - -- 用户互动:如文章评论、点赞等; -- 订单和流程提醒:如订单创建、处理完成等; -- 内容更新:如文章发布、系统公告等。 - -## 通知系统工作流程 - -下图展示了 Halo 通知功能的工作流程,包括事件声明、订阅查找、通知发送等关键步骤。 - -```mermaid - graph TD - A[开发者声明 ReasonType] --> B[业务代码创建 Reason] - B --> C[查找匹配的 Subscription] - C --> D{用户订阅了事件吗?} - D -- 是 --> E[获取用户通知偏好] - E --> F[查找通知模板] - F --> G[查找对应的 Notifier] - G --> H[发送通知] - D -- 否 --> I[结束,无需通知] - H --> J[用户接收通知] - J --> K[用户查看/处理通知] - Z[用户订阅事件] --> D -``` - -1. 声明事件类型:开发者首先需要声明通知事件的类型 (ReasonType),指定事件的名称、描述和事件所需的属性。 -2. 插件触发事件:当插件中的某个业务操作发生时,插件需要触发相应的事件。 -3. 创建 Reason 实例:Halo 根据发送的事件创建一个 Reason 实例,表示具体的事件信息,步骤 2 和 3 可以合并为`业务代码创建 Reason`。 -4. 订阅查找:Halo 通知中心会根据事件的类型和属性查找匹配的订阅。 -5. 通知发送:如果找到匹配的订阅,系统根据用户的偏好设置通过不同的通知器(如站内消息、邮件等)发送通知。 -6. 用户接收和处理通知:用户通过预设的通知渠道接收事件通知,并可以在系统中查看或处理这些通知。 - -## 通知事件 - -在 Halo 通知系统中,`ReasonType` 和 `Reason` 是两个核心自定义模型,用于定义通知事件的类别和具体的事件实例。 -理解它们的字段对于开发者扩展通知功能至关重要。 - -下面将详细说明这两个模型的字段及其作用。 - -### ReasonType 模型 - -`ReasonType` 是用于定义通知事件类别的模型。每个 `ReasonType` 都代表一类特定的事件,例如文章评论、新文章发布等。通过 `ReasonType`,系统可以了解该事件的特定属性和数据结构。 -用户个人中心的通知设置页面会根据 `ReasonType` 的名称和描述展示事件类型和通知方式。 - -#### ReasonType 字段说明 - -| 字段名 | 类型 | 是否必填 | 说明 | -| ------------------ | -------- | -------- | --------------------------------------------------- | -| `apiVersion` | `string` | 是 | API 版本号,定义为 `notification.halo.run/v1alpha1` | -| `kind` | `string` | 是 | 自定义资源类型,必须为 `ReasonType` | -| `metadata.name` | `string` | 是 | 事件类别的唯一标识名称,例如 `comment` | -| `spec.displayName` | `string` | 是 | 事件类别的展示名称,用户界面中显示的事件名称 | -| `spec.description` | `string` | 否 | 事件类别的描述,说明此事件的用途和含义 | -| `spec.properties` | `array` | 是 | 此事件包含的属性字段,用于定义该类事件应携带的数据 | - -#### spec.properties 字段 - -`properties` 字段用于定义该通知事件需要传递的参数或属性。每个属性都是一个对象,通常包含以下字段: - -| 字段名 | 类型 | 是否必填 | 说明 | -| ------------- | --------- | -------- | -------------------------------------------------------- | -| `name` | `string` | 是 | 属性的名称,表示事件数据中的某个字段 | -| `type` | `string` | 是 | 属性的数据类型,例如 `string`、`boolean` 等,仅用于描述 | -| `description` | `string` | 否 | 对该属性的描述,说明其在事件中的作用 | -| `optional` | `boolean` | 否 | 该属性是否为可选字段,默认值为 `false`,`false` 表示必填 | - -`properties.type` 字段仅用于文档性目的,不会在运行时进行数据类型检查。有了此描述,便于编写通知模板时使用正确的数据类型。 - -**示例:** 声明评论事件的 ReasonType - -```yaml -apiVersion: notification.halo.run/v1alpha1 -kind: ReasonType -metadata: - name: comment -spec: - displayName: "评论事件" - description: "用户在文章上收到评论时触发。" - properties: - - name: postName - type: string - description: "文章的名称。" - - name: commenter - type: string - description: "评论者用户名。" - - name: content - type: string - description: "评论内容。" -``` - -在这个示例中,`ReasonType` 定义了一个评论事件,该事件包括三种属性:`postName`(文章名)、`commenter`(评论者)和 `content`(评论内容),这些属性在触发事件时将传递给通知系统。 - -这一类型的资源声明非常适合放在插件的 `resources` 目录下,以便插件安装时自动创建。参考 [声明自定义模型对象](./extension.md#declare-extension-object) - -### Reason 模型 - -`Reason` 模型用于描述具体的事件实例,它是 `ReasonType` 的一个实例化,包含触发该事件时的具体数据。 -`Reason` 通常在某个事件发生时创建,例如某篇文章收到评论时生成一个 `Reason`,记录具体的评论信息。 - -#### Reason 字段说明 - -| 字段名 | 类型 | 是否必填 | 说明 | -| ----------------- | -------- | -------- | -------------------------------------------------------------- | -| `apiVersion` | `string` | 是 | API 版本号,定义为 `notification.halo.run/v1alpha1` | -| `kind` | `string` | 是 | 自定义资源类型,必须为 `Reason` | -| `metadata.name` | `string` | 是 | 该事件实例的唯一标识名称,通常自动生成 | -| `spec.reasonType` | `string` | 是 | 引用的 `ReasonType` 名称,表示该事件实例属于哪个事件类型 | -| `spec.author` | `string` | 是 | 事件的触发者或创建者,通常为用户或系统的标识符 | -| `spec.subject` | `object` | 是 | 事件的主题,指向该事件所涉及的具体对象(如文章、评论等) | -| `spec.attributes` | `object` | 是 | 包含事件具体数据的键值对,内容与 `ReasonType` 中定义的属性一致 | - -#### spec.subject 字段 - -`subject` 字段描述了与该事件相关的主体对象,例如,评论事件中的文章对象。`subject` 通常包含以下字段: - -| 字段名 | 类型 | 是否必填 | 说明 | -| ------------ | -------- | -------- | ------------------------------------------------------- | -| `apiVersion` | `string` | 是 | 主题对象的 API 版本号,例如 `content.halo.run/v1alpha1` | -| `kind` | `string` | 是 | 主题对象的类型,例如 `Post` 表示文章 | -| `name` | `string` | 是 | 主题对象的唯一标识,通常是对象的名称或 ID | -| `title` | `string` | 是 | 主题对象的标题,通常是人类可读的名称 | -| `url` | `string` | 否 | 主题对象的访问链接或详情页面的 URL | - -参考 [Reason 自定义模型](https://github.com/halo-dev/halo/blob/0d1a0992231fd5e66a65b4e9d426d3f373b1903f/api/src/main/java/run/halo/app/core/extension/notification/Reason.java) - -#### spec.attributes 字段 - -`attributes` 字段用于存储该事件实例的具体数据。每个键值对表示一个 `ReasonType` 中定义的属性和其对应的值。 - -**示例:** 创建评论事件的 Reason - -```yaml -apiVersion: notification.halo.run/v1alpha1 -kind: Reason -metadata: - name: comment-123 -spec: - reasonType: comment - author: "访客" - subject: - apiVersion: 'content.halo.run/v1alpha1' - kind: Post - name: 'post-456' - title: 'Halo 系统介绍' - url: 'https://example.com/archives/456' - attributes: - postName: "Halo 系统介绍" - commenter: "访客" - content: "这是一篇非常有帮助的文章!" -``` - -在这个示例中,`Reason` 表示具体的评论事件。 -它关联了 `comment` 这一 `ReasonType`,并提供了详细的事件信息,包括文章(subject)的标识信息以及评论的内容(attributes)。 - -通过 ReasonType 和 Reason,开发者可以定义和管理各种事件,并在事件发生时触发相应的通知逻辑。 - -## 通知模板 - -在 Halo 系统中,通知模板用于定义每种通知类型的展示格式和内容结构。每当触发某个通知事件时,系统会根据事件的类型选择相应的通知模板,并将事件的属性嵌入到模板中生成最终的通知内容。如果未定义通知模板,则系统无法确定通知的具体格式和内容,这可能导致通知发送失败。因此,定义通知模板是实现通知功能的关键步骤。 - -### 通知模板的基本结构 - -通知模板是通过 `NotificationTemplate` 自定义模型定义的。 -每个模板指定了与某个事件类型 (ReasonType) 关联的内容格式,包括通知的标题、正文内容等。 -模板内容可以使用属性占位符,以便在通知生成时自动填充事件属性。Halo 支持使用 [Thymeleaf](https://www.thymeleaf.org/) 模板引擎进行内容渲染。 - -#### 通知模板字段说明 - -| 字段名 | 类型 | 是否必填 | 说明 | -| -------------------------------- | -------- | -------- | --------------------------------------------------- | -| `apiVersion` | `string` | 是 | API 版本号,定义为 `notification.halo.run/v1alpha1` | -| `kind` | `string` | 是 | 自定义资源类型,必须为 `NotificationTemplate` | -| `metadata.name` | `string` | 是 | 模板的唯一标识名称 | -| `spec.reasonSelector.reasonType` | `string` | 是 | 关联的事件类型 (`ReasonType`) 名称 | -| `spec.reasonSelector.language` | `string` | 是 | 模板语言,固定写为 `default` | -| `spec.template.title` | `string` | 是 | 通知的标题模板,支持占位符 | -| `spec.template.rawBody` | `string` | 是 | 通知的正文模板,应当是纯文本,支持占位符 | -| `spec.template.htmlBody` | `string` | 是 | 通知的正文模板,格式为 HTML 的模板,支持占位符 | - -#### 定义通知模板 - -定义通知模板时,开发者需要指定模板的 `reasonSelector`,用于与事件类型关联,并在 `template` 中定义通知标题和内容的格式。 - -`spec.reasonSelector.language` 字段用于指定模板的语言,设计支持多语言,但目前 Halo 没有提供保存用户语言偏好的入口,因此只能使用 `default`。 - -模板内容支持纯文本和 HTML 格式,建议开发者两种内容都提供,以适应不同的通知渠道。比如邮件通知需要 HTML 格式,而短信通知则仅支持纯文本。 - -**示例:** 定义评论事件的通知模板 - -假设我们需要为“新评论”事件定义一个通知模板,该模板包括事件的标题和正文内容: - -```yaml -apiVersion: notification.halo.run/v1alpha1 -kind: NotificationTemplate -metadata: - name: template-new-comment-on-post -spec: - reasonSelector: - reasonType: new-comment-on-post - language: default - template: - title: "你的文章 [(${subject.title})] 收到了一条新评论" - rawBody: | - 评论者 [(${author.name})] 评论了您的文章 [(${subject.title})],内容如下: - [(${props.comment})] - htmlBody: | -

评论者 [(${author.name})] 评论了您的文章 [(${subject.title})],内容如下:

-

[(${props.comment})]

-``` - -参考 [Halo 默认通知模板 YAML](https://github.com/halo-dev/halo/blob/0d1a0992231fd5e66a65b4e9d426d3f373b1903f/application/src/main/resources/extensions/notification-templates.yaml) - -#### 通知模板设计小技巧与最佳实践 - -##### 合理使用占位符 - -在通知模板中,使用占位符来动态插入事件数据是提高模板灵活性的关键。占位符的设计应遵循以下规则: - -- 使用清晰的命名:占位符应具有明确的含义,例如 `[(${quoteReplyName})]` 表示引用回复的名称,让代码更具可读性,建议使用 `CamelCase` 命名规范,不要使用嵌套对象作为变量如 `subject.title`。 -- 避免嵌套复杂表达式:为了避免通知生成的内容过于复杂,建议在模板中尽量使用简单的占位符。复杂逻辑应在事件触发时处理,尽量保持模板的简洁。 -- 确保属性完整性:占位符应与 ReasonType 中定义的属性一致,避免由于属性缺失导致的通知发送错误。 - -##### 提供多种格式的内容 - -在 Halo 中,不同的通知渠道可能支持不同格式的内容,建议模板中同时提供纯文本和 HTML 格式,以便适应各类通知渠道需求。 - -- 纯文本格式:适合即时通讯类应用消息和短信通知,尽量简洁明了,关注重点信息。 -- HTML 格式:适合富文本展示渠道,如邮件通知。HTML 模板可使用简单的样式和链接,帮助用户更好地理解通知内容。 -- 适配不同渠道的内容:对于可能发送到多渠道的通知,可以在不同格式中包含适合该渠道的具体内容,如 HTML 中使用 `` 标签提供链接,而纯文本只展示简洁的链接地址。 -- 如果在模板中需要用到日期,建议提前将其格式化为带时区的日期字符串,避免在模板中使用复杂的日期格式化。 - -##### 模板语法 - -Halo 使用 Thymeleaf 模板引擎来渲染通知模板,开发者可以在模板中使用 Thymeleaf 的语法来处理模板中的逻辑和数据。 - -- 对于纯文本如标题和 `rawBody`,使用 Thymeleaf 的 [Textual syntax](https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#textual-syntax) 语法来引用变量和表达式。 -其取值格式为:`[(${expression})]`,例如 `[(${title})]`。 -- 对于 HTML 内容如 `htmlBody`,使用 Thymeleaf 的 [Standard syntax](https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#standard-syntax) 语法。 -其取值格式为:`${expression}`,例如 `${title}`。 - -#### 模板渲染与发送 - -在通知事件触发时,Halo 通知中心会查找与该事件 `ReasonType` 匹配的 `NotificationTemplate`,并将事件数据填充到模板中生成最终通知内容。 -没有定义模板的事件将无法发送通知,因此为每个 `ReasonType` 定义模板是保证通知发送成功的前提。 - -可以有多个相同的 `reasonSelector` 绑定到同一个事件类型,比如 `reasonType=new-comment-on-post`且 `language=default` 的模板存在多个,Halo 会**选择最近创建的模板**。 -根据这个特点,**插件或主题开发者可以提供自己的通知模板以覆盖默认的通知模板**。 - -在生成通知内容时,系统还会提供一些额外的全局属性(如 `site.title` 等),这些属性可以在模板中直接使用: - -| 字段名 | 类型 | 说明 | -| ------------------------ | -------- | ---------------------------- | -| `site.title` | `string` | 站点标题 | -| `site.subtitle` | `string` | 站点副标题 | -| `site.logo` | `string` | 站点 Logo URL | -| `site.url` | `string` | 站点外部访问地址 | -| `subscriber.displayName` | `string` | 订阅者显示名称 | -| `subscriber.id` | `string` | 订阅者唯一标识符 | -| `unsubscribeUrl` | `string` | 退订地址,用于取消订阅的链接 | - -## 触发通知事件 - -在 Halo 系统中,通知功能的核心是通过触发特定的事件,生成相应的 Reason 实例,然后通过系统的通知机制将该事件通知给订阅者。 -Halo 提供了 `NotificationReasonEmitter` 接口,开发者可以通过它轻松触发通知事件,将业务逻辑与通知机制结合起来。 - -### 工作机制 - -`NotificationReasonEmitter` 的作用是简化事件触发和通知的处理流程,它的主要职责是: - -- 接收业务事件的参数,生成 `Reason` 实例。 -- 将 `Reason` 实例与 `ReasonType` 进行匹配,触发事件。 -- 通过 Halo 的通知系统,将事件推送给订阅了该事件的用户。 - -定义参考 [NotificationReasonEmitter](../../basics/server/object-management.md#notificationreasonemitter) - -- `reasonType`:事件类型的名称,对应于 `ReasonType` 的 `metadata.name` 字段。 -- `reasonData`:事件数据的构建器,用于构建 `Reason` 实例的属性。 - -Reason 数据的构建器有以下属性: - -```java -public class ReasonPayloadBuilder { - private Reason.Subject subject; - private UserIdentity author; - private Map attributes; -} -``` - -- `subject`:事件的主体对象,参考 `Reason` 中的 `spec.subject` 字段。 -- `author`:事件的触发者,通常是用户或系统的标识符,这是一个 `UserIdentity` 对象,如果作者是匿名的则需要传递邮箱地址来构造,比如评论者可能没有关联具体用户而仅仅是邮箱地址。 -- `attributes`:事件的具体数据,包含了 `ReasonType` 中定义的属性和对应的值,如果必填字段没有传递则会抛出异常。 - -使用场景举例: - -- 当用户在博客上发表文章时,触发“新文章发布”事件,通知订阅了该事件的用户。 -- 当用户在文章上发表评论时,触发“评论回复”事件,通知文章作者或订阅了评论的用户。 -- 当管理员处理插件订单时,触发“订单创建”事件,通知管理员新订单的详情。 - -### 示例 {#reason-emitter-example} - -假设我们需要在用户发布的文章时有新评论时,向订阅了“文章有新评论”事件的用户发送通知。可以通过 `NotificationReasonEmitter` 实现如下逻辑。 - -第一步:在你的插件代码中注入 `NotificationReasonEmitter`。 - -```java -import run.halo.app.notification.NotificationReasonEmitter; - -@Service -@RequiredArgsConstructor -public class CommentEventListener { - - private final NotificationReasonEmitter notificationReasonEmitter; -} -``` - -第二步:有新评论被创建时触发 - -```java -import run.halo.app.core.extension.content.Comment; -import run.halo.app.core.extension.content.Post; - -@Async -@EventListener(CommentCreatedEvent.class) -public void onNewComment(CommentCreatedEvent event) { - Comment comment = event.getComment(); - Ref subjectRef = comment.getSpec().getSubjectRef(); - Post post = client.fetch(Post.class, subjectRef.getName()).orElseThrow(); - // 评论的主体是文章,因此构建文章的主体对象 - var reasonSubject = Reason.Subject.builder() - .apiVersion(post.getApiVersion()) - .kind(post.getKind()) - .name(subjectRef.getName()) - .title(post.getSpec().getTitle()) - .url(postUrl) - .build(); - // new-comment-on-post 用于表示 ReasonType 的 metadata.name - notificationReasonEmitter.emit("new-comment-on-post", - builder -> { - // 定义了一个类型用于保存评论事件的数据,避免 map 的 key 写错 - var attributes = CommentOnPostReasonData.builder() - .postName(subjectRef.getName()) - .postOwner(post.getSpec().getOwner()) - .postTitle(post.getSpec().getTitle()) - .postUrl(postUrl) - .commenter(owner.getDisplayName()) - .content(comment.getSpec().getContent()) - .commentName(comment.getMetadata().getName()) - .build(); - // 将 CommentOnPostReasonData 转换为事件所需的 Map 类型数据 - builder.attributes(toAttributeMap(attributes)) - .author(identityFrom(owner)) - .subject(reasonSubject); - }).block(); -} - -public static Map toAttributeMap(T data) { - Assert.notNull(data, "Reason attributes must not be null"); - return JsonUtils.mapper().convertValue(data, new TypeReference<>() { - }); -} -``` - -参考 [Halo 评论事件触发](https://github.com/halo-dev/halo/blob/0d1a0992231fd5e66a65b4e9d426d3f373b1903f/application/src/main/java/run/halo/app/content/comment/CommentNotificationReasonPublisher.java#L89) - -## 订阅通知 - -在 Halo 系统中,用户可以通过订阅通知来接收感兴趣的事件,并且可以使用表达式对事件进行过滤,确保只接收到符合条件的通知。 -通过这种方式,用户可以精确订阅某些事件,并避免被无关的事件打扰。 -订阅机制支持根据事件属性、主体对象和事件发起人来过滤事件,并使用 SpEL(Spring Expression Language)编写过滤表达式。 - -### 订阅通知的工作机制 - -通知订阅机制允许用户订阅各种类型的事件(如文章发布、评论回复等),并且支持通过表达式过滤事件。 - -订阅系统可以通过以下三种根对象来筛选感兴趣的事件: - -- props:事件属性的根对象,表示事件携带的具体数据。例如,在评论回复事件中,属性可以是 `{repliedOwner: 'guqing'}`,可以通过 `props.repliedOwner` 来访问 `repliedOwner` 属性。 -- subject:事件的主体对象,表示事件所关联的核心对象,字段同 `Reason` 中的 `spec.subject` 字段。例如,在“新评论”事件中,subject 是评论所属的文章。 -- author:事件的发起人,表示触发事件的用户标识符,字符串类型。例如,在评论事件中,发起人是评论者。 - -通过这些对象,订阅者可以编写表达式来对事件进行过滤。 - -表达式的结果必须为布尔值,用于判断当前事件是否符合订阅者的条件。 - -### 表达式过滤规则 - -使用 [SpEL](https://docs.spring.io/spring-framework/reference/core/expressions.html) 规范,开发者可以通过以下方式过滤事件: - -- props:访问事件的属性。例如,`props.repliedOwner == 'guqing'` 用于筛选事件属性中 repliedOwner 为 guqing 的事件。 -- subject:访问事件主体的属性。例如,`subject.kind == 'Post'` 用于筛选主体为 Post 的事件。 -- author:访问事件的发起人。例如,`author == 'guqing'` 用于筛选 guqing 发起的评事件。 - -### 订阅方式 - -目前 Halo 没有提供用户界面来订阅通知,订阅均是通过事件发起前由开发者为用户创建的订阅对象来实现的。 -因此,开发者需要在插件中实现订阅逻辑,为用户创建订阅对象,需要注意以下几点: - -1. 订阅对象的创建需要在用户订阅事件前完成,否则用户无法接收到通知。 -2. 哪些用户能够接收到通知需要开发者谨慎考虑,确保用户订阅的事件是符合其需求的,避免将无关的事件推送给用户。 -3. 在创建订阅时,同样的逻辑反复执行不会重复创建订阅,因此可以在每次事件触发前订阅。 - -Halo 提供 `NotificationCenter` Bean 来帮助开发者创建订阅对象和取消订阅对象。 - -参考 [NotificationCenter Bean](../../basics/server/object-management.md#notificationcenter) - -`subscribe` 方法用于订阅事件,`unsubscribe` 方法用于取消订阅事件,第一个参数是订阅者,第二个参数是感兴趣的事件。 - -#### Subscription.Subscriber - -`Subscription.Subscriber` 具有以下属性: - -| 字段名 | 类型 | 是否必填 | 说明 | -| ------ | -------- | -------- | -------------------------------------------------------- | -| `name` | `string` | 是 | 订阅者的用户名,或通过 `UserIdentity` 构建的匿名订阅标识 | - -#### Subscription.InterestReason - -`Subscription.InterestReason` 具有以下属性: - -| 字段名 | 类型 | 是否必填 | 说明 | -| ------------ | -------- | -------- | -------------------------------------------------------------------- | -| `reasonType` | `string` | 是 | 感兴趣的事件类型的名称,对应于 `ReasonType` 的 `metadata.name` 字段 | -| `expression` | `string` | 是 | 订阅事件的过滤表达式,用于过滤感兴趣的事件避免用户接收到不相关的通知 | - -参考 [Subscription 自定义模型](https://github.com/halo-dev/halo/blob/0d1a0992231fd5e66a65b4e9d426d3f373b1903f/api/src/main/java/run/halo/app/core/extension/notification/Subscription.java) - -### 示例 {#subscribe-example} - -以下是一个通过 Java 代码实现订阅文章“新评论”通知的示例。该示例展示了如何使用表达式来筛选事件,只接收特定文章下的评论通知。 - -```java -// step 0: 依赖注入 NotificationCenter -private final NotificationCenter notificationCenter; - -Mono subscribeNewCommentOnPostNotification(String username) { - // step1: 创建订阅者对象 - var subscriber = new Subscription.Subscriber(); - subscriber.setName(username); // 设置订阅者的用户名 - - // step2: 创建感兴趣的事件类型对象 - var interestReason = new Subscription.InterestReason(); - // 设置订阅的事件类型为“新评论” - interestReason.setReasonType(NEW_COMMENT_ON_POST); - - // step3: 使用表达式过滤事件,props 是事件属性的根对象 - // 只接收由指定用户创建的文章下的评论事件 - interestReason.setExpression("props.repliedOwner == '%s'".formatted(username)); - - // step4: 订阅事件 - return notificationCenter.subscribe(subscriber, interestReason); -} -``` - -在这个示例中,我们通过以下步骤实现了订阅: - -1. 创建订阅者对象:通过 `Subscription.Subscriber()` 实例化一个订阅者对象,并为其设置订阅者的用户名。 -2. 定义感兴趣的事件类型:使用 `Subscription.InterestReason()` 定义感兴趣的事件类型,这里使用 `NEW_COMMENT_ON_POST` 表示订阅“新评论”事件。 -3. 使用表达式过滤事件:通过表达式 `props.repliedOwner == '%s'`,订阅者只会接收到`被评论者/回复者`是他们自己的评论通知。 -4. 执行订阅操作:调用 `notificationCenter.subscribe(subscriber, interestReason)` 方法完成订阅操作。 - -参考 [Halo 评论订阅](https://github.com/halo-dev/halo/blob/0d1a0992231fd5e66a65b4e9d426d3f373b1903f/application/src/main/java/run/halo/app/core/reconciler/CommentReconciler.java#L70) - -## 通知器 - -开发者可以通过 Halo 的通知器机制扩展通知的发送方式。 - -参考 [通知器扩展点](../../extension-points/server/notifier.md) diff --git a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/reconciler.md b/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/reconciler.md deleted file mode 100644 index 9aeed2b8..00000000 --- a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/reconciler.md +++ /dev/null @@ -1,365 +0,0 @@ ---- -title: 编写控制器 -description: 了解如何为自定义模型编写控制器 ---- - -控制器是 Halo 的关键组件,它们负责对每个自定义模型对象进行操作,协调所需状态和当前状态,参考: [控制器概述](../../../core/framework.md#controller)。 - -控制器通常在具有一般事件序列的控制循环中运行: - -1. 观察:每个控制器将被设计为观察一组自定义模型对象,例如文章的控制器会观察文章对象,插件的控制器会观察插件自定义模型对象等。 -2. 比较:控制器将对象配置的期望状态与其当前状态进行比较,以确定是否需要更改,例如插件的 `spec.enabled` 为 `true`,而插件的当前状态是未启动,则插件控制器会处理启动插件的逻辑。 -3. 操作:控制器将根据比较的结果执行相应的操作,以确保对象的实际状态与其期望状态一致,例如插件期望启动,插件控制器会处理启动插件的逻辑。 -3. 重复:上述所有步骤都由控制器重复执行直到与期望状态一致。 - -这是一个描述控制器作用的例子:房间里的温度自动调节器。 - -当你设置了温度,告诉了温度自动调节器你的期望状态(Desired State)。 -房间的实际温度是当前状态(Current State)。通过对设备的开关控制,温度自动调节器让其当前状态接近期望状态,未到达期望状态则继续调节,直到达到期望状态。 - -在 Halo 中控制器的运行部分已经有一个默认实现,你只需要编写控制器的调谐的逻辑也就是 [控制器概述](../../../core/framework.md#controller) 中的所说的 Reconciler 即可。 - -## 编写 Reconciler - -Reconciler 是控制器的核心,它是一个接口,你需要实现它的 `reconcile()` 方法,该方法接收一个 `Reconciler.Request` 对象,它包含了当前自定义模型对象的名称,你可以通过它来获取自定义模型对象的当前状态和期望状态,然后编写调谐的逻辑。 - -```java -@Component -public class PostReconciler implements Reconciler { - @Override - public Result reconcile(Request request) { - - } - - @Override - public Controller setupWith(ControllerBuilder builder) { - return builder - .extension(new Post()) - .build(); - } -} -``` - -以上是一个简单的 Reconciler 实现,它实现了 `reconcile()` 方法,然后在 `setupWith()` 方法中将其通过 `ControllerBuilder` 构建为一个控制器并指定了 -它要观察的自定义模型对象为`Post`,当文章自定义模型对象发生变化时,`reconcile()` 方法就会被调用,从 `Request request` 参数中你可以获得当前发生变化的文章自定义模型对象的名称,然后你就可以通过名称来查询到自定义模型对象进行调谐了。 - -### 构建控制器 - -`setupWith()` 方法用于根据当前类的 `reconcile` 方法构建控制器,你可以通过 `ControllerBuilder` 提供的方法来构建并定制控制器: - -```java -public class ControllerBuilder { - private final String name; - private Duration minDelay; - private Duration maxDelay; - private final Reconciler reconciler; - private Supplier nowSupplier; - private Extension extension; - private ExtensionMatcher onAddMatcher; - private ExtensionMatcher onDeleteMatcher; - private ExtensionMatcher onUpdateMatcher; - private ListOptions syncAllListOptions; - private boolean syncAllOnStart = true; - private int workerCount = 1; -} -``` - -- `name`:控制器的名称,用于标识控制器。 -- `minDelay`:控制器的最小延迟,用于控制控制器的最小调谐间隔,默认为 5 毫秒。 -- `maxDelay`:控制器的最大延迟,用于控制控制器的最大调谐间隔,默认为 1000 秒。 -- `reconciler`:控制器的调谐器,用于执行调谐逻辑,你需要实现 `Reconciler` 接口。 -- `nowSupplier`:用于获取当前时间的供应商,用于控制器的时间戳,默认使用 `Instant.now()` 获取当前时间。 -- `extension`:控制器要观察的自定义模型对象。 -- `onAddMatcher`:用于匹配添加事件的匹配器,当自定义模型对象被创建时会触发。 -- `onDeleteMatcher`:用于匹配删除事件的匹配器,当自定义模型对象被删除时会触发。 -- `onUpdateMatcher`:用于匹配更新事件的匹配器,当自定义模型对象被更新时会触发。 -- `syncAllListOptions`:用于同步所有自定义模型对象的查询条件,仅当 `syncAllOnStart` 为 `true` 时生效。 -- `syncAllOnStart`:是否在控制器启动时同步所有自定义模型对象,默认为 `true`,可以配合 `syncAllListOptions` 使用以缩小需要同步的对象范围避免不必要的同步,例如只同步某个用户创建的文章或者某个固定名称的 ConfigMap 对象。如果你的控制器不需要同步所有对象,可以将其设置为 `false`。 -- `workerCount`:控制器的工作线程数,用于控制控制器的并发度,如果你的控制器需要处理大量的对象,可以将其设置为大于 1 的值,以提高控制器的处理能力,但需要注意的是并发度越高,系统的负载也会越高。这里的并发度是指控制器的并发度,但是每个控制器还是单线程执行的。 - -#### ExtensionMatcher - -`onAddMatcher/onUpdateMatcher/onDeleteMatcher` 都是 `ExtensionMatcher` 类型,用于决定当自定义模型对象发生变化时是否触发控制器: - -```java -public interface ExtensionMatcher { - boolean match(Extension extension); -} -``` - -这里`match` 方法的 `Extension` 参数类型与 `ControllerBuilder` 中的 `extension` 类型始终是一致的,因此可以直接通过强制类型转换来得到需要的类型。 - -比如我们想要观察文章对象,但是只想观察文章对象中 `visible` 字段为 `PUBLIC` 的文章,可以这样 - -```java -public class PostReconciler implements Reconciler { - @Override - public Result reconcile(Request request) { - return Result.doNotRetry(); - } - - @Override - public Controller setupWith(ControllerBuilder builder) { - // 只想观察 VisibleEnum.PUBLIC 的文章 - ExtensionMatcher extensionMatcher = extension -> { - var post = (Post) extension; - return VisibleEnum.PUBLIC.equals(post.getSpec().getVisible()); - }; - return builder - .extension(new Post()) - .onAddMatcher(extensionMatcher) - .onUpdateMatcher(extensionMatcher) - .onDeleteMatcher(extensionMatcher) - .build(); - } -} -``` - -#### 控制启动时同步的范围 - -如果想要在控制器启动时控制同步对象的范围,可以通过 `syncAllListOptions` 和 `syncAllOnStart` 来实现,例如只同步某个用户创建的文章: - -```java -public class PostReconciler implements Reconciler { - @Override - public Result reconcile(Request request) { - return Result.doNotRetry(); - } - - @Override - public Controller setupWith(ControllerBuilder builder) { - return builder - .extension(new Post()) - .syncAllListOptions(ListOptions.builder() - .fieldQuery(QueryFactory.equal("spec.owner", "guqing")) - .build() - ) - .syncAllOnStart(true) - .build(); - } -} -``` - -### Reconciler 的返回值 - -`reconcile()` 方法的返回值是一个 `Result` 对象,它包含了调谐的结果,你可以通过它来告诉控制器是否需要重试,如果需要重试则控制器会在稍后再次调用 `reconcile()` 方法,而这个过程会一直重复,直到 `reconcile()` 方法返回成功为止,这个过程被称之为调谐循环(Reconciliation Loop)。 - -```java -record Result(boolean reEnqueue, Duration retryAfter) {} -``` - -`Result` 对象包含了两个属性:reEnqueue 和 retryAfter,reEnqueue 用于标识是否需要重试,retryAfter 用于标识重试的时间间隔,如果 reEnqueue 为 true 则会在 retryAfter 指定的时间间隔后再次调用 `reconcile()` 方法,如果 reEnqueue 为 false 则不会再次调用 `reconcile()` 方法。 - -在没有特殊需要时,`retryAfter` 可以不指定,控制器会有一套默认的重试策略。 - -如果直接返回 `null` 则会被视为成功,效果等同于返回 `new Result(false, null)`。 - -### Reconciler 的异常处理 - -当 `reconcile()` 方法抛出异常时,控制器会将异常记录到日志中,然后会将 `Request request` 对象重新放入队列中,等待下次调用 `reconcile()` 方法,这个过程会一直重复,直到 `reconcile()` 成功,对于默认重试策略,每次重试间隔会越来越长,直到达到最长间隔后不再增加。 - -## 控制器示例 - -本章节将通过一个简单的示例来演示如何编写控制器。 - -### 场景:事件管理系统 - -创建一个名为”EventTracker“的自定义模型,用于管理和追踪组织内的各种事件。这些事件可以是会议、研讨会、社交聚会或任何其他类型的组织活动。 -“EventTracker“自定义模型将提供一个框架,用于记录事件的详细信息,如时间、地点、参与者和状态。 - -由于这里的重点是控制器,因此我们将忽略自定义模型的详细信息,只关注控制器的实现,一个可能的“EventTracker”数据结构如下: - -```yaml -apiVersion: tracker.halo.run/v1alpha1 -kind: EventTracker -metadata: - name: event-tracker-1 -spec: - eventName: "Halo Meetup" - eventDate: "2024-01-20T12:00:00Z" - location: "Chengdu" - participants: ["@sig-doc", "@sig-console", "@sig-halo"] - description: "Halo Meetup in Chengdu" -status: - phase: "Planned" # Planned, Ongoing, Completed - participants: [] - conditions: - - type: "Invalid" - status: "True" - reason: "InvalidEventDate" - message: "Event date is invalid" -``` - -业务逻辑处理: - -1. 事件创建: - - - 当新的 EventTracker 资源被创建时,控制器需验证所有必要字段的存在和格式正确性。 - - 初始化事件状态为 Planned。 - -2. 事件更新: - - - 检查 eventDate、location 和 participants 字段的变更。 - - 如果接近事件日期,自动更新状态为 Ongoing。 - -3. 状态管理: - - - 根据当前日期和事件日期自动管理 phase 字段。 - - 当事件日期过去时,将状态更新为 Completed。 -4. 数据验证和完整性: - - 确保所有输入数据的格式正确且合理。 - - 如有不一致或缺失的重要信息,记录警告或错误。 -5. 事件提醒和通知: - - 在事件状态改变或临近事件日期时发送通知。 -6. 清理和维护: - - 对于已完成的事件,提供自动清理机制,例如在事件结束后一定时间内删除资源。 - -首先实现 EventTracker 控制器的协调循环主体,通过依赖注入 `ExtensionClient` 可以用于获取当前变更的对象: - -```java -@Component -@RequiredArgsConstructor -public class EventTrackerReconciler implements Reconciler { - - private final ExtensionClient client; - - @Override - public Result reconcile(Request request) { - // ... - } - - @Override - public Controller setupWith(ControllerBuilder builder) { - return builder - .extension(new EventTracker()) - .build(); - } -} -``` - -然后在 `reconcile()` 方法中根据 `EventTracker` 对象的状态来执行响应的操作,确保执行逻辑是是幂等的,这意味着即使多次执行相同操作,结果也应该是一致的。 - -```java -public Result reconcile(Request request) { - client.fetch(EventTracker.class, request.name()).ifPresent(eventTracker -> { - // 获取到当前变更的 EventTracker 对象 - // 1. 检查必要字段的存在和格式正确性 - // 2. 初始化事件状态为 Planned - if (eventTracker.getStatus() == null) { - eventTracker.setStatus(new EventTracker.Status()); - } - var status = eventTracker.getStatus(); - if (status.getPhase() == null) { - status.setPhase(EventTracker.Phase.PLANNED); - } - - var eventName = eventTracker.getSpec().getEventName(); - if (StringUtils.isBlank(eventName)) { - Condition condition = Condition.builder() - .type("Invalid") - .reason("InvalidEventName") - .message("Event name is invalid") - .status(ConditionStatus.FALSE) - .lastTransitionTime(Instant.now()) - .build(); - status.getConditions().addAndEvictFIFO(condition); - } - - client.update(eventTracker); - }); - return new Result(false, null); -} -``` - -上述,我们通过 `client.fetch()` 方法获取到了当前变更的 `EventTracker` 对象,然后根据 `EventTracker` 对象的状态来执行响应的操作,例如初始化事件状态为 Planned,检查必要字段的存在和格式正确性等,但需要注意控制器的执行是异步的,如果我们通过 `EventTracker` 的 API 来创建或更改了一个 `EventTracker` 对象,那么 API 会在控制器执行之前返回结果,这意味着在用户界面看到的结果可能不是最新的,并且可能会在稍后更新。 - -对于上述校验 `eventName` 的逻辑只是保证后续的执行是可靠的,如果有些字段是必须的,那么我们可以通过 `@Schema` 注解来标注,为了让控制器中校验字段失败的信息能够呈现到用户界面,我们通过向 `status.conditions` 中添加了一条 Condition 记录来用于记录这个事件,再用户界面可以展示这个 Condition 记录的信息以让用户知晓。 - -最后,我们通过 `client.update()` 方法来更新 `EventTracker` 对象,这个过程就是将实际状态回写到 `EventTracker` 对象并应用到数据库中,这样就完成了一次调谐。 - -当 `EventTracker` 对象发生变更时,控制器也会被执行,这时我们可以根据 `EventTracker` 对象的状态来执行响应的操作,例如检查和更新 `eventDate`、`location` 和 `participants` 字段的变更,如果接近事件日期,自动更新状态为 Ongoing。 - -```java -public Result reconcile(Request request) { - client.fetch(EventTracker.class, request.name()).ifPresent(eventTracker -> { - // ...此处省略之前的逻辑 - if (isApproach(eventTracker.getSpec().getEventDate())) { - status.setPhase(EventTracker.Phase.ONGOING); - sendNotification(eventTracker, "Event is ongoing"); - } - }); - return new Result(false, null); -} -``` - -这里我们通过 `isApproach()` 方法来表示判断是否接近事件日期,如果接近则更新状态为 Ongoing,并使用 `sendNotification` 来发送发送通知。 - -> 为了简化示例,我们省略了 `isApproach()` 和 `sendNotification` 方法的实现。 - -还可以根据 `spec.participants` 字段来解析参与者信息,然后将其添加到 `status.participants` 中,这样就可以在用户界面看到参与者信息了。 - -```java -public Result reconcile(Request request) { - client.fetch(EventTracker.class, request.name()).ifPresent(eventTracker -> { - // ...此处省略之前的逻辑 - var participants = eventTracker.getSpec().getParticipants(); - resolveParticipants(participants).forEach(status::addParticipant); - }); - return new Result(false, null); -} -``` - -### 使用 Finalizers - -`Finalizers` 允许控制器实现异步预删除钩子。假设您为正在实现的 API 类型的每个对象创建了一个外部资源,例如存储桶,并且您希望在从 Halo 中删除相应对象 -时清理外部资源,您可以使用终结器来删除外部资源资源。 - -比如 `EventTracker` 对象被删除时,我们需要删除 `EventTracker` 对象记录的日志,这时我们可以通过 `Finalizers` 来实现。 - -首先我们需要在 `reconcile()` 的开头判断 `EventTracker` 对象的 `metadata.deletionTimestamp` 是否存在,如果存在则表示 `EventTracker` 对象被删除了, -这时我们就可以执行清理操作。 - -```java -public Result reconcile(Request request) { - client.fetch(EventTracker.class, request.name()).ifPresent(eventTracker -> { - if (ExtensionOperator.isDeleted(eventTracker)) { // 1. 判断是否被删除 - // 2. 调用 removeFinalizers 方法移除终结器(稍后会说明) - ExtensionUtil.removeFinalizers(eventTracker.getMetadata(), Set.of(FINALIZER_NAME)); - // 3. 执行清理操作 - cleanUpLogsForTracker(eventTracker); - // 4. 更新 EventTracker 对象将变更应用到数据库中 - client.update(eventTracker); - // 5. return 避免执行后续逻辑 - return; - } - // ...此处省略之前的逻辑 - }); - return new Result(false, null); -} -``` - -1. `ExtensionOperator.isDeleted` 方法是 Halo 提供的工具方法,用于判断对象是否被删除,它会判断 `metadata.deletionTimestamp` 是否存在,如果存在则表示对象被标记删除了。 -关于自定义模型对象的删除可以参考:[自定义模型对象生命周期](../../../core/framework.md#extension-lifecycle) -2. `ExtensionUtil.removeFinalizers` 方法是 Halo 提供的工具方法,用于移除对象的终结器,它接收两个参数,第一个参数是对象的元数据,第二个参数是要移除的终结器名称集合,它来自 `run.halo.app.extension.ExtensionUtil`。 -3. `cleanUpLogsForTracker` 方法是我们自己实现的,这里的示例是用于清理 `EventTracker` 对象记录的日志,你可以根据自己的业务需求来实现,如清理外部资源等。 - -经过上述步骤,我们只是写了移除终结器但是发现没有添加终结器的逻辑,添加终结器的逻辑需要在判断删除之后,`metadata.finalizers` 是一个字符串集合,用于标识对象是否可回收,如果 `metadata.finalizers` 不为空则表示对象不可回收,否则表示对象可回收,我们可以通过 `ExtensionUtil.addFinalizers` 方法来添加终结器。 - -最佳实践是,一个控制器最多添加一个终结器,名称为了防止冲突可以使用当前业务的 `group/终结器名称` 来命名,例如 `tracker.halo.run/finalizer`,例如在 Halo 中文章的控制器使用了一个终结器,但可能插件也会定义一个文章控制器来扩展文章的业务,那么根据最佳实践命名终结器可以避免冲突。 - -```java -private static final String FINALIZER_NAME = "tracker.halo.run/finalizer"; - -public Result reconcile(Request request) { - client.fetch(EventTracker.class, request.name()).ifPresent(eventTracker -> { - if (ExtensionOperator.isDeleted(eventTracker)) { -// ... 省略删除逻辑 - } - // 添加终结器 - ExtensionUtil.addFinalizers(post.getMetadata(), Set.of(FINALIZER_NAME)); - // ...此处省略之前的逻辑 - // 会在更新时将终结器的变更写入到数据库中 - client.update(eventTracker); - }); -} -``` diff --git a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/reverseproxy.md b/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/reverseproxy.md deleted file mode 100644 index ff0ae286..00000000 --- a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/reverseproxy.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: 静态资源代理 -description: 了解如何使用静态资源代理来访问插件中的静态资源 ---- - -插件中的静态资源如图片等如果想被外部访问到,需要放到 `src/main/resources` 目录下,并通过创建 `ReverseProxy` 自定义模型对象来进行静态资源代理访问。 - -例如 `src/main/resources` 下的 `static` 目录下有一张 `halo.jpg`: - -1. 首先需要在 `src/main/resources/extensions` 下创建一个 `yaml`,文件名可以任意。 -2. 声明 `ReverseProxy` 对象如下: - - ```yaml - apiVersion: plugin.halo.run/v1alpha1 - kind: ReverseProxy - metadata: - # 为了避免与其他插件冲突,推荐带上插件名称前缀 - name: my-plugin-fake-reverse-proxy - rules: - - path: /res/** - file: - directory: static - # 如果想代理 static 下所有静态资源则省略 filename 配置 - filename: halo.jpg - ``` - -插件启动后会根据 `/plugins/{plugin-name}/assets/**` 规则生成访问路径, -因此该 `ReverseProxy` 的访问路径为:`/plugins/my-plugin/assets/res/halo.jpg`。 - -- `rules` 下可以添加多组规则。 -- `path` 为路径前缀。 -- `file` 表示访问文件系统,目前暂时仅支持这一种。 -- `directory` 表示要代理的目标文件目录,它相对于 `src/main/resources/` 目录。 -- `filename` 表示要代理的目标文件名。 - -`directory` 和 `filename` 都是可选的,但必须至少有一个被配置。 diff --git a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/setting-fetcher.md b/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/setting-fetcher.md deleted file mode 100644 index a572a39c..00000000 --- a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/setting-fetcher.md +++ /dev/null @@ -1,139 +0,0 @@ ---- -title: 获取插件配置 -description: 了解如何获取插件定义的设置表单对应的配置数据,以及如何在插件中使用配置数据。 ---- - -插件的 `plugin.yaml` 中允许配置 `settingName` 和 `configMapName` 字段,用于定义插件的个性化设置。 -本文介绍如何获取插件定义的设置表单对应的配置数据,以及如何在插件中使用配置数据。 - -## 概述 - -Halo 提供了两个 Bean 用于获取插件配置数据:`SettingFetcher` 和 `ReactiveSettingFetcher`,分别用于同步和异步获取配置数据。 - -以 `ReactiveSettingFetcher` 为例,提供了以下方法: - -```java -public interface ReactiveSettingFetcher { - - Mono fetch(String group, Class clazz); - - @NonNull - Mono get(String group); - - @NonNull - Mono> getValues(); -} -``` - -- `fetch` 方法用于获取指定分组的配置数据,并将其转换为指定的 Java 类型。 -- `get` 方法用于获取指定分组的配置数据,返回 `JsonNode` 类型。 -- `getValues` 方法用于获取所有配置数据,返回 `Map` 类型,其中键为分组名称,值为配置对象。 - -`ReactiveSettingFetcher` 和 `SettingFetcher` 底层都对配置数据进行了缓存,以提高性能,并且在配置变更时会自动刷新缓存,所以直接调用这些方法即可获取最新的配置数据。 - -## 监听配置变更 - -当用户修改插件配置时,可以通过监听 `PluginConfigUpdatedEvent` 事件,执行相应的操作。`PluginConfigUpdatedEvent` 包含了配置变更前后的数据,使插件能够对变化做出响应。 - -```java -public class PluginConfigUpdatedEvent extends ApplicationEvent { - private final Map oldConfig; - private final Map newConfig; - - // ... -} -``` - -## 使用示例 - -### 定义设置表单 - -假设插件定义了一个名为 `setting-seo` 的设置表单,其中包含了 `blockSpiders`、`keywords` 和 `description` 三个字段: - -```yaml -apiVersion: v1alpha1 -kind: Setting -metadata: - name: setting-seo -spec: - forms: - - group: seo - label: SEO 设置 - formSchema: - - $formkit: checkbox - name: blockSpiders - label: "屏蔽搜索引擎" - value: false - - $formkit: textarea - name: keywords - label: "站点关键词" - - $formkit: textarea - name: description - label: "站点描述" -``` - -### 配置 plugin.yaml - -在 `plugin.yaml` 中配置 `settingName` 和 `configMapName` 字段: - -```yaml -apiVersion: plugin.halo.run/v1alpha1 -kind: Plugin -metadata: - name: fake-plugin -spec: - displayName: "Fake Plugin" - # ... - configMapName: setting-seo-configmap - settingName: setting-seo -``` - -### 定义值类 - -为了方便使用,定义一个值类存储配置数据: - -```java -public record SeoSetting(boolean blockSpiders, String keywords, String description) { - public static final String GROUP = "seo"; -} -``` - -### 获取配置数据 - -通过依赖注入 `ReactiveSettingFetcher` 并使用 `fetch(group, type)` 方法查询配置: - -```java -@Service -@RequiredArgsConstructor -public class SeoService { - private final ReactiveSettingFetcher settingFetcher; - - public Mono checkSeo() { - return settingFetcher.fetch(SeoSetting.GROUP, SeoSetting.class) - .doOnNext(seoSetting -> { - if (seoSetting.blockSpiders()) { - // do something - } - }) - .then(); - } -} -``` - -### 监听配置变更 - -通过监听 `PluginConfigUpdatedEvent` 事件来处理配置变更: - -```java -@Component -public class SeoConfigListener { - @EventListener - public void onConfigUpdated(PluginConfigUpdatedEvent event) { - if (event.getNewConfig().containsKey(SeoSetting.GROUP)) { - // do something - } - } -} -``` - -通过以上示例,可以看到如何使用 `ReactiveSettingFetcher` 获取配置数据,并通过监听 `PluginConfigUpdatedEvent` 来处理配置变更事件,确保系统能及时响应配置的变化。 diff --git a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/template-for-theme.md b/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/template-for-theme.md deleted file mode 100644 index fed27911..00000000 --- a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/template-for-theme.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -title: 在插件中提供主题模板 -description: 了解如何为主题扩充模板。 ---- - -当你在插件中创建了自己的自定义模型后,你可能需要在主题端提供一个模板来展示这些数据,这一般有两种方式: - -1. 插件规定模板名称,由主题选择性适配,如瞬间插件提供了 `/moments` 的路由渲染 `moment.html` 模板,主题可以选择性的提供 `moment.html` 模板来展示瞬间数据。 -2. 插件提供默认模板,当主题没有提供对应的模板时,使用默认模板,主题提供了对应的模板时,使用主题提供的模板。 - -## 创建一个模板 - -首先,你需要在插件的 `resources` 目录下创建一个 `templates` 目录,然后在 `templates` 目录下提供你的模板,例如: - -```text -├── templates -│ ├── moment.html -``` - -然后提供一个路由用于渲染这个模板,例如: - -```java -import run.halo.app.theme.TemplateNameResolver; - -@RequiredArgsConstructor -@Configuration(proxyBeanMethods = false) -public class MomentRouter { - private final TemplateNameResolver templateNameResolver; - - @Bean - RouterFunction momentRouterFunction() { - return route(GET("/moments"), this::renderMomentPage).build(); - } - - Mono renderMomentPage(ServerRequest request) { - // 或许你需要准备你需要提供给模板的默认数据,非必须 - var model = new HashMap(); - model.put("moments", List.of()); - return templateNameResolver.resolveTemplateNameOrDefault(request.exchange(), "moments") - .flatMap(templateName -> ServerResponse.ok().render(templateName, model)); - } -} -``` - -使用 `TemplateNameResolver` 来解析模板名称,如果主题提供了对应的模板,那么就使用主题提供的模板,否则使用插件提供的模板,如果直接返回模板名称,那么只会使用主题提供的模板,如果主题没有提供对应的模板,那么会抛出异常。 - -## 模板片段 - -如果你的默认模板不止一个,你可能需要通过模板片段来抽取一些公共的部分,例如,你的插件提供了一个 `moment.html` 模板,你可能需要抽取一些公共的部分,例如头部、尾部等,你可以这样做: - -```text -├── templates -│ ├── moment.html -│ ├── fragments -│ │ ├── layout.html -``` - -然后定义一个 `layout.html` 模板,例如: - -```html - - - - - Moment - - -
- -
- - -``` - -那么使用 `layout.html` 模板中提供的 `fragment` 时,你需要这样做: - -```html -
- Hello World -
-``` - -`plugin:plugin-moment:fragments/layout` 即为使用 `layout.html` 模板的路径,必须以 `plugin::`前缀作为开头,`fragments/layout` 为模板相对于 `resources/templates` 的路径,`` 即为你的插件名称。 - -**总结:** - -1. 定义模板片段时与主题端定义模板片段时一样 -2. 使用模板片段时,必须以 `plugin::` 前缀作为开头,后跟模板相对于 `resources/templates` 的路径,例如 `plugin:plugin-moment:fragments/layout`,`plugin-moment` 即为你的插件名称,`fragments/layout` 为模板相对于 `resources/templates` 的路径。 - -参考:[Thymeleaf 模板片段](https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#including-template-fragments) diff --git a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/websocket.md b/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/websocket.md deleted file mode 100644 index 503d2968..00000000 --- a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/server/websocket.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: 实现 WebSocket -description: 了解在插件中如何实现 WebSocket。 ---- - -从 Halo 2.15.0 版本开始,核心提供了 WebSocketEndpoint 接口,其主要目的是为了方便插件实现 WebSocket 功能。 - -插件只需要实现这个接口,并添加 `@Component` 注解,WebSocket 实现将会在插件启动后生效,插件卸载后,该实现也会随之删除。 - -在插件中实现 WebSocket 的代码样例如下: - -```java -@Component -public class MyWebSocketEndpoint implements WebSocketEndpoint { - - @Override - public GroupVersion groupVersion() { - return GroupVersion.parseApiVersion("my-plugin.halowrite.com/v1alpha1"); - } - - @Override - public String urlPath() { - return "/resources"; - } - - @Override - public WebSocketHandler handler() { - return session -> { - var messages = session.receive() - .map(message -> { - var payload = message.getPayloadAsText(); - return session.textMessage(payload.toUpperCase()); - }); - return session.send(messages); - }; - } -} -``` - -当插件安装成功后,可以通过路径 `/apis/my-plugin.halowrite.com/v1alpha1/resources` 访问。示例如下: - -```bash -websocat --basic-auth admin:admin ws://127.0.0.1:8090/apis/my-plugin.halowrite.com/v1alpha1/resources -``` - -需要注意的是,插件中实现的 WebSocket 相关的 API 仍然受当前权限系统约束。 diff --git a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/ui/api-request.md b/versioned_docs/version-2.20/developer-guide/plugin/api-reference/ui/api-request.md deleted file mode 100644 index f7d4a724..00000000 --- a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/ui/api-request.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: API 请求 -description: 介绍如何在插件的 UI 中请求 API 接口 ---- - -在 2.17.0 版本中,Halo 提供了新的 `@halo-dev/api-client` 包,用于简化在 Halo 内部、插件的 UI 中、外部应用程序中请求 Halo 接口的逻辑。此文档将介绍如何在插件的 UI 中使用 `@halo-dev/api-client` 包。 - -## 安装 - -```shell -pnpm install @halo-dev/api-client axios -``` - -## 模块介绍 - -在 `@halo-dev/api-client` 包中导出了以下模块: - -```ts -import { - coreApiClient, - consoleApiClient, - ucApiClient, - publicApiClient, - createCoreApiClient, - createConsoleApiClient, - createUcApiClient, - createPublicApiClient, - axiosInstance -} from "@halo-dev/api-client" -``` - -- **coreApiClient**: 为 Halo 所有自定义模型的 CRUD 接口封装的 API Client。 -- **consoleApiClient**: 为 Halo 针对 Console 提供的接口封装的 API Client。 -- **ucApiClient**: 为 Halo 针对 UC 提供的接口封装的 API Client。 -- **publicApiClient**: 为 Halo 所有公开访问的接口封装的 API Client。 -- **createCoreApiClient**: 用于创建自定义模型的 CRUD 接口封装的 API Client,需要传入 axios 实例。 -- **createConsoleApiClient**: 用于创建 Console 接口封装的 API Client,需要传入 axios 实例。 -- **createUcApiClient**: 用于创建 UC 接口封装的 API Client,需要传入 axios 实例。 -- **createPublicApiClient**: 用于创建公开访问接口封装的 API Client,需要传入 axios 实例。 -- **axiosInstance**: 内部默认创建的 axios 实例。 - -## 使用 - -在 Halo 的插件项目中,如果是调用 Halo 内部的接口,那么直接使用上面介绍的模块即可,无需任何配置,在 Halo 内部已经处理好了异常逻辑,包括登录失效、无权限等。 - -其中,`coreApiClient`、`consoleApiClient`、`ucApiClient`、`publicApiClient` 模块是对 Halo 内部所有 API 请求的封装,无需传入任何请求地址,比如: - -```ts -import { coreApiClient } from "@halo-dev/api-client" - -coreApiClient.content.post.listPost().then(response => { - // handle response -}) -``` - -如果需要调用插件提供的接口,可以直接使用 `axiosInstance` 实例,比如: - -```ts -import { axiosInstance } from "@halo-dev/api-client" - -axiosInstance.get("/apis/foo.halo.run/v1alpha1/bar").then(response => { - // handle response -}) -``` - -此外,在最新的 `@halo-dev/ui-plugin-bundler-kit@2.17.0` 中,已经排除了 `@halo-dev/api-client`、`axios` 依赖,所以最终产物中的相关依赖会自动使用 Halo 本身提供的依赖,无需关心最终产物大小。 - -:::info[提醒] -如果插件中使用了 `@halo-dev/api-client@2.17.0` 和 `@halo-dev/ui-plugin-bundler-kit@2.17.0`,需要提升 `plugin.yaml` 中的 `spec.requires` 版本为 `>=2.17.0`。 -::: diff --git a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/ui/components/annotations-form.md b/versioned_docs/version-2.20/developer-guide/plugin/api-reference/ui/components/annotations-form.md deleted file mode 100644 index 98f4e349..00000000 --- a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/ui/components/annotations-form.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: AnnotationsForm -description: 元数据表单组件 ---- - -此组件用于提供统一的 [Annotations 表单](../../../../annotations-form.md),可以根据 `group` 和 `kind` 属性自动渲染对应的表单项。 - -## 使用示例 - -```html - - - -``` - -## Props - -| 属性名 | 类型 | 默认值 | 描述 | -|---------|------------------------------------|---------|-----------------------------------------| -| `group` | string | 无,必填 | 定义组件所属的分组。 | -| `kind` | string | 无,必填 | 定义组件的种类。 | -| `value` | \{ [key: string]: string; \} \| null \| null | 可选,包含键值对的对象或空值,用于存储数据。 | diff --git a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/ui/components/attachment-file-type-icon.md b/versioned_docs/version-2.20/developer-guide/plugin/api-reference/ui/components/attachment-file-type-icon.md deleted file mode 100644 index 2b24894f..00000000 --- a/versioned_docs/version-2.20/developer-guide/plugin/api-reference/ui/components/attachment-file-type-icon.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: AttachmentFileTypeIcon -description: 附件文件类型图标组件 ---- - -此组件用于根据文件名显示文件类型图标。 - -## 使用示例 - -```html - - -