refactor: plugin extension points of console plugin (#738)

#### What type of PR is this?

/kind improvement
/milestone 2.0

#### What this PR does / why we need it:

重构 Console 端插件扩展点的定义方式。现在需要如下定义:

```ts
  extensionPoints: {
    "page:functional:create": () => {
      return [
        {
          name: "链接",
          url: "/links",
          path: "/pages/functional/links",
          permissions: ["plugin:links:view"],
        },
      ];
    },
  },
```

#### Special notes for your reviewer:

可以用以下插件进行测试:

- https://github.com/halo-sigs/plugin-links
- https://github.com/halo-sigs/plugin-unsplash

#### Does this PR introduce a user-facing change?


```release-note
None
```
pull/741/head
Ryan Wang 2022-12-01 01:15:50 +08:00 committed by GitHub
parent 7e2a2852ea
commit 8b24cee0f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 95 additions and 106 deletions

View File

@ -1,5 +1,5 @@
import type { Plugin } from "../types/plugin";
import type { PluginModule } from "../types/plugin";
export function definePlugin(plugin: Plugin): Plugin {
export function definePlugin(plugin: PluginModule): PluginModule {
return plugin;
}

View File

@ -1,10 +1,6 @@
import type { Attachment } from "@halo-dev/api-client";
import type { Component } from "vue";
export interface AttachmentSelectorPublicState {
providers: AttachmentProvider[];
}
export type AttachmentLike =
| Attachment
| string
@ -13,7 +9,7 @@ export type AttachmentLike =
type: string;
};
export interface AttachmentProvider {
export interface AttachmentSelectProvider {
id: string;
label: string;
component: Component | string;

View File

@ -1,8 +1,4 @@
export interface PagesPublicState {
functionalPages: FunctionalPagesState[];
}
export interface FunctionalPagesState {
export interface FunctionalPage {
name: string;
path: string;
url?: string;

View File

@ -1,22 +1,22 @@
import type { Component, Ref } from "vue";
import type { Component } from "vue";
import type { RouteRecordRaw, RouteRecordName } from "vue-router";
import type { PagesPublicState } from "../states/pages";
import type { AttachmentSelectorPublicState } from "../states/attachment-selector";
export type ExtensionPointName = "PAGES" | "POSTS" | "ATTACHMENT_SELECTOR";
export type ExtensionPointState =
| PagesPublicState
| AttachmentSelectorPublicState;
import type { FunctionalPage } from "../states/pages";
import type { AttachmentSelectProvider } from "../states/attachment-selector";
export interface RouteRecordAppend {
parentName: RouteRecordName;
route: RouteRecordRaw;
}
export interface Plugin {
name: string;
export interface ExtensionPoint {
"page:functional:create"?: () => FunctionalPage[] | Promise<FunctionalPage[]>;
"attachment:selector:create"?: () =>
| AttachmentSelectProvider[]
| Promise<AttachmentSelectProvider[]>;
}
export interface PluginModule {
/**
* These components will be registered when plugin is activated.
*/
@ -34,7 +34,5 @@ export interface Plugin {
routes?: RouteRecordRaw[] | RouteRecordAppend[];
extensionPoints?: {
[key in ExtensionPointName]?: (state: Ref<ExtensionPointState>) => void;
};
extensionPoints?: ExtensionPoint;
}

View File

@ -1,23 +0,0 @@
import { usePluginStore } from "@/stores/plugin";
import type {
ExtensionPointName,
ExtensionPointState,
} from "@halo-dev/console-shared";
import type { Plugin } from "@halo-dev/api-client";
import type { Ref } from "vue";
export function useExtensionPointsState(
point: ExtensionPointName,
state: Ref<ExtensionPointState>
) {
const { plugins } = usePluginStore();
plugins.forEach((plugin: Plugin) => {
// @ts-ignore
if (!plugin.spec.module?.extensionPoints?.[point]) {
return;
}
// @ts-ignore
plugin.spec.module.extensionPoints[point]?.(state);
});
}

View File

@ -3,7 +3,7 @@ import type { DirectiveBinding } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
import router from "./router";
import type { Plugin, RouteRecordAppend } from "@halo-dev/console-shared";
import type { PluginModule, RouteRecordAppend } from "@halo-dev/console-shared";
import { Toast } from "@halo-dev/components";
import { apiClient } from "@/utils/api-client";
// setup
@ -12,7 +12,7 @@ import { setupComponents } from "./setup/setupComponents";
// core modules
import { coreModules } from "./modules";
import { useScriptTag } from "@vueuse/core";
import { usePluginStore } from "@/stores/plugin";
import { usePluginModuleStore } from "@/stores/plugin";
import { hasPermission } from "@/utils/permission";
import { useRoleStore } from "@/stores/role";
import type { RouteRecordRaw } from "vue-router";
@ -26,7 +26,7 @@ setupComponents(app);
app.use(createPinia());
function registerModule(pluginModule: Plugin, core: boolean) {
function registerModule(pluginModule: PluginModule, core: boolean) {
if (pluginModule.components) {
Object.keys(pluginModule.components).forEach((key) => {
const component = pluginModule.components?.[key];
@ -38,7 +38,6 @@ function registerModule(pluginModule: Plugin, core: boolean) {
if (pluginModule.routes) {
if (!Array.isArray(pluginModule.routes)) {
console.error(`${pluginModule.name}: Plugin routes must be an array`);
return;
}
@ -86,7 +85,7 @@ function loadCoreModules() {
});
}
const pluginStore = usePluginStore();
const pluginModuleStore = usePluginModuleStore();
function loadStyle(href: string) {
return new Promise(function (resolve, reject) {
@ -144,9 +143,8 @@ async function loadPluginModules() {
const pluginModule = window[plugin.metadata.name];
if (pluginModule) {
// @ts-ignore
plugin.spec.module = pluginModule;
registerModule(pluginModule, false);
pluginModuleStore.registerPluginModule(pluginModule);
}
} catch (e) {
const message = `${plugin.metadata.name}: 加载插件入口文件失败`;
@ -164,8 +162,6 @@ async function loadPluginModules() {
pluginErrorMessages.push(message);
}
}
pluginStore.registerPlugin(plugin);
}
if (pluginErrorMessages.length > 0) {

View File

@ -1,13 +1,14 @@
<script lang="ts" setup>
import { VButton, VModal, VTabbar } from "@halo-dev/components";
import { ref, markRaw } from "vue";
import { ref, markRaw, onMounted } from "vue";
import CoreSelectorProvider from "./selector-providers/CoreSelectorProvider.vue";
import UploadSelectorProvider from "./selector-providers/UploadSelectorProvider.vue";
import type {
AttachmentLike,
AttachmentSelectorPublicState,
AttachmentSelectProvider,
PluginModule,
} from "@halo-dev/console-shared";
import { useExtensionPointsState } from "@/composables/usePlugins";
import { usePluginModuleStore } from "@/stores/plugin";
withDefaults(
defineProps<{
@ -26,24 +27,42 @@ const emit = defineEmits<{
const selected = ref<AttachmentLike[]>([] as AttachmentLike[]);
const attachmentSelectorPublicState = ref<AttachmentSelectorPublicState>({
providers: [
{
id: "core",
label: "附件库",
component: markRaw(CoreSelectorProvider),
},
{
id: "upload",
label: "上传",
component: markRaw(UploadSelectorProvider),
},
],
const attachmentSelectProviders = ref<AttachmentSelectProvider[]>([
{
id: "core",
label: "附件库",
component: markRaw(CoreSelectorProvider),
},
{
id: "upload",
label: "上传",
component: markRaw(UploadSelectorProvider),
},
]);
// resolve plugin extension points
const { pluginModules } = usePluginModuleStore();
onMounted(() => {
pluginModules.forEach((pluginModule: PluginModule) => {
const { extensionPoints } = pluginModule;
if (!extensionPoints?.["attachment:selector:create"]) {
return;
}
const providers = extensionPoints[
"attachment:selector:create"
]() as AttachmentSelectProvider[];
if (providers) {
providers.forEach((provider) => {
attachmentSelectProviders.value.push(provider);
});
}
});
});
useExtensionPointsState("ATTACHMENT_SELECTOR", attachmentSelectorPublicState);
const activeId = ref(attachmentSelectorPublicState.value.providers[0].id);
const activeId = ref(attachmentSelectProviders.value[0].id);
const onVisibleChange = (visible: boolean) => {
emit("update:visible", visible);
@ -53,7 +72,7 @@ const onVisibleChange = (visible: boolean) => {
};
const onChangeProvider = (providerId: string) => {
const provider = attachmentSelectorPublicState.value.providers.find(
const provider = attachmentSelectProviders.value.find(
(provider) => provider.id === providerId
);
@ -80,14 +99,14 @@ const handleConfirm = () => {
>
<VTabbar
v-model:active-id="activeId"
:items="attachmentSelectorPublicState.providers"
:items="attachmentSelectProviders"
class="w-full"
type="outline"
></VTabbar>
<div v-if="visible" class="mt-2">
<template
v-for="(provider, index) in attachmentSelectorPublicState.providers"
v-for="(provider, index) in attachmentSelectProviders"
:key="index"
>
<Suspense>

View File

@ -6,7 +6,6 @@ import { IconFolder } from "@halo-dev/components";
import { markRaw } from "vue";
export default definePlugin({
name: "attachmentModule",
components: {
AttachmentSelectorModal,
},

View File

@ -6,7 +6,6 @@ import CommentStatsWidget from "./widgets/CommentStatsWidget.vue";
import { markRaw } from "vue";
export default definePlugin({
name: "commentModule",
components: {
CommentStatsWidget,
},

View File

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { ref } from "vue";
import { onMounted, ref } from "vue";
import {
VEmpty,
VSpace,
@ -8,19 +8,37 @@ import {
VEntity,
VEntityField,
} from "@halo-dev/components";
import type { PagesPublicState } from "@halo-dev/console-shared";
import { useExtensionPointsState } from "@/composables/usePlugins";
import type { FunctionalPage, PluginModule } from "@halo-dev/console-shared";
import { usePluginModuleStore } from "@/stores/plugin";
const pagesPublicState = ref<PagesPublicState>({
functionalPages: [],
const functionalPages = ref<FunctionalPage[]>([] as FunctionalPage[]);
// resolve plugin extension points
const { pluginModules } = usePluginModuleStore();
onMounted(() => {
pluginModules.forEach((pluginModule: PluginModule) => {
const { extensionPoints } = pluginModule;
if (!extensionPoints?.["page:functional:create"]) {
return;
}
const pages = extensionPoints[
"page:functional:create"
]() as FunctionalPage[];
if (pages) {
pages.forEach((page) => {
functionalPages.value.push(page);
});
}
});
});
useExtensionPointsState("PAGES", pagesPublicState);
</script>
<template>
<VEmpty
v-if="!pagesPublicState.functionalPages.length"
v-if="!functionalPages.length"
message="当前没有功能页面,功能页面通常由各个插件提供,你可以尝试安装新插件以获得支持"
title="当前没有功能页面"
>
@ -41,7 +59,7 @@ useExtensionPointsState("PAGES", pagesPublicState);
role="list"
>
<li
v-for="(page, index) in pagesPublicState.functionalPages"
v-for="(page, index) in functionalPages"
:key="index"
v-permission="page.permissions"
>

View File

@ -11,7 +11,6 @@ import { IconPages } from "@halo-dev/components";
import { markRaw } from "vue";
export default definePlugin({
name: "pageModule",
components: {
SinglePageStatsWidget,
},

View File

@ -12,7 +12,6 @@ import RecentPublishedWidget from "./widgets/RecentPublishedWidget.vue";
import { markRaw } from "vue";
export default definePlugin({
name: "postModule",
components: {
PostStatsWidget,
RecentPublishedWidget,

View File

@ -8,7 +8,6 @@ import ViewsStatsWidget from "./widgets/ViewsStatsWidget.vue";
import { markRaw } from "vue";
export default definePlugin({
name: "dashboardModule",
components: {
QuickLinkWidget,
ViewsStatsWidget,

View File

@ -5,7 +5,6 @@ import { IconListSettings } from "@halo-dev/components";
import { markRaw } from "vue";
export default definePlugin({
name: "menuModule",
components: {},
routes: [
{

View File

@ -6,7 +6,6 @@ import { IconPalette } from "@halo-dev/components";
import { markRaw } from "vue";
export default definePlugin({
name: "themeModule",
components: {},
routes: [
{

View File

@ -9,7 +9,6 @@ import { IconPlug } from "@halo-dev/components";
import { markRaw } from "vue";
export default definePlugin({
name: "pluginModule",
components: {},
routes: [
{

View File

@ -4,7 +4,6 @@ import RoleList from "./RoleList.vue";
import RoleDetail from "./RoleDetail.vue";
export default definePlugin({
name: "roleModule",
components: {},
routes: [
{

View File

@ -5,7 +5,6 @@ import { IconSettings } from "@halo-dev/components";
import { markRaw } from "vue";
export default definePlugin({
name: "settingModule",
components: {},
routes: [
{

View File

@ -11,7 +11,6 @@ import { IconUserSettings } from "@halo-dev/components";
import { markRaw } from "vue";
export default definePlugin({
name: "userModule",
components: {
UserStatsWidget,
},

View File

@ -1,17 +1,17 @@
import { defineStore } from "pinia";
import type { Plugin } from "@halo-dev/api-client";
import type { PluginModule } from "@halo-dev/console-shared";
interface PluginStoreState {
plugins: Plugin[];
pluginModules: PluginModule[];
}
export const usePluginStore = defineStore("plugin", {
export const usePluginModuleStore = defineStore("plugin", {
state: (): PluginStoreState => ({
plugins: [],
pluginModules: [],
}),
actions: {
registerPlugin(plugin: Plugin) {
this.plugins.push(plugin);
registerPluginModule(pluginModule: PluginModule) {
this.pluginModules.push(pluginModule);
},
},
});