docs: update plugin development document

Signed-off-by: Ryan Wang <i@ryanc.cc>
This commit is contained in:
Ryan Wang
2022-07-15 18:03:10 +08:00
parent a058a08907
commit cb8508bb5f

405
README.md
View File

@@ -1,6 +1,6 @@
# halo-plugin-template # plugin-template
A Halo plugin template for backend development Halo 2.0 插件开发快速开始模板WIP
## 如何开发一个 Halo 插件 ## 如何开发一个 Halo 插件
@@ -16,176 +16,191 @@ A Halo plugin template for backend development
├── LICENSE ├── LICENSE
├── README.md ├── README.md
├── admin-frontend ├── admin-frontend
│   ├── ... │   ├── README.md
├── build │   ├── env.d.ts
│   ├── classes │   ├── package.json
│   ├── libs │   ├── pnpm-lock.yaml
│   │   ├── halo-plugin-template-0.0.1-SNAPSHOT-plain.jar │   ├── src
│   ├── resources │   │   ├── assets
│   │   │   └── logo.svg
│   │   ├── components
│   │   │   └── HelloWorld.vue
│   │   ├── index.ts # Admin Frontend Entry file
│   │   ├── styles
│   │   │   └── index.css
│   │   └── views
│   │   └── DefaultView.vue # Views Component
│   ├── tsconfig.app.json
│   ├── tsconfig.config.json
│   ├── tsconfig.json
│   ├── tsconfig.vitest.json
│   └── vite.config.ts
├── build.gradle ├── build.gradle
├── gradle
│   └── wrapper
│   ├── gradle-wrapper.jar
│   └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── lib ├── lib
│   └── halo-2.0.0-SNAPSHOT-plain.jar │   └── halo-2.0.0-SNAPSHOT-plain.jar
├── settings.gradle ├── settings.gradle
└── src └── src
└── main └── main
├── java ├── java
│   └── io │   └── run
│   └── github │   └── halo
│   └── guqing
│   └── template │   └── template
│   ├── Apple.java │   ├── Apple.java
│   ├── ApplePlugin.java │   ├── ApplesController.java
│   └── ApplesController.java │   └── TemplatePlugin.java # Main Class
└── resources └── resources
├── admin ├── admin
│   ├── main.js │   ├── main.js # Admin Frontend Entry file(production build)
│   └── style.css │   └── style.css
├── extensions ├── extensions
│   ├── apple.yaml │   ├── apple.yaml
│   ├── reverseProxy.yaml │   ├── reverseProxy.yaml # Reverse Proxy Config
│   ── roleTemplate.yaml │   ── roleTemplate.yaml # Role Template Config
├── plugin.yaml │   └── settings.yaml # Settings Config
├── plugin.yaml # Plugin Config
└── static └── static
├── ... ├── image.jpeg
├── some.txt
└── test.html
``` ```
对于以上目录树: 对于以上目录树:
- `admin-frontend` 插件前端项目目录,为一个 vue 项目 - `admin-frontend` 插件前端项目目录,为一个 Vue 项目,技术栈为 Vue 3 + Vite其中已经预配置好了构建策略。
- `build`:插件后端构建目录,`build/libs` 下的 jar 包为最终插件产物 - `build`:插件后端构建目录,`build/libs` 下的 jar 包为最终插件产物
- `lib`:为临时的 halo 依赖,为了使用 halo 中提供的类在 `build.gradle` 中作为编译时依赖引入 `compileOnly files("lib/halo-2.0.0-SNAPSHOT-plain.jar")`,待 `2.0` 正式发布会将其发布到 `maven` 中央仓库,便可通过 `gradle `依赖; - `lib`:为临时的 Halo 依赖,为了使用 Halo 中提供的类在 `build.gradle` 中作为编译时依赖引入 `compileOnly files("lib/halo-2.0.0-SNAPSHOT-plain.jar")`
,待 `2.0` 正式发布会将其发布到 `maven` 中央仓库,便可通过 `gradle` 依赖。
- `src` 为插件后端源码目录。
- `Apple.java` 为自定义模型。
- `src`: 为插件后端源码目录; ```java
- `Apple.java` 为自定义模型 @GVK(group = "apple.guqing.xyz", kind = "Apple",
```java
@GVK(group = "apple.guqing.xyz", kind = "Apple",
version = "v1alpha1", singular = "apple", plural = "apples") version = "v1alpha1", singular = "apple", plural = "apples")
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class Apple extends AbstractExtension {} public class Apple extends AbstractExtension {
``` }
```
关键在于标注 `@GVK `注解和 `extends AbstractExtension`,当如此定义了一个模型后,插件启动时会根据 `@GVK` 配置自动生成`CRUD``RESTful API`,以此为例子会生成如下`APIs` 关键在于标注 `@GVK` 注解和 `extends AbstractExtension`,当如此定义了一个模型后,插件启动时会根据 `@GVK` 配置自动生成`CRUD`的 `RESTful API`
,以此为例子会生成如下 `APIs`
```text ```http
GET /apis/apple.guqing.xyz/v1alpha1/apples GET /apis/apple.guqing.xyz/v1alpha1/apples
POST /apis/apple.guqing.xyz/v1alpha1/apples POST /apis/apple.guqing.xyz/v1alpha1/apples
GET /apis/apple.guqing.xyz/v1alpha1/apples/{name} GET /apis/apple.guqing.xyz/v1alpha1/apples/{name}
PUT /apis/apple.guqing.xyz/v1alpha1/apples/{name} PUT /apis/apple.guqing.xyz/v1alpha1/apples/{name}
DELETE /apis/apple.guqing.xyz/v1alpha1/apples/{name} DELETE /apis/apple.guqing.xyz/v1alpha1/apples/{name}
``` ```
生成规则见:[Halo extension RFC](https://github.com/halo-dev/rfcs/tree/main/extension) 生成规则见:[Halo extension RFC](https://github.com/halo-dev/rfcs/tree/main/extension)
- `ApplePlugin.java`:插件生命周期入口,它继承 `BasePlugin`,可以通过`getApplicationContext()`方法获取到 `SchemeManager`,然后在 `start()` 方法中注册自定义模型,这一步必不可少,所有定义的自定义模型都需要在此注册,并在 `stop()` 生命周期方法中清理资源。 - `TemplatePlugin.java`:插件生命周期入口,它继承 `BasePlugin`,可以通过 `getApplicationContext()` 方法获取到 `SchemeManager`,然后在 `start()`
方法中注册自定义模型,这一步必不可少,所有定义的自定义模型都需要在此注册,并在 `stop()` 生命周期方法中清理资源。
```java ```java
@Override public class TemplatePlugin extends BasePlugin {
public void start() { // ...
@Override
public void start() {
schemeManager.register(Apple.class); schemeManager.register(Apple.class);
} }
@Override @Override
public void stop() { public void stop() {
// TODO 优化取消注册自定义模型的写法
Scheme scheme = schemeManager.get(Apple.class); Scheme scheme = schemeManager.get(Apple.class);
schemeManager.unregister(scheme); schemeManager.unregister(scheme);
} }
```
注意:该类不能标注 `@Component` 等能将其声明为 `Spring Bean` 的注解 // ...
}
```
- `ApplesController.java`:如果根据模型自动生成的 `CURD RESTful APIs` 无法满足业务需要,可以写常规 `Controller` 来自定义`APIs`,示例: 注意:该类不能标注 `@Component` 等能将其声明为 `Spring Bean` 的注解
```java - `ApplesController.java`:如果根据模型自动生成的 `CURD RESTful APIs` 无法满足业务需要,可以写常规 `Controller` 来自定义 `APIs`,示例:
@ApiVersion("v1alpha1")
@RestController ```java
@RequestMapping("colors")
public class ApplesController { @ApiVersion("v1alpha1")
@RestController
@RequestMapping("colors")
public class ApplesController {
@GetMapping @GetMapping
public Mono<String> hello() { public Mono<String> hello() {
return Mono.just("Hello world"); return Mono.just("Hello world");
} }
} }
``` ```
插件定义 `Controller` 必须要标注 `@ApiVersion` 注解,否则启动时会报错。如果定义了这样的 `Controller` ,插件启动后会生成如下的 `APIs` 插件定义 `Controller` 必须要标注 `@ApiVersion` 注解,否则启动时会报错。如果定义了这样的 `Controller` ,插件启动后会生成如下的 `APIs`
```text ```http
GET /api/v1alpha1/plugins/apples/colors GET /api/v1alpha1/plugins/apples/colors
``` ```
生成规则为 `/api/{version}/plugins/{plugin-name}/{mapping-in-class}/{mapping-in-method}` 生成规则为 `/api/{version}/plugins/{plugin-name}/{mapping-in-class}/{mapping-in-method}`
其中: 其中:
- `version`:来自 `@ApiVersion("v1alpha1") `的 value详情参考[Halo plugin API composition](https://github.com/halo-dev/rfcs/blob/main/plugin/pluggable-design.md#api-%E6%9E%84%E6%88%90%E8%AE%A8%E8%AE%BA) - `version`:来自 `@ApiVersion("v1alpha1")` 的
value详情参考[Halo plugin API composition](https://github.com/halo-dev/rfcs/blob/main/plugin/pluggable-design.md#api-%E6%9E%84%E6%88%90%E8%AE%A8%E8%AE%BA)
- `plugin-name`:值来自 `plugin.yaml` 中的 `metadata.name` 属性
- `mapping-in-class`:来自标注在类上的 `@RequestMapping("colors")`
- `mapping-in-method`:来自标注在方法上的 `@GetMapping`
- `plugin-name`:值来自 `plugin.yaml` 中的 `metadata.name` 属性 插件还允许使用 `@Service`、`@Component` 注解将其声明为一个 `Spring Bean`,便可通过依赖注入使用 `Spring Bean`,示例:
- `mapping-in-class`:来自标注在类上的 `@RequestMapping("colors")`
- `mapping-in-method`:来自标注在方法上的 `@GetMapping`
插件还允许使用 `@Service``@Component` 注解将其声明为一个 `Spring Bean`,便可通过依赖注入使用 `Spring Bean`,示例: ```java
```java @Service
@Service public class ColorService {
public class ColorService {
public String getColor(String appleName) { public String getColor(String appleName) {
return "red"; return "red";
} }
} }
@ApiVersion("v1alpha1") @ApiVersion("v1alpha1")
@RestController @RestController
@RequestMapping("colors") @RequestMapping("colors")
public class ApplesController { public class ApplesController {
private final ColorService colorService; private final ColorService colorService;
// 构造器注入,当然也同样允许 @Autowired 注入 和 setter 方法注入 // 构造器注入,当然也同样允许 @Autowired 注入 和 setter 方法注入
public ApplesController(ColorService colorService) { public ApplesController(ColorService colorService) {
this.colorService = colorService; this.colorService = colorService;
} }
} }
``` ```
- `resources`:目录为插件资源目录 - `resources`:目录为插件资源目录
- `admin` 目录下为插件前端打包后的产物存放目录,固定为 `main.js` 和 `style.css `两个文件
- `extensions` 存放自定义模型资源配置
- `plugin.yaml`为插件描述配置
- `static` 为静态资源示例目录
`admin` 目录下为插件前端打包后的产物存放目录,固定为 `main.js``style.css `两个文件 插件启动时会加载 `extensions` 目录的 `yaml` 保存到数据库,`apple.yaml` 为本项目自定义的模型的数据示例,当启用插件后调用
`extensions` 存放自定义模型资源配置 ```text
GET /apis/apple.guqing.xyz/v1alpha1/apples
```
`plugin.yaml`为插件描述配置 便可查询到 `apple.yaml` 中定义的记录
`static` 为静态资源示例目录 ```json
[
├── admin
│ ├── main.js
│ └── style.css
├── extensions
│ ├── apple.yaml
│ ├── reverseProxy.yaml
│ └── roleTemplate.yaml
├── plugin.yaml
└── static
├── image.jpeg
├── some.txt
└── test.html
插件启动时会加载 `extensions` 目录的 `yaml` 保存到数据库,`apple.yaml` 为本项目自定义的模型的数据示例,当启用插件后调用
```text
GET /apis/apple.guqing.xyz/v1alpha1/apples
```
便可查询到 `apple.yaml` 中定义的记录
```json
[
{ {
"spec": { "spec": {
"varieties": "Fuji", "varieties": "Fuji",
@@ -204,84 +219,174 @@ GET /apis/apple.guqing.xyz/v1alpha1/apples
"creationTimestamp": "2022-06-24T04:03:22.890741Z" "creationTimestamp": "2022-06-24T04:03:22.890741Z"
} }
} }
] ]
``` ```
`reverseProxy.yaml` 为 Halo 中提供的模型,表示反向代理,允许插件配置规则代理到插件目录 `reverseProxy.yaml` 为 Halo 中提供的模型,表示反向代理,允许插件配置规则代理到插件目录
```yaml ```yaml
apiVersion: plugin.halo.run/v1alpha1 apiVersion: plugin.halo.run/v1alpha1
kind: ReverseProxy kind: ReverseProxy
metadata: metadata:
name: reverse-proxy-template name: reverse-proxy-template
rules: rules:
- path: /static/** - path: /static/**
file: file:
directory: static directory: static
``` ```
它表示访问 `GET /assets/{plugin-name}/static/**` 时代理访问到插件的 `resources/static` 目录,本插件便可访问到一下静态资源 它表示访问 `GET /assets/{plugin-name}/static/**` 时代理访问到插件的 `resources/static` 目录,本插件便可访问到一下静态资源
``` ```
GET /assets/apples/static/image.jpeg GET /assets/apples/static/image.jpeg
GET /assets/apples/static/some.txt GET /assets/apples/static/some.txt
GET /assets/apples/static/test.html GET /assets/apples/static/test.html
``` ```
`roleTemplate.yaml` 文件中 `kind: Role` 表示插件允许提供角色模版,定义格式如下: `roleTemplate.yaml` 文件中 `kind: Role` 表示插件允许提供角色模版,定义格式如下:
```yaml ```yaml
apiVersion: v1alpha1 apiVersion: v1alpha1
kind: Role kind: Role
metadata: metadata:
name: a name here name: a name here
labels: labels:
plugin.halo.run/role-template: "true" plugin.halo.run/role-template: "true"
annotations: annotations:
plugin.halo.run/module: "module name" plugin.halo.run/module: "module name"
plugin.halo.run/alias-name: "display name" plugin.halo.run/alias-name: "display name"
rules: rules:
# ... # ...
``` ```
必须带有`plugin.halo.run/role-template: "true"` labels表示该角色为角色模版当用户启用插件后创建角色或修改角色时会将其列在权限列表位置。 必须带有`plugin.halo.run/role-template: "true"` labels表示该角色为角色模版当用户启用插件后创建角色或修改角色时会将其列在权限列表位置。
插件如果不提供角色模版除非是超级管理员否则其他账号没有权限访问,因为 Halo 规定 `/api``/apis` 开头的 `api` 都需要授权才能访问,因此插件不提供角色模版的自定义资源,就无法将其分配给用户。 插件如果不提供角色模版除非是超级管理员否则其他账号没有权限访问,因为 Halo 规定 `/api` 和 `/apis` 开头的 `api` 都需要授权才能访问,因此插件不提供角色模版的自定义资源,就无法将其分配给用户。
更多详情参考:[Halo security RFC](https://github.com/halo-dev/rfcs/blob/main/identity/002-security.md) 更多详情参考:[Halo security RFC](https://github.com/halo-dev/rfcs/blob/main/identity/002-security.md)
### 打包 ### 开发环境
#### 打包前端项目 #### 环境要求
1.`admin-frontend` 目录下执行 - openJDK 17
- NodeJS 16+
- pnpm 7+
#### 拉取 Halo 相关项目源码
```bash
mkdir ./halo-dev
mkdir ./halo-dev/dev-plugins # 存放插件源码
```
```bash
cd ./halo-dev
git clone https://github.com/halo-dev/halo --branch next
```
```bash
git clone https://github.com/halo-dev/halo-admin --branch next
```
```bash
cd ./dev-plugins
git clone https://github.com/halo-sigs/plugin-template
```
#### Halo 配置文件修改
修改 halo/src/resources/application-dev.yaml
```yaml
halo:
security:
initializer:
super-admin-username: admin
super-admin-password: P@88w0rd
oauth2:
jwt:
jwsAlgorithm: rs512
public-key-location: classpath:app.pub
private-key-location: classpath:app.key
plugin:
runtime-mode: development # development, deployment
classes-directories:
- "build/classes"
- "build/resources"
lib-directories:
- "libs"
plugins-root: /Users/ryanwang/Workspace/github/ruibaby/halo-dev/dev-plugins # 修改为上方存放插件源码的实际目录
initial-extension-locations: # 初始化资源加载配置,需要配置当前开发中插件的资源目录或者文件
- "/Users/ryanwang/Workspace/github/ruibaby/halo-dev/dev-plugins/plugin-template/src/main/resources/plugin.yaml"
```
#### 编译插件
下载前端依赖:
```bash
cd ./halo-dev/dev-plugins/plugin-template
./gradlew.bat pnpmInstall
# or macOS/Linux
./gradlew pnpmInstall
```
构建:
```bash
./gradlew.bat build
# or macOS/Linux
./gradlew build
```
#### 启动 Halo
```bash
cd ./halo-dev/halo
./graldew.bat bootRun --args="--spring.profiles.active=dev"
# or macOS/Linux
./gradlew bootRun --args="--spring.profiles.active=dev"
```
或者在 IntelliJ IDEA 中运行 Application 启动类。但注意需要配置好 `spring.profiles.active` 为 dev。
#### 启动 Halo Admin
```bash
cd ./halo-dev/halo-admin
```shell
pnpm install pnpm install
pnpm dev
``` ```
2. 开发时执行 #### 访问后台
```shell 在浏览器中访问 https://localhost:3000 即可,登录用户名和密码为上方 `application-dev.yaml` 配置中的 `super-admin-username`
pnpm run dev 和 `super-admin-password`。
```
3. 打包时执行 然后在左侧菜单中选择 `插件`,即可查看所有插件的状态。
```shell #### 开发
pnpm run build
```
#### 构建后端 修改前端代码或者后端代码,然后运行 `./gradlew.bat build` 或者 `./gradlew build`macOS/Linux即可构建插件无需重启 Halo。但修改配置文件后需要 build 插件以及重启 Halo。
1. 开发时可以使用 `command + F9 / ctrl + F9` ### 构建生产产物
2. 生产时:点击 `gradle``build`
或者执行
``` ```
./gradlew -x build ./gradlew -x build
``` ```
然后只需复制例如`build/libs/halo-plugin-template-0.0.1-SNAPSHOT-plain.jar``jar` 包即可使用 然后只需复制例如`build/libs/plugin-template-0.0.1-SNAPSHOT-plain.jar` 的 `jar` 包即可使用