mirror of
https://github.com/halo-dev/docs.git
synced 2025-10-21 10:17:34 +00:00
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:
@@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
```
|
@@ -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 全局。
|
||||
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user