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; return plugin;
} }

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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