Halo 2.0 如何开发一个插件 [WIP]
模板仓库:https://github.com/halo-dev/plugin-starter
前期准备:
- 使用此仓库作为模版创建仓库
- 克隆新创建好的仓库到本地
使用模板仓库开发友情链接插件示例:https://github.com/halo-sigs/plugin-links
项目结构介绍
.
├── LICENSE
├── README.md
├── admin-frontend
│ ├── ...
├── build
│ ├── classes
│ ├── libs
│ │ ├── halo-plugin-template-0.0.1-SNAPSHOT-plain.jar
│ ├── resources
├── build.gradle
├── lib
│ └── halo-2.0.0-SNAPSHOT-plain.jar
├── settings.gradle
└── src
└── main
├── java
│ └── io
│ └── github
│ └── guqing
│ └── template
│ ├── Apple.java
│ ├── ApplePlugin.java
│ └── ApplesController.java
└── resources
├── admin
│ ├── main.js
│ └── style.css
├── extensions
│ ├── apple.yaml
│ ├── reverseProxy.yaml
│ └── roleTemplate.yaml
├── plugin.yaml
└── static
├── ...
对于以上目录树:
-
admin-frontend
: 插件前端项目目录,为一个 vue 项目; -
build
:插件后端构建目录,build/libs
下的 jar 包为最终插件产物; -
lib
:为临时的 halo 依赖,为了使用 halo 中提供的类在build.gradle
中作为编译时依赖引入compileOnly files("lib/halo-2.0.0-SNAPSHOT-plain.jar")
,待2.0
正式发布会将其发布到maven
中央仓库,便可通过gradle
依赖; -
src
: 为插件后端源码目录; -
Apple.java
为自定义模型
@GVK(group = "apple.guqing.xyz", kind = "Apple",
version = "v1alpha1", singular = "apple", plural = "apples")
@Data
@EqualsAndHashCode(callSuper = true)
public class Apple extends AbstractExtension {}
关键在于标注 @GVK
注解和 extends AbstractExtension
,当如此定义了一个模型后,插件启动时会根据 @GVK
配置自动生成CRUD
的 RESTful API
,以此为例子会生成如下APIs
GET /apis/apple.guqing.xyz/v1alpha1/apples
POST /apis/apple.guqing.xyz/v1alpha1/apples
GET /apis/apple.guqing.xyz/v1alpha1/apples/{name}
PUT /apis/apple.guqing.xyz/v1alpha1/apples/{name}
DELETE /apis/apple.guqing.xyz/v1alpha1/apples/{name}
生成规则见:Halo extension RFC
ApplePlugin.java
:插件生命周期入口,它继承BasePlugin
,可以通过getApplicationContext()
方法获取到SchemeManager
,然后在start()
方法中注册自定义模型,这一步必不可少,所有定义的自定义模型都需要在此注册,并在stop()
生命周期方法中清理资源。
@Override
public void start() {
schemeManager.register(Apple.class);
}
@Override
public void stop() {
// TODO 优化取消注册自定义模型的写法
Scheme scheme = schemeManager.get(Apple.class);
schemeManager.unregister(scheme);
}
注意:该类不能标注 @Component
等能将其声明为 Spring Bean
的注解
ApplesController.java
:如果根据模型自动生成的CURD RESTful APIs
无法满足业务需要,可以写常规Controller
来自定义APIs
,示例:
@ApiVersion("v1alpha1")
@RestController
@RequestMapping("colors")
public class ApplesController {
@GetMapping
public Mono<String> hello() {
return Mono.just("Hello world");
}
}
插件定义 Controller
必须要标注 @ApiVersion
注解,否则启动时会报错。如果定义了这样的 Controller
,插件启动后会生成如下的 APIs
GET /api/v1alpha1/plugins/apples/colors
生成规则为 /api/{version}/plugins/{plugin-name}/{mapping-in-class}/{mapping-in-method}
其中:
-
version
:来自@ApiVersion("v1alpha1")
的 value,详情参考:Halo plugin API composition -
plugin-name
:值来自plugin.yaml
中的metadata.name
属性 -
mapping-in-class
:来自标注在类上的@RequestMapping("colors")
-
mapping-in-method
:来自标注在方法上的@GetMapping
插件还允许使用 @Service
、@Component
注解将其声明为一个 Spring Bean
,便可通过依赖注入使用 Spring Bean
,示例:
@Service
public class ColorService {
public String getColor(String appleName) {
return "red";
}
}
@ApiVersion("v1alpha1")
@RestController
@RequestMapping("colors")
public class ApplesController {
private final ColorService colorService;
// 构造器注入,当然也同样允许 @Autowired 注入 和 setter 方法注入
public ApplesController(ColorService colorService) {
this.colorService = colorService;
}
}
resources
:目录为插件资源目录
admin
目录下为插件前端打包后的产物存放目录,固定为 main.js
和 style.css
两个文件
extensions
存放自定义模型资源配置
plugin.yaml
为插件描述配置
static
为静态资源示例目录
├── 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
为本项目自定义的模型的数据示例,当启用插件后调用
GET /apis/apple.guqing.xyz/v1alpha1/apples
便可查询到 apple.yaml
中定义的记录
[
{
"spec": {
"varieties": "Fuji",
"color": "red",
"size": "middle",
"producingArea": "China"
},
"apiVersion": "apple.guqing.xyz/v1alpha1",
"kind": "Apple",
"metadata": {
"name": "Fuji-apple",
"labels": {
"plugin.halo.run/plugin-name": "apples"
},
"version": 0,
"creationTimestamp": "2022-06-24T04:03:22.890741Z"
}
}
]
reverseProxy.yaml
为 Halo 中提供的模型,表示反向代理,允许插件配置规则代理到插件目录
apiVersion: plugin.halo.run/v1alpha1
kind: ReverseProxy
metadata:
name: reverse-proxy-template
rules:
- path: /static/**
file:
directory: static
它表示访问 GET /assets/{plugin-name}/static/**
时代理访问到插件的 resources/static
目录,本插件便可访问到一下静态资源
GET /assets/apples/static/image.jpeg
GET /assets/apples/static/some.txt
GET /assets/apples/static/test.html
roleTemplate.yaml
文件中 kind: Role
表示插件允许提供角色模版,定义格式如下:
apiVersion: v1alpha1
kind: Role
metadata:
name: a name here
labels:
plugin.halo.run/role-template: "true"
annotations:
plugin.halo.run/module: "module name"
plugin.halo.run/alias-name: "display name"
rules:
# ...
必须带有plugin.halo.run/role-template: "true"
labels,表示该角色为角色模版,当用户启用插件后,创建角色或修改角色时会将其列在权限列表位置。
插件如果不提供角色模版除非是超级管理员否则其他账号没有权限访问,因为 Halo 规定 /api
和 /apis
开头的 api
都需要授权才能访问,因此插件不提供角色模版的自定义资源,就无法将其分配给用户。
更多详情参考:Halo security RFC
打包
打包前端项目
- 到
admin-frontend
目录下执行
pnpm install
- 开发时执行
pnpm run dev
- 打包时执行
pnpm run build
构建后端
-
开发时可以使用
command + F9/ctrl + F9
-
生产时:点击
gradle
的build
或者执行
./gradlew -x build
然后只需复制例如build/libs/halo-plugin-template-0.0.1-SNAPSHOT-plain.jar
的 jar
包即可使用