import { RuntimeError } from '@ibiz-template/core'; import { IPluginFactory, IPluginItem, RemotePluginConfig, RemotePluginItem, } from '@ibiz-template/runtime'; import { IPSAppPFPluginRef, IPSSysPFPlugin } from '@ibiz/dynamic-model-api'; import path from 'path-browserify'; import Vue, { VueConstructor, PluginObject } from 'vue'; const { join } = path; /** * 插件工具类 * * @author chitanda * @date 2022-10-21 16:10:29 * @export * @class PluginFactory */ export class PluginFactory implements IPluginFactory { /** * 是否为 http || https 开头 * * @author chitanda * @date 2022-11-07 14:11:28 * @protected */ protected urlReg = /^http[s]?:\/\/[^\s]*/; /** * 是否已经加载过文件缓存 * * @author chitanda * @date 2022-10-31 14:10:17 * @protected * @type {Map<string, boolean>} */ protected cache: Map<string, boolean> = new Map(); /** * 本地开发测试包,只在本地开发生效 * * @author chitanda * @date 2022-11-02 21:11:41 * @protected */ protected devPackages: Map<string, () => Promise<unknown>> = new Map(); /** * 插件缓存 * * @author chitanda * @date 2022-10-31 14:10:28 * @protected * @type {Map<string, RemotePluginItem>} */ protected pluginCache: Map<string, RemotePluginItem> = new Map(); /** * 预定义插件集合 * * @author chitanda * @date 2023-03-09 17:03:46 * @protected * @type {Map<string, IPluginItem>} */ protected predefinedPlugins: Map<string, IPluginItem> = new Map(); /** * 在预置加载清单的插件 * * @author chitanda * @date 2023-04-02 23:04:40 * @type {string[]} */ readonly appPlugins: string[] = []; /** * 注册视图默认插件 * * @author chitanda * @date 2023-02-06 21:02:10 * @param {IPluginItem} plugin */ registerPredefinedPlugin(plugin: IPluginItem): void { this.predefinedPlugins.set(plugin.name, plugin); } /** * 给入应用实例,将已经加载的过插件注入。主要用于多实例的情况 * * @author chitanda * @date 2023-02-02 16:02:51 * @param {App} app */ register(_app: VueConstructor): void {} /** * 加载预置插件 * * @author chitanda * @date 2023-03-09 18:03:48 * @param {string} name * @return {*} {Promise<void>} */ async loadPredefinedPlugin(name: string): Promise<void> { if (this.predefinedPlugins.has(name)) { const plugin = this.predefinedPlugins.get(name)!; if (plugin) { await this.loadPluginRef(plugin.name, plugin.path); } } } /** * 插件刚加载完成回来,设置到目前所有的 vue 实例当中 * * @author chitanda * @date 2023-02-02 17:02:38 * @protected * @param {PluginObject<unknown>} code */ protected setPluginCode(code: PluginObject<unknown>): void { Vue.use(code); } /** * 设置开发插件,用于本地调试 * * @author chitanda * @date 2022-11-02 21:11:56 * @param {string} name * @param {() => Promise<unknown>} fn */ setDevPlugin(name: string, fn: () => Promise<unknown>): void { this.devPackages.set(name, fn); } /** * 加载插件 * * @author chitanda * @date 2022-10-31 14:10:13 * @param {IPSSysPFPlugin} plugin * @return {*} {Promise<boolean>} */ async loadPlugin(plugin: IPSSysPFPlugin): Promise<boolean> { if (plugin.runtimeObject === true) { const pluginRef = plugin as unknown as IPSAppPFPluginRef; if (pluginRef) { try { await this.loadPluginRef( pluginRef.rTObjectName, pluginRef.rTObjectRepo, ); } catch (error) { ibiz.log.error(error); } } } return false; } /** * 加载应用插件 * * @author chitanda * @date 2022-10-31 16:10:57 * @param {IPSAppPFPluginRef} pluginRef * @return {*} {Promise<boolean>} */ async loadPluginRef( rtObjectName: string, rtObjectRepo: string, ): Promise<boolean> { if (this.pluginCache.has(rtObjectName)) { return true; } let configData: unknown = null; const fn = this.devPackages.get(rtObjectName); if (fn) { configData = await fn(); } else { const pluginPath: string = rtObjectRepo; const configUrl = this.urlReg.test(pluginPath) ? `${pluginPath}/package.json` : `${ibiz.env.pluginBaseUrl}/${join(pluginPath, 'package.json')}`; const res = await ibiz.net.axios({ method: 'get', headers: { 'Access-Control-Allow-Origin': '*' }, url: configUrl, }); if (res.status !== 200) { throw new Error(`配置加载失败`); } configData = res.data; } const remotePlugin = new RemotePluginItem( rtObjectName, rtObjectRepo, configData as RemotePluginConfig, ); if (remotePlugin) { try { await this.loadScript(remotePlugin); this.pluginCache.set(rtObjectName, remotePlugin); return true; } catch (error) { ibiz.log.error(error); } } return false; } /** * 加载插件 * * @author chitanda * @date 2022-11-02 14:11:31 * @protected * @param {RemotePluginItem} remotePlugin * @return {*} {Promise<void>} */ protected async loadScript(remotePlugin: RemotePluginItem): Promise<void> { const pluginPath: string = remotePlugin.repo; const { name, version, system, module } = remotePlugin.config; let scriptUrl = ''; if (this.devPackages.has(name)) { scriptUrl = join(`${name}@${version}`, module); } else if (ibiz.env.dev) { scriptUrl = this.urlReg.test(pluginPath) ? `${pluginPath}/${join(module)}` : `${ibiz.env.pluginBaseUrl}/${pluginPath}/${join(module)}`; } else { scriptUrl = `${pluginPath}${pluginPath.endsWith('/') ? '' : '/'}${join( system, )}`; } if (scriptUrl) { if (this.cache.has(scriptUrl)) { return; } let data: IParams | null = null; const url = this.parseUrl(scriptUrl); // 本地开发模式下的包引用加载 if (this.devPackages.has(name)) { const fn = this.devPackages.get(name)!; // eslint-disable-next-line @typescript-eslint/no-explicit-any const pluginModule = (await fn()) as any; this.setPluginCode(pluginModule.default); this.cache.set(scriptUrl, true); } else if (ibiz.env.dev) { data = await import(scriptUrl /* @vite-ignore */); } else { System.addImportMap({ imports: { [name]: url, }, }); data = await System.import(name); } if (data) { if (data.default) { this.setPluginCode(data.default); } else { throw new RuntimeError( `远程插件加载失败, 远程插件未找到[default]默认导出`, ); } this.cache.set(scriptUrl, true); } else { throw new RuntimeError( `远程插件加载失败, 未找到文件或文件内容格式不正确`, ); } } } /** * 编译请求文件地址 * * @author chitanda * @date 2022-10-31 14:10:19 * @protected * @param {string} script * @return {*} {string} */ protected parseUrl(script: string): string { if (this.urlReg.test(script)) { return script; } let url: string = ''; if (this.urlReg.test(ibiz.env.pluginBaseUrl)) { if (script.startsWith('/')) { url = ibiz.env.pluginBaseUrl + script; } else { url = `${ibiz.env.pluginBaseUrl}/${script}`; } } else { url = `${join(ibiz.env.pluginBaseUrl, script)}`; } const { origin, pathname } = window.location; if (pathname.endsWith('/') && url.startsWith('/')) { url = url.substring(1); } url = `${origin}${pathname}${url}`; return url; } }