chore: refine plugin development documentation (#503)

* chore: refine plugin development documentation

Signed-off-by: Ryan Wang <i@ryanc.cc>

* chore: refine plugin development documentation

Signed-off-by: Ryan Wang <i@ryanc.cc>

* chore: refine plugin development documentation

Signed-off-by: Ryan Wang <i@ryanc.cc>

---------

Signed-off-by: Ryan Wang <i@ryanc.cc>
This commit is contained in:
Ryan Wang
2025-06-21 20:47:16 +08:00
committed by GitHub
parent 9df9374ce3
commit 7e097c93f9
12 changed files with 1028 additions and 269 deletions

9
.editorconfig Normal file
View File

@@ -0,0 +1,9 @@
root = true
[*.md]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = true

View File

@@ -0,0 +1,414 @@
---
title: 构建
description: UI 部分的构建说明
---
在 [halo-dev/plugin-starter](https://github.com/halo-dev/plugin-starter) 模板中,我们已经配置好了 UI 的构建工具和流程,此文档主要说明一些构建细节以及其他可能的构建选项。
## 原理
Halo 插件的 UI 部分Console / UC的实现方式其实很简单本质上就是构建一个结构固定的大对象交给 Halo 去解析,其中包括全局注册的组件、路由定义、扩展点等。在 [halo-dev/plugin-starter](https://github.com/halo-dev/plugin-starter) 模板中,我们使用 `index.ts` 作为入口文件,并在构建之后将 `main.js``style.css` 放到插件项目的 `src/main/resources/console` 目录中,后续 Halo 在内部会自动合并所有插件的 `main.js``style.css` 文件,并生成最终的 `bundle.js``bundle.css` 文件,然后在 Console 和 UC 中加载这两个资源并解析。
所以本质上,我们只需要使用支持将 `index.ts` 编译为 `main.js``style.css` 的工具,然后输出到插件项目的 `src/main/resources/console` 目录中即可,在 [halo-dev/plugin-starter](https://github.com/halo-dev/plugin-starter) 模板中可以看到,我们提供了一个名为 `@halo-dev/ui-plugin-bundler-kit` 的库,这个库包含了 [Vite](https://vite.dev/) 和 [Rsbuild](https://rsbuild.dev/) 的预配置,插件项目只需要通过简单的配置即可使用。
## @halo-dev/ui-plugin-bundler-kit
在这个库中,我们提供了三个预配置,分别是:
1. `viteConfig`: Vite 的预配置,[halo-dev/plugin-starter](https://github.com/halo-dev/plugin-starter) 中默认使用的配置
2. `rsbuildConfig`: Rsbuild 的预配置
3. `HaloUIPluginBundlerKit`:已过时,迁移方式可以参考下面的文档
### viteConfig
#### 使用
如果你想要使用 Vite 构建 UI 部分,那么使用 `viteConfig` 即可,并且已经在 [halo-dev/plugin-starter](https://github.com/halo-dev/plugin-starter) 中配置,直接使用即可。
#### 配置
```js
import { viteConfig } from "@halo-dev/ui-plugin-bundler-kit";
export default viteConfig({
vite: {
// 自定义 Vite 配置
plugins: [
// 额外的插件Vue 插件已预配置)
],
// 其他配置...
},
});
```
示例:
1. 添加路径别名
```js
import { viteConfig } from "@halo-dev/ui-plugin-bundler-kit";
import path from "path";
export default viteConfig({
vite: {
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
"@components": path.resolve(__dirname, "src/components"),
},
},
},
});
```
2. 添加额外的 Vite 插件
```js
import { viteConfig } from "@halo-dev/ui-plugin-bundler-kit";
import { defineConfig } from "vite";
import UnoCSS from "unocss/vite";
export default viteConfig({
vite: {
plugins: [
UnoCSS(), // 添加 UnoCSS 插件
],
},
});
```
### rsbuildConfig
Rsbuild 是基于 Rspack 开发的上层构建工具,其优势在于兼容 Webpack 生态并且性能优异。我们为什么要选择 Rsbuild 可以查阅 [Vite vs Rsbuild](#vite-vs-rsbuild)。
#### 使用
因为在 [halo-dev/plugin-starter](https://github.com/halo-dev/plugin-starter) 中,默认采用 Vite 构建,所以如果想要使用 Rsbuild 构建,需要手动配置,以下是切换到 Rsbuild 的过程:
安装依赖:
```bash
pnpm install @halo-dev/ui-plugin-bundler-kit@2.21.1 @rsbuild/core -D
```
创建 rsbuild.config.mjs:
```js
import { rsbuildConfig } from "@halo-dev/ui-plugin-bundler-kit";
export default rsbuildConfig()
```
更新 package.json:
```json
{
"type": "module",
"scripts": {
"dev": "rsbuild build --env-mode development --watch",
"build": "rsbuild build"
}
}
```
#### 配置
```js
import { rsbuildConfig } from "@halo-dev/ui-plugin-bundler-kit";
export default rsbuildConfig({
rsbuild: {
// 自定义 Rsbuild 配置
plugins: [
// 额外的插件Vue 插件已预配置)
],
// 其他配置...
},
});
```
示例:
1. 添加路径别名
```js
import { rsbuildConfig } from "@halo-dev/ui-plugin-bundler-kit";
export default rsbuildConfig({
rsbuild: {
source: {
alias: {
"@": "./src",
"@components": "./src/components",
},
},
},
});
```
2. 添加额外的 Rsbuild 插件
```js
import { rsbuildConfig } from "@halo-dev/ui-plugin-bundler-kit";
import { pluginSass } from "@rsbuild/plugin-sass";
export default rsbuildConfig({
rsbuild: {
plugins: [
pluginSass(), // 添加 Sass 插件
],
},
});
```
### HaloUIPluginBundlerKit
旧版本 [plugin-starter](https://github.com/halo-dev/plugin-starter) 使用的方式,目前已经不再推荐。
## 构建输出
在 `viteConfig` 和 `rsbuildConfig` 中,已经配置好了开发环境和生产构建的输出目录,分别是:
- **开发环境**`build/resources/main/console`,在开发 UI 的过程中,可以使用 `pnpm dev` 来实时查看效果
- **生产环境**`ui/build/dist`
> 需要注意的是,生产构建的目录仅仅是临时目录,最终在使用 Gradle 构建插件时会自动构建 UI 并复制到 `src/main/resources/console` 目录中。
## Vite vs Rsbuild{#vite-vs-rsbuild}
Vite 和 Rsbuild 都是优秀的构建工具,但它们在不同的使用场景下有各自的优势:
### 何时使用 Rsbuild
- ✅ **代码分割支持** - Rsbuild 为代码分割和懒加载提供了优秀的支持
- ✅ **更好的性能** - 对于复杂应用,通常有更快的构建时间和更小的包体积
- ✅ **动态导入** - 非常适合有重度前端组件的插件
**动态导入示例:**
```typescript
import { definePlugin } from '@halo-dev/console-shared';
import { defineAsyncComponent } from 'vue';
import { VLoading } from '@halo-dev/components';
export default definePlugin({
routes: [
{
parentName: 'Root',
route: {
path: 'demo',
name: 'DemoPage',
// 懒加载重型组件
component: defineAsyncComponent({
loader: () => import('./views/DemoPage.vue'),
loadingComponent: VLoading,
}),
},
},
],
extensionPoints: {},
});
```
### 何时使用 Vite
- ✅ **Vue 生态友好** - 与 Vue 生态系统工具和插件有更好的集成
- ✅ **丰富的插件生态** - 有大量可用的 Vite 插件
- ✅ **简单配置** - 对于直接的使用场景更容易配置
### 总结
| 特性 | Vite | Rsbuild |
| ---------- | ------ | -------- |
| 代码分割 | ❌ 有限 | ✅ 优秀 |
| Vue 生态 | ✅ 优秀 | ✅ 良好 |
| 构建性能 | ✅ 良好 | ✅ 优秀 |
| 开发体验 | ✅ 优秀 | ✅ 优秀 |
| 插件生态 | ✅ 丰富 | ✅ 增长中 |
| 配置复杂度 | ✅ 简单 | ⚖️ 中等 |
**建议**:对于有大型前端代码库的复杂插件使用 **Rsbuild**,对于简单插件或需要广泛 Vue 生态系统集成时使用 **Vite**。
## 迁移{#migration}
如果你当前的插件使用的是旧版本的 [plugin-starter](https://github.com/halo-dev/plugin-starter),并且想使用新的 `viteConfig` 和 `rsbuildConfig`,可以参考以下步骤:
1. 更新 `@halo-dev/ui-plugin-bundler-kit` 至 `2.21.1` 或更高版本
```bash
pnpm install @halo-dev/ui-plugin-bundler-kit@2.21.1 -D
```
2. 更新 `vite.config.ts` 文件
```typescript
import { viteConfig } from "@halo-dev/ui-plugin-bundler-kit";
export default viteConfig({
vite: {
// Vite 配置需要按照原有的配置进行修改,但需要移除 Vue 插件,因为已经内置
plugins: [
],
},
});
```
3. 更新项目根目录的 `build.gradle` 文件
```diff
plugins {
id 'java'
- id "com.github.node-gradle.node" version "7.0.2"
- id "io.freefair.lombok" version "8.0.1"
- id "run.halo.plugin.devtools" version "0.2.0"
+ id "io.freefair.lombok" version "8.13"
+ id "run.halo.plugin.devtools" version "0.6.0"
}
group 'run.halo.starter'
-sourceCompatibility = JavaVersion.VERSION_17
repositories {
mavenCentral()
- maven { url 'https://s01.oss.sonatype.org/content/repositories/releases' }
- maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots/' }
- maven { url 'https://repo.spring.io/milestone' }
}
dependencies {
- implementation platform('run.halo.tools.platform:plugin:2.20.0-SNAPSHOT')
+ implementation platform('run.halo.tools.platform:plugin:2.21.0')
compileOnly 'run.halo.app:api'
testImplementation 'run.halo.app:api'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
+ testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
test {
useJUnitPlatform()
}
-tasks.withType(JavaCompile).configureEach {
- options.encoding = "UTF-8"
-}
-
-node {
- nodeProjectDir = file("${project.projectDir}/ui")
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(21)
+ }
}
-tasks.register('buildFrontend', PnpmTask) {
- args = ['build']
- dependsOn('installDepsForUI')
+tasks.withType(JavaCompile).configureEach {
+ options.encoding = "UTF-8"
+ options.release = 21
}
-tasks.register('installDepsForUI', PnpmTask) {
- args = ['install']
+tasks.register('processUiResources', Copy) {
+ from project(':ui').tasks.named('buildFrontend')
+ into layout.buildDirectory.dir('resources/main/console')
}
-build {
- // build frontend before build
- tasks.named('compileJava').configure {
- dependsOn('buildFrontend')
- }
+tasks.named('processResources', ProcessResources) {
+ dependsOn tasks.named('processUiResources')
}
```
4. 在 ui 或者 console 目录新建 `build.gradle` 文件,内容如下:
```gradle
plugins {
id 'base'
id "com.github.node-gradle.node" version "7.1.0"
}
group 'run.halo.starter.ui'
tasks.register('buildFrontend', PnpmTask) {
args = ['build']
dependsOn tasks.named('pnpmInstall')
inputs.dir(layout.projectDirectory.dir('src'))
inputs.files(fileTree(
dir: layout.projectDirectory,
includes: ['*.cjs', '*.ts', '*.js', '*.json', '*.yaml']))
outputs.dir(layout.buildDirectory.dir('dist'))
shouldRunAfter(tasks.named('check'))
}
tasks.register('checkFrontend', PnpmTask) {
args = ['test:unit']
dependsOn tasks.named('pnpmInstall')
}
tasks.named('check') {
dependsOn tasks.named('checkFrontend')
}
tasks.named('build') {
dependsOn tasks.named('buildFrontend')
}
```
进行此变更的主要目的是保证 UI 构建的产物不直接输出到源码目录的 resources 目录中,而是通过 Gradle 构建插件时复制到 `src/main/resources/console` 目录中。
完整变更过程可参考:[halo-dev/plugin-starter#52](https://github.com/halo-dev/plugin-starter/pull/52)
如果你不想使用新的 Gradle 构建配置,也可以修改 viteConfig 或 rsbuildConfig 的输出目录,和旧版本保持一致:
viteConfig:
```js
import { viteConfig } from "@halo-dev/ui-plugin-bundler-kit";
const OUT_DIR_PROD = "../src/main/resources/console";
const OUT_DIR_DEV = "../build/resources/main/console";
export default viteConfig({
vite: ({ mode }) => {
const isProduction = mode === "production";
const outDir = isProduction ? OUT_DIR_PROD : OUT_DIR_DEV;
return {
build: {
outDir,
},
};
},
});
```
rsbuildConfig:
```js
import { rsbuildConfig } from "@halo-dev/ui-plugin-bundler-kit";
const OUT_DIR_PROD = "../src/main/resources/console";
const OUT_DIR_DEV = "../build/resources/main/console";
export default rsbuildConfig({
rsbuild: ({ envMode }) => {
const isProduction = envMode === "production";
const outDir = isProduction ? OUT_DIR_PROD : OUT_DIR_DEV;
return {
output: {
distPath: {
root: outDir,
},
},
};
},
});
```

View File

@@ -29,28 +29,37 @@ export function definePlugin(plugin: PluginModule): PluginModule {
```
```ts title="PluginModule"
import type { Component, Ref } from "vue";
import type { RouteRecordRaw, RouteRecordName } from "vue-router";
import type { FunctionalPage } from "../states/pages";
import type { AttachmentSelectProvider } from "../states/attachment-selector";
import type { EditorProvider, PluginTab } from "..";
import type { AnyExtension } from "@halo-dev/richtext-editor";
import type { CommentSubjectRefProvider } from "@/states/comment-subject-ref";
import type { BackupTab } from "@/states/backup";
import type { PluginInstallationTab } from "@/states/plugin-installation-tabs";
import type { CommentSubjectRefProvider } from "@/states/comment-subject-ref";
import type { EntityFieldItem } from "@/states/entity";
import type { OperationItem } from "@/states/operation";
import type { PluginInstallationTab } from "@/states/plugin-installation-tabs";
import type { ThemeListTab } from "@/states/theme-list-tabs";
import type { UserProfileTab, UserTab } from "@/states/user-tab";
import type {
Attachment,
Backup,
ListedPost,
Plugin,
Theme,
ListedComment,
ListedReply,
ListedSinglePage,
} from "@halo-dev/api-client";
import type { AnyExtension } from "@halo-dev/richtext-editor";
import type { Component, Ref } from "vue";
import type { RouteRecordName, RouteRecordRaw } from "vue-router";
import type {
DashboardWidgetDefinition,
DashboardWidgetQuickActionItem,
EditorProvider,
PluginTab,
} from "..";
import type { AttachmentSelectProvider } from "../states/attachment-selector";
import type { FunctionalPage } from "../states/pages";
export interface RouteRecordAppend {
parentName: RouteRecordName;
parentName: NonNullable<RouteRecordName>;
route: RouteRecordRaw;
}
@@ -80,36 +89,65 @@ export interface ExtensionPoint {
"post:list-item:operation:create"?: (
post: Ref<ListedPost>
) => OperationItem<ListedPost>[] | Promise<OperationItem<ListedPost>[]>;
) => OperationItem<ListedPost>[];
"single-page:list-item:operation:create"?: (
singlePage: Ref<ListedSinglePage>
) => OperationItem<ListedSinglePage>[];
"comment:list-item:operation:create"?: (
comment: Ref<ListedComment>
) => OperationItem<ListedComment>[];
"reply:list-item:operation:create"?: (
reply: Ref<ListedReply>
) => OperationItem<ListedReply>[];
"plugin:list-item:operation:create"?: (
plugin: Ref<Plugin>
) => OperationItem<Plugin>[] | Promise<OperationItem<Plugin>[]>;
) => OperationItem<Plugin>[];
"backup:list-item:operation:create"?: (
backup: Ref<Backup>
) => OperationItem<Backup>[] | Promise<OperationItem<Backup>[]>;
) => OperationItem<Backup>[];
"attachment:list-item:operation:create"?: (
attachment: Ref<Attachment>
) => OperationItem<Attachment>[] | Promise<OperationItem<Attachment>[]>;
) => OperationItem<Attachment>[];
"plugin:list-item:field:create"?: (
plugin: Ref<Plugin>
) => EntityFieldItem[] | Promise<EntityFieldItem[]>;
"plugin:list-item:field:create"?: (plugin: Ref<Plugin>) => EntityFieldItem[];
"post:list-item:field:create"?: (
post: Ref<ListedPost>
) => EntityFieldItem[] | Promise<EntityFieldItem[]>;
"post:list-item:field:create"?: (post: Ref<ListedPost>) => EntityFieldItem[];
"single-page:list-item:field:create"?: (
singlePage: Ref<ListedSinglePage>
) => EntityFieldItem[];
"theme:list:tabs:create"?: () => ThemeListTab[] | Promise<ThemeListTab[]>;
"theme:list-item:operation:create"?: (
theme: Ref<Theme>
) => OperationItem<Theme>[] | Promise<OperationItem<Theme>[]>;
) => OperationItem<Theme>[];
"user:detail:tabs:create"?: () => UserTab[] | Promise<UserTab[]>;
"uc:user:profile:tabs:create"?: () =>
| UserProfileTab[]
| Promise<UserProfileTab[]>;
"console:dashboard:widgets:create"?: () =>
| DashboardWidgetDefinition[]
| Promise<DashboardWidgetDefinition[]>;
"console:dashboard:widgets:internal:quick-action:item:create"?: () =>
| DashboardWidgetQuickActionItem[]
| Promise<DashboardWidgetQuickActionItem[]>;
}
export interface PluginModule {
/**
* These components will be registered when plugin is activated.
*/
components?: Record<string, Component>;
routes?: RouteRecordRaw[] | RouteRecordAppend[];
@@ -118,6 +156,7 @@ export interface PluginModule {
extensionPoints?: ExtensionPoint;
}
```
- `components`组件列表key 为组件名称value 为组件对象,在此定义之后,加载插件时会自动注册到 Vue App 全局。

View File

@@ -7,6 +7,6 @@ Halo 插件体系的 UI 部分可以让开发者在 Console 控制台和 UC 个
在开始之前,建议先熟悉或安装以下库和工具:
1. [Node.js 18+](https://nodejs.org)
2. [pnpm 8+](https://pnpm.io)
1. [Node.js 20+](https://nodejs.org)
2. [pnpm 10+](https://pnpm.io)
3. [Vue.js 3](https://vuejs.org)

View File

@@ -11,126 +11,51 @@ description: 了解如何与我们的社区分享你的插件
## 自动构建
如果你是基于 [halo-dev/plugin-starter](https://github.com/halo-dev/plugin-starter) 创建的插件项目,那么已经包含了适用于 GitHub Action 的 `workflow.yaml` 文件,里面包含了构建插件和发布插件资源到 Release 的步骤,可以根据自己的实际需要进行修改,以下是示例:
如果你是基于 [halo-dev/plugin-starter](https://github.com/halo-dev/plugin-starter) 创建的插件项目,那么已经包含了适用于 GitHub Action 的 `ci.yaml``cd.yaml` 文件,里面包含了构建插件和发布插件资源到 Release 的步骤,可以根据自己的实际需要进行修改,以下是示例:
```yaml
name: Build Plugin JAR File
name: CI
on:
push:
branches:
- main
paths:
- "**"
- "!**.md"
release:
types:
- created
pull_request:
branches:
- main
paths:
- "**"
- "!**.md"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Set up JDK 17
uses: actions/setup-java@v2
with:
distribution: 'temurin'
cache: 'gradle'
java-version: 17
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 18
- uses: pnpm/action-setup@v2.0.1
name: Install pnpm
id: pnpm-install
with:
version: 8
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/widget/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install Frontend Dependencies
run: |
./gradlew pnpmInstall
- name: Build with Gradle
run: |
# Set the version with tag name when releasing
version=${{ github.event.release.tag_name }}
version=${version#v}
sed -i "s/version=.*-SNAPSHOT$/version=$version/1" gradle.properties
./gradlew clean build -x test
- name: Archive plugin-starter jar
uses: actions/upload-artifact@v2
with:
name: plugin-starter
path: |
build/libs/*.jar
retention-days: 1
github-release:
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'release'
steps:
- name: Download plugin-starter jar
uses: actions/download-artifact@v2
with:
name: plugin-starter
path: build/libs
- name: Get Name of Artifact
id: get_artifact
run: |
ARTIFACT_PATHNAME=$(ls build/libs/*.jar | head -n 1)
ARTIFACT_NAME=$(basename ${ARTIFACT_PATHNAME})
echo "Artifact pathname: ${ARTIFACT_PATHNAME}"
echo "Artifact name: ${ARTIFACT_NAME}"
echo "ARTIFACT_PATHNAME=${ARTIFACT_PATHNAME}" >> $GITHUB_ENV
echo "ARTIFACT_NAME=${ARTIFACT_NAME}" >> $GITHUB_ENV
echo "RELEASE_ID=${{ github.event.release.id }}" >> $GITHUB_ENV
- name: Upload a Release Asset
uses: actions/github-script@v2
if: github.event_name == 'release'
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
console.log('environment', process.versions);
const fs = require('fs').promises;
const { repo: { owner, repo }, sha } = context;
console.log({ owner, repo, sha });
const releaseId = process.env.RELEASE_ID
const artifactPathName = process.env.ARTIFACT_PATHNAME
const artifactName = process.env.ARTIFACT_NAME
console.log('Releasing', releaseId, artifactPathName, artifactName)
await github.repos.uploadReleaseAsset({
owner, repo,
release_id: releaseId,
name: artifactName,
data: await fs.readFile(artifactPathName)
});
ci:
uses: halo-sigs/reusable-workflows/.github/workflows/plugin-ci.yaml@v3
with:
ui-path: "ui"
pnpm-version: 9
node-version: 22
java-version: 21
```
```yaml
name: CD
on:
release:
types:
- published
jobs:
cd:
uses: halo-sigs/reusable-workflows/.github/workflows/plugin-cd.yaml@v3
permissions:
contents: write
with:
pnpm-version: 9
node-version: 22
java-version: 21
skip-appstore-release: true
```
关于 CI / CD 的更多详细信息,可查阅:[halo-sigs/reusable-workflows](https://github.com/halo-sigs/reusable-workflows)
## 发布你的插件
用户可以在你的仓库 Release 下载使用,但为了方便让 Halo 的用户知道你的插件,可以在以下渠道发布:

View File

@@ -202,15 +202,13 @@ const config = {
prism: {
theme: darkCodeTheme,
darkTheme: darkCodeTheme,
additionalLanguages: ["java", "json", "sql"],
additionalLanguages: ["java", "json", "sql", "diff"],
},
zoom: {
selector: ".markdown :not(a) > img",
},
}),
plugins: [
require.resolve("docusaurus-plugin-image-zoom"),
],
plugins: [require.resolve("docusaurus-plugin-image-zoom")],
scripts: [
{
src: "https://track.halo.run/api/script.js",

View File

@@ -175,6 +175,7 @@ module.exports = {
items: [
"developer-guide/plugin/basics/ui/intro",
"developer-guide/plugin/basics/ui/entry",
"developer-guide/plugin/basics/ui/build",
],
},
],

View File

@@ -0,0 +1,414 @@
---
title: 构建
description: UI 部分的构建说明
---
在 [halo-dev/plugin-starter](https://github.com/halo-dev/plugin-starter) 模板中,我们已经配置好了 UI 的构建工具和流程,此文档主要说明一些构建细节以及其他可能的构建选项。
## 原理
Halo 插件的 UI 部分Console / UC的实现方式其实很简单本质上就是构建一个结构固定的大对象交给 Halo 去解析,其中包括全局注册的组件、路由定义、扩展点等。在 [halo-dev/plugin-starter](https://github.com/halo-dev/plugin-starter) 模板中,我们使用 `index.ts` 作为入口文件,并在构建之后将 `main.js``style.css` 放到插件项目的 `src/main/resources/console` 目录中,后续 Halo 在内部会自动合并所有插件的 `main.js``style.css` 文件,并生成最终的 `bundle.js``bundle.css` 文件,然后在 Console 和 UC 中加载这两个资源并解析。
所以本质上,我们只需要使用支持将 `index.ts` 编译为 `main.js``style.css` 的工具,然后输出到插件项目的 `src/main/resources/console` 目录中即可,在 [halo-dev/plugin-starter](https://github.com/halo-dev/plugin-starter) 模板中可以看到,我们提供了一个名为 `@halo-dev/ui-plugin-bundler-kit` 的库,这个库包含了 [Vite](https://vite.dev/) 和 [Rsbuild](https://rsbuild.dev/) 的预配置,插件项目只需要通过简单的配置即可使用。
## @halo-dev/ui-plugin-bundler-kit
在这个库中,我们提供了三个预配置,分别是:
1. `viteConfig`: Vite 的预配置,[halo-dev/plugin-starter](https://github.com/halo-dev/plugin-starter) 中默认使用的配置
2. `rsbuildConfig`: Rsbuild 的预配置
3. `HaloUIPluginBundlerKit`:已过时,迁移方式可以参考下面的文档
### viteConfig
#### 使用
如果你想要使用 Vite 构建 UI 部分,那么使用 `viteConfig` 即可,并且已经在 [halo-dev/plugin-starter](https://github.com/halo-dev/plugin-starter) 中配置,直接使用即可。
#### 配置
```js
import { viteConfig } from "@halo-dev/ui-plugin-bundler-kit";
export default viteConfig({
vite: {
// 自定义 Vite 配置
plugins: [
// 额外的插件Vue 插件已预配置)
],
// 其他配置...
},
});
```
示例:
1. 添加路径别名
```js
import { viteConfig } from "@halo-dev/ui-plugin-bundler-kit";
import path from "path";
export default viteConfig({
vite: {
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
"@components": path.resolve(__dirname, "src/components"),
},
},
},
});
```
2. 添加额外的 Vite 插件
```js
import { viteConfig } from "@halo-dev/ui-plugin-bundler-kit";
import { defineConfig } from "vite";
import UnoCSS from "unocss/vite";
export default viteConfig({
vite: {
plugins: [
UnoCSS(), // 添加 UnoCSS 插件
],
},
});
```
### rsbuildConfig
Rsbuild 是基于 Rspack 开发的上层构建工具,其优势在于兼容 Webpack 生态并且性能优异。我们为什么要选择 Rsbuild 可以查阅 [Vite vs Rsbuild](#vite-vs-rsbuild)。
#### 使用
因为在 [halo-dev/plugin-starter](https://github.com/halo-dev/plugin-starter) 中,默认采用 Vite 构建,所以如果想要使用 Rsbuild 构建,需要手动配置,以下是切换到 Rsbuild 的过程:
安装依赖:
```bash
pnpm install @halo-dev/ui-plugin-bundler-kit@2.21.1 @rsbuild/core -D
```
创建 rsbuild.config.mjs:
```js
import { rsbuildConfig } from "@halo-dev/ui-plugin-bundler-kit";
export default rsbuildConfig()
```
更新 package.json:
```json
{
"type": "module",
"scripts": {
"dev": "rsbuild build --env-mode development --watch",
"build": "rsbuild build"
}
}
```
#### 配置
```js
import { rsbuildConfig } from "@halo-dev/ui-plugin-bundler-kit";
export default rsbuildConfig({
rsbuild: {
// 自定义 Rsbuild 配置
plugins: [
// 额外的插件Vue 插件已预配置)
],
// 其他配置...
},
});
```
示例:
1. 添加路径别名
```js
import { rsbuildConfig } from "@halo-dev/ui-plugin-bundler-kit";
export default rsbuildConfig({
rsbuild: {
source: {
alias: {
"@": "./src",
"@components": "./src/components",
},
},
},
});
```
2. 添加额外的 Rsbuild 插件
```js
import { rsbuildConfig } from "@halo-dev/ui-plugin-bundler-kit";
import { pluginSass } from "@rsbuild/plugin-sass";
export default rsbuildConfig({
rsbuild: {
plugins: [
pluginSass(), // 添加 Sass 插件
],
},
});
```
### HaloUIPluginBundlerKit
旧版本 [plugin-starter](https://github.com/halo-dev/plugin-starter) 使用的方式,目前已经不再推荐。
## 构建输出
在 `viteConfig` 和 `rsbuildConfig` 中,已经配置好了开发环境和生产构建的输出目录,分别是:
- **开发环境**`build/resources/main/console`,在开发 UI 的过程中,可以使用 `pnpm dev` 来实时查看效果
- **生产环境**`ui/build/dist`
> 需要注意的是,生产构建的目录仅仅是临时目录,最终在使用 Gradle 构建插件时会自动构建 UI 并复制到 `src/main/resources/console` 目录中。
## Vite vs Rsbuild{#vite-vs-rsbuild}
Vite 和 Rsbuild 都是优秀的构建工具,但它们在不同的使用场景下有各自的优势:
### 何时使用 Rsbuild
- ✅ **代码分割支持** - Rsbuild 为代码分割和懒加载提供了优秀的支持
- ✅ **更好的性能** - 对于复杂应用,通常有更快的构建时间和更小的包体积
- ✅ **动态导入** - 非常适合有重度前端组件的插件
**动态导入示例:**
```typescript
import { definePlugin } from '@halo-dev/console-shared';
import { defineAsyncComponent } from 'vue';
import { VLoading } from '@halo-dev/components';
export default definePlugin({
routes: [
{
parentName: 'Root',
route: {
path: 'demo',
name: 'DemoPage',
// 懒加载重型组件
component: defineAsyncComponent({
loader: () => import('./views/DemoPage.vue'),
loadingComponent: VLoading,
}),
},
},
],
extensionPoints: {},
});
```
### 何时使用 Vite
- ✅ **Vue 生态友好** - 与 Vue 生态系统工具和插件有更好的集成
- ✅ **丰富的插件生态** - 有大量可用的 Vite 插件
- ✅ **简单配置** - 对于直接的使用场景更容易配置
### 总结
| 特性 | Vite | Rsbuild |
| ---------- | ------ | -------- |
| 代码分割 | ❌ 有限 | ✅ 优秀 |
| Vue 生态 | ✅ 优秀 | ✅ 良好 |
| 构建性能 | ✅ 良好 | ✅ 优秀 |
| 开发体验 | ✅ 优秀 | ✅ 优秀 |
| 插件生态 | ✅ 丰富 | ✅ 增长中 |
| 配置复杂度 | ✅ 简单 | ⚖️ 中等 |
**建议**:对于有大型前端代码库的复杂插件使用 **Rsbuild**,对于简单插件或需要广泛 Vue 生态系统集成时使用 **Vite**。
## 迁移{#migration}
如果你当前的插件使用的是旧版本的 [plugin-starter](https://github.com/halo-dev/plugin-starter),并且想使用新的 `viteConfig` 和 `rsbuildConfig`,可以参考以下步骤:
1. 更新 `@halo-dev/ui-plugin-bundler-kit` 至 `2.21.1` 或更高版本
```bash
pnpm install @halo-dev/ui-plugin-bundler-kit@2.21.1 -D
```
2. 更新 `vite.config.ts` 文件
```typescript
import { viteConfig } from "@halo-dev/ui-plugin-bundler-kit";
export default viteConfig({
vite: {
// Vite 配置需要按照原有的配置进行修改,但需要移除 Vue 插件,因为已经内置
plugins: [
],
},
});
```
3. 更新项目根目录的 `build.gradle` 文件
```diff
plugins {
id 'java'
- id "com.github.node-gradle.node" version "7.0.2"
- id "io.freefair.lombok" version "8.0.1"
- id "run.halo.plugin.devtools" version "0.2.0"
+ id "io.freefair.lombok" version "8.13"
+ id "run.halo.plugin.devtools" version "0.6.0"
}
group 'run.halo.starter'
-sourceCompatibility = JavaVersion.VERSION_17
repositories {
mavenCentral()
- maven { url 'https://s01.oss.sonatype.org/content/repositories/releases' }
- maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots/' }
- maven { url 'https://repo.spring.io/milestone' }
}
dependencies {
- implementation platform('run.halo.tools.platform:plugin:2.20.0-SNAPSHOT')
+ implementation platform('run.halo.tools.platform:plugin:2.21.0')
compileOnly 'run.halo.app:api'
testImplementation 'run.halo.app:api'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
+ testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
test {
useJUnitPlatform()
}
-tasks.withType(JavaCompile).configureEach {
- options.encoding = "UTF-8"
-}
-
-node {
- nodeProjectDir = file("${project.projectDir}/ui")
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(21)
+ }
}
-tasks.register('buildFrontend', PnpmTask) {
- args = ['build']
- dependsOn('installDepsForUI')
+tasks.withType(JavaCompile).configureEach {
+ options.encoding = "UTF-8"
+ options.release = 21
}
-tasks.register('installDepsForUI', PnpmTask) {
- args = ['install']
+tasks.register('processUiResources', Copy) {
+ from project(':ui').tasks.named('buildFrontend')
+ into layout.buildDirectory.dir('resources/main/console')
}
-build {
- // build frontend before build
- tasks.named('compileJava').configure {
- dependsOn('buildFrontend')
- }
+tasks.named('processResources', ProcessResources) {
+ dependsOn tasks.named('processUiResources')
}
```
4. 在 ui 或者 console 目录新建 `build.gradle` 文件,内容如下:
```gradle
plugins {
id 'base'
id "com.github.node-gradle.node" version "7.1.0"
}
group 'run.halo.starter.ui'
tasks.register('buildFrontend', PnpmTask) {
args = ['build']
dependsOn tasks.named('pnpmInstall')
inputs.dir(layout.projectDirectory.dir('src'))
inputs.files(fileTree(
dir: layout.projectDirectory,
includes: ['*.cjs', '*.ts', '*.js', '*.json', '*.yaml']))
outputs.dir(layout.buildDirectory.dir('dist'))
shouldRunAfter(tasks.named('check'))
}
tasks.register('checkFrontend', PnpmTask) {
args = ['test:unit']
dependsOn tasks.named('pnpmInstall')
}
tasks.named('check') {
dependsOn tasks.named('checkFrontend')
}
tasks.named('build') {
dependsOn tasks.named('buildFrontend')
}
```
进行此变更的主要目的是保证 UI 构建的产物不直接输出到源码目录的 resources 目录中,而是通过 Gradle 构建插件时复制到 `src/main/resources/console` 目录中。
完整变更过程可参考:[halo-dev/plugin-starter#52](https://github.com/halo-dev/plugin-starter/pull/52)
如果你不想使用新的 Gradle 构建配置,也可以修改 viteConfig 或 rsbuildConfig 的输出目录,和旧版本保持一致:
viteConfig:
```js
import { viteConfig } from "@halo-dev/ui-plugin-bundler-kit";
const OUT_DIR_PROD = "../src/main/resources/console";
const OUT_DIR_DEV = "../build/resources/main/console";
export default viteConfig({
vite: ({ mode }) => {
const isProduction = mode === "production";
const outDir = isProduction ? OUT_DIR_PROD : OUT_DIR_DEV;
return {
build: {
outDir,
},
};
},
});
```
rsbuildConfig:
```js
import { rsbuildConfig } from "@halo-dev/ui-plugin-bundler-kit";
const OUT_DIR_PROD = "../src/main/resources/console";
const OUT_DIR_DEV = "../build/resources/main/console";
export default rsbuildConfig({
rsbuild: ({ envMode }) => {
const isProduction = envMode === "production";
const outDir = isProduction ? OUT_DIR_PROD : OUT_DIR_DEV;
return {
output: {
distPath: {
root: outDir,
},
},
};
},
});
```

View File

@@ -29,28 +29,37 @@ export function definePlugin(plugin: PluginModule): PluginModule {
```
```ts title="PluginModule"
import type { Component, Ref } from "vue";
import type { RouteRecordRaw, RouteRecordName } from "vue-router";
import type { FunctionalPage } from "../states/pages";
import type { AttachmentSelectProvider } from "../states/attachment-selector";
import type { EditorProvider, PluginTab } from "..";
import type { AnyExtension } from "@halo-dev/richtext-editor";
import type { CommentSubjectRefProvider } from "@/states/comment-subject-ref";
import type { BackupTab } from "@/states/backup";
import type { PluginInstallationTab } from "@/states/plugin-installation-tabs";
import type { CommentSubjectRefProvider } from "@/states/comment-subject-ref";
import type { EntityFieldItem } from "@/states/entity";
import type { OperationItem } from "@/states/operation";
import type { PluginInstallationTab } from "@/states/plugin-installation-tabs";
import type { ThemeListTab } from "@/states/theme-list-tabs";
import type { UserProfileTab, UserTab } from "@/states/user-tab";
import type {
Attachment,
Backup,
ListedPost,
Plugin,
Theme,
ListedComment,
ListedReply,
ListedSinglePage,
} from "@halo-dev/api-client";
import type { AnyExtension } from "@halo-dev/richtext-editor";
import type { Component, Ref } from "vue";
import type { RouteRecordName, RouteRecordRaw } from "vue-router";
import type {
DashboardWidgetDefinition,
DashboardWidgetQuickActionItem,
EditorProvider,
PluginTab,
} from "..";
import type { AttachmentSelectProvider } from "../states/attachment-selector";
import type { FunctionalPage } from "../states/pages";
export interface RouteRecordAppend {
parentName: RouteRecordName;
parentName: NonNullable<RouteRecordName>;
route: RouteRecordRaw;
}
@@ -80,36 +89,65 @@ export interface ExtensionPoint {
"post:list-item:operation:create"?: (
post: Ref<ListedPost>
) => OperationItem<ListedPost>[] | Promise<OperationItem<ListedPost>[]>;
) => OperationItem<ListedPost>[];
"single-page:list-item:operation:create"?: (
singlePage: Ref<ListedSinglePage>
) => OperationItem<ListedSinglePage>[];
"comment:list-item:operation:create"?: (
comment: Ref<ListedComment>
) => OperationItem<ListedComment>[];
"reply:list-item:operation:create"?: (
reply: Ref<ListedReply>
) => OperationItem<ListedReply>[];
"plugin:list-item:operation:create"?: (
plugin: Ref<Plugin>
) => OperationItem<Plugin>[] | Promise<OperationItem<Plugin>[]>;
) => OperationItem<Plugin>[];
"backup:list-item:operation:create"?: (
backup: Ref<Backup>
) => OperationItem<Backup>[] | Promise<OperationItem<Backup>[]>;
) => OperationItem<Backup>[];
"attachment:list-item:operation:create"?: (
attachment: Ref<Attachment>
) => OperationItem<Attachment>[] | Promise<OperationItem<Attachment>[]>;
) => OperationItem<Attachment>[];
"plugin:list-item:field:create"?: (
plugin: Ref<Plugin>
) => EntityFieldItem[] | Promise<EntityFieldItem[]>;
"plugin:list-item:field:create"?: (plugin: Ref<Plugin>) => EntityFieldItem[];
"post:list-item:field:create"?: (
post: Ref<ListedPost>
) => EntityFieldItem[] | Promise<EntityFieldItem[]>;
"post:list-item:field:create"?: (post: Ref<ListedPost>) => EntityFieldItem[];
"single-page:list-item:field:create"?: (
singlePage: Ref<ListedSinglePage>
) => EntityFieldItem[];
"theme:list:tabs:create"?: () => ThemeListTab[] | Promise<ThemeListTab[]>;
"theme:list-item:operation:create"?: (
theme: Ref<Theme>
) => OperationItem<Theme>[] | Promise<OperationItem<Theme>[]>;
) => OperationItem<Theme>[];
"user:detail:tabs:create"?: () => UserTab[] | Promise<UserTab[]>;
"uc:user:profile:tabs:create"?: () =>
| UserProfileTab[]
| Promise<UserProfileTab[]>;
"console:dashboard:widgets:create"?: () =>
| DashboardWidgetDefinition[]
| Promise<DashboardWidgetDefinition[]>;
"console:dashboard:widgets:internal:quick-action:item:create"?: () =>
| DashboardWidgetQuickActionItem[]
| Promise<DashboardWidgetQuickActionItem[]>;
}
export interface PluginModule {
/**
* These components will be registered when plugin is activated.
*/
components?: Record<string, Component>;
routes?: RouteRecordRaw[] | RouteRecordAppend[];
@@ -118,6 +156,7 @@ export interface PluginModule {
extensionPoints?: ExtensionPoint;
}
```
- `components`组件列表key 为组件名称value 为组件对象,在此定义之后,加载插件时会自动注册到 Vue App 全局。

View File

@@ -7,6 +7,6 @@ Halo 插件体系的 UI 部分可以让开发者在 Console 控制台和 UC 个
在开始之前,建议先熟悉或安装以下库和工具:
1. [Node.js 18+](https://nodejs.org)
2. [pnpm 8+](https://pnpm.io)
1. [Node.js 20+](https://nodejs.org)
2. [pnpm 10+](https://pnpm.io)
3. [Vue.js 3](https://vuejs.org)

View File

@@ -11,126 +11,51 @@ description: 了解如何与我们的社区分享你的插件
## 自动构建
如果你是基于 [halo-dev/plugin-starter](https://github.com/halo-dev/plugin-starter) 创建的插件项目,那么已经包含了适用于 GitHub Action 的 `workflow.yaml` 文件,里面包含了构建插件和发布插件资源到 Release 的步骤,可以根据自己的实际需要进行修改,以下是示例:
如果你是基于 [halo-dev/plugin-starter](https://github.com/halo-dev/plugin-starter) 创建的插件项目,那么已经包含了适用于 GitHub Action 的 `ci.yaml``cd.yaml` 文件,里面包含了构建插件和发布插件资源到 Release 的步骤,可以根据自己的实际需要进行修改,以下是示例:
```yaml
name: Build Plugin JAR File
name: CI
on:
push:
branches:
- main
paths:
- "**"
- "!**.md"
release:
types:
- created
pull_request:
branches:
- main
paths:
- "**"
- "!**.md"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Set up JDK 17
uses: actions/setup-java@v2
with:
distribution: 'temurin'
cache: 'gradle'
java-version: 17
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 18
- uses: pnpm/action-setup@v2.0.1
name: Install pnpm
id: pnpm-install
with:
version: 8
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/widget/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install Frontend Dependencies
run: |
./gradlew pnpmInstall
- name: Build with Gradle
run: |
# Set the version with tag name when releasing
version=${{ github.event.release.tag_name }}
version=${version#v}
sed -i "s/version=.*-SNAPSHOT$/version=$version/1" gradle.properties
./gradlew clean build -x test
- name: Archive plugin-starter jar
uses: actions/upload-artifact@v2
with:
name: plugin-starter
path: |
build/libs/*.jar
retention-days: 1
github-release:
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'release'
steps:
- name: Download plugin-starter jar
uses: actions/download-artifact@v2
with:
name: plugin-starter
path: build/libs
- name: Get Name of Artifact
id: get_artifact
run: |
ARTIFACT_PATHNAME=$(ls build/libs/*.jar | head -n 1)
ARTIFACT_NAME=$(basename ${ARTIFACT_PATHNAME})
echo "Artifact pathname: ${ARTIFACT_PATHNAME}"
echo "Artifact name: ${ARTIFACT_NAME}"
echo "ARTIFACT_PATHNAME=${ARTIFACT_PATHNAME}" >> $GITHUB_ENV
echo "ARTIFACT_NAME=${ARTIFACT_NAME}" >> $GITHUB_ENV
echo "RELEASE_ID=${{ github.event.release.id }}" >> $GITHUB_ENV
- name: Upload a Release Asset
uses: actions/github-script@v2
if: github.event_name == 'release'
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
console.log('environment', process.versions);
const fs = require('fs').promises;
const { repo: { owner, repo }, sha } = context;
console.log({ owner, repo, sha });
const releaseId = process.env.RELEASE_ID
const artifactPathName = process.env.ARTIFACT_PATHNAME
const artifactName = process.env.ARTIFACT_NAME
console.log('Releasing', releaseId, artifactPathName, artifactName)
await github.repos.uploadReleaseAsset({
owner, repo,
release_id: releaseId,
name: artifactName,
data: await fs.readFile(artifactPathName)
});
ci:
uses: halo-sigs/reusable-workflows/.github/workflows/plugin-ci.yaml@v3
with:
ui-path: "ui"
pnpm-version: 9
node-version: 22
java-version: 21
```
```yaml
name: CD
on:
release:
types:
- published
jobs:
cd:
uses: halo-sigs/reusable-workflows/.github/workflows/plugin-cd.yaml@v3
permissions:
contents: write
with:
pnpm-version: 9
node-version: 22
java-version: 21
skip-appstore-release: true
```
关于 CI / CD 的更多详细信息,可查阅:[halo-sigs/reusable-workflows](https://github.com/halo-sigs/reusable-workflows)
## 发布你的插件
用户可以在你的仓库 Release 下载使用,但为了方便让 Halo 的用户知道你的插件,可以在以下渠道发布:

View File

@@ -95,11 +95,7 @@
"link": {
"type": "generated-index"
},
"items": [
"contribution/issue",
"contribution/pr",
"contribution/sponsor"
]
"items": ["contribution/issue", "contribution/pr", "contribution/sponsor"]
},
"about"
],
@@ -157,7 +153,8 @@
},
"items": [
"developer-guide/plugin/basics/ui/intro",
"developer-guide/plugin/basics/ui/entry"
"developer-guide/plugin/basics/ui/entry",
"developer-guide/plugin/basics/ui/build"
]
}
]
@@ -319,9 +316,7 @@
"link": {
"type": "generated-index"
},
"items": [
"developer-guide/plugin/examples/todolist"
]
"items": ["developer-guide/plugin/examples/todolist"]
}
]
},