refactor(editor): 统一配置管理、完善插件卸载和热更新同步 (#298)
主要变更: 1. 统一配置管理 (EditorConfig) - 新增 EditorConfig 集中管理路径、文件名、全局变量等配置 - 添加 SDK 模块配置系统 (ISDKModuleConfig) - 重构 PluginSDKRegistry 使用配置而非硬编码 2. 完善插件卸载机制 - 扩展 PluginRegisteredResources 追踪运行时资源 - 实现完整的 deactivatePluginRuntime 清理流程 - ComponentRegistry 添加 unregister/getRegisteredComponents 方法 3. 热更新同步机制 - 新增 HotReloadCoordinator 协调热更新过程 - 热更新期间暂停 ECS 循环避免竞态条件 - 支持超时保护和失败恢复
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
* Project Plugin Loader
|
||||
*/
|
||||
|
||||
import { PluginManager, LocaleService, MessageHub } from '@esengine/editor-core';
|
||||
import { PluginManager, LocaleService, MessageHub, EditorConfig, getPluginsPath } from '@esengine/editor-core';
|
||||
import type { IPlugin, ModuleManifest } from '@esengine/editor-core';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { TauriAPI } from '../api/tauri';
|
||||
@@ -30,13 +30,19 @@ interface LoadedPluginMeta {
|
||||
scriptElement?: HTMLScriptElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件容器全局变量名
|
||||
* Plugin container global variable name
|
||||
*/
|
||||
const PLUGINS_GLOBAL_NAME = EditorConfig.globals.plugins;
|
||||
|
||||
/**
|
||||
* 项目插件加载器
|
||||
*
|
||||
* 使用全局变量方案加载插件:
|
||||
* 1. 插件构建时将 @esengine/* 标记为 external
|
||||
* 2. 插件输出为 IIFE 格式,依赖从 window.__ESENGINE__ 获取
|
||||
* 3. 插件导出到 window.__ESENGINE_PLUGINS__[pluginName]
|
||||
* 2. 插件输出为 IIFE 格式,依赖从全局 SDK 对象获取
|
||||
* 3. 插件导出到全局插件容器
|
||||
*/
|
||||
export class PluginLoader {
|
||||
private loadedPlugins: Map<string, LoadedPluginMeta> = new Map();
|
||||
@@ -51,7 +57,7 @@ export class PluginLoader {
|
||||
// 初始化插件容器
|
||||
this.initPluginContainer();
|
||||
|
||||
const pluginsPath = `${projectPath}/plugins`;
|
||||
const pluginsPath = getPluginsPath(projectPath);
|
||||
|
||||
try {
|
||||
const exists = await TauriAPI.pathExists(pluginsPath);
|
||||
@@ -75,8 +81,8 @@ export class PluginLoader {
|
||||
* 初始化插件容器
|
||||
*/
|
||||
private initPluginContainer(): void {
|
||||
if (!window.__ESENGINE_PLUGINS__) {
|
||||
window.__ESENGINE_PLUGINS__ = {};
|
||||
if (!(window as any)[PLUGINS_GLOBAL_NAME]) {
|
||||
(window as any)[PLUGINS_GLOBAL_NAME] = {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,25 +167,27 @@ export class PluginLoader {
|
||||
): Promise<IPlugin | null> {
|
||||
const pluginKey = this.sanitizePluginKey(pluginName);
|
||||
|
||||
const pluginsContainer = (window as any)[PLUGINS_GLOBAL_NAME] as Record<string, any>;
|
||||
|
||||
try {
|
||||
// 插件代码是 IIFE 格式,会自动导出到 window.__ESENGINE_PLUGINS__
|
||||
// 插件代码是 IIFE 格式,会自动导出到全局插件容器
|
||||
await this.executeViaScriptTag(code, pluginName);
|
||||
|
||||
// 从全局容器获取插件模块
|
||||
const pluginModule = window.__ESENGINE_PLUGINS__[pluginKey];
|
||||
const pluginModule = pluginsContainer[pluginKey];
|
||||
if (!pluginModule) {
|
||||
// 尝试其他可能的 key 格式
|
||||
const altKeys = Object.keys(window.__ESENGINE_PLUGINS__).filter(k =>
|
||||
const altKeys = Object.keys(pluginsContainer).filter(k =>
|
||||
k.includes(pluginName.replace(/@/g, '').replace(/\//g, '_').replace(/-/g, '_'))
|
||||
);
|
||||
|
||||
if (altKeys.length > 0 && altKeys[0] !== undefined) {
|
||||
const foundKey = altKeys[0];
|
||||
const altModule = window.__ESENGINE_PLUGINS__[foundKey];
|
||||
const altModule = pluginsContainer[foundKey];
|
||||
return this.findPluginLoader(altModule);
|
||||
}
|
||||
|
||||
console.error(`[PluginLoader] Plugin ${pluginName} did not export to __ESENGINE_PLUGINS__`);
|
||||
console.error(`[PluginLoader] Plugin ${pluginName} did not export to ${PLUGINS_GLOBAL_NAME}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -330,11 +338,13 @@ export class PluginLoader {
|
||||
* 卸载所有已加载的插件
|
||||
*/
|
||||
async unloadProjectPlugins(_pluginManager: PluginManager): Promise<void> {
|
||||
const pluginsContainer = (window as any)[PLUGINS_GLOBAL_NAME] as Record<string, any> | undefined;
|
||||
|
||||
for (const pluginName of this.loadedPlugins.keys()) {
|
||||
// 清理全局容器中的插件
|
||||
const pluginKey = this.sanitizePluginKey(pluginName);
|
||||
if (window.__ESENGINE_PLUGINS__?.[pluginKey]) {
|
||||
delete window.__ESENGINE_PLUGINS__[pluginKey];
|
||||
if (pluginsContainer?.[pluginKey]) {
|
||||
delete pluginsContainer[pluginKey];
|
||||
}
|
||||
|
||||
// 移除 script 标签
|
||||
@@ -353,10 +363,3 @@ export class PluginLoader {
|
||||
return Array.from(this.loadedPlugins.keys());
|
||||
}
|
||||
}
|
||||
|
||||
// 全局类型声明
|
||||
declare global {
|
||||
interface Window {
|
||||
__ESENGINE_PLUGINS__: Record<string, any>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,96 +5,122 @@
|
||||
* 将编辑器核心模块暴露为全局变量,供插件使用。
|
||||
* 插件构建时将这些模块标记为 external,运行时从全局对象获取。
|
||||
*
|
||||
* Exposes editor core modules as global variables for plugin use.
|
||||
* Plugins mark these modules as external during build, and access them from global object at runtime.
|
||||
*
|
||||
* 使用方式:
|
||||
* 1. 编辑器启动时调用 PluginSDKRegistry.initialize()
|
||||
* 2. 插件构建配置中设置 external: ['@esengine/editor-runtime', ...]
|
||||
* 3. 插件构建配置中设置 globals: { '@esengine/editor-runtime': '__ESENGINE__.editorRuntime' }
|
||||
* 2. 插件构建配置中设置 external: getSDKPackageNames()
|
||||
* 3. 插件构建配置中设置 globals: getSDKGlobalsMapping()
|
||||
*/
|
||||
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { EntityStoreService, MessageHub } from '@esengine/editor-core';
|
||||
import {
|
||||
EntityStoreService,
|
||||
MessageHub,
|
||||
EditorConfig,
|
||||
getSDKGlobalsMapping,
|
||||
getSDKPackageNames,
|
||||
getEnabledSDKModules,
|
||||
type ISDKModuleConfig
|
||||
} from '@esengine/editor-core';
|
||||
|
||||
// 导入所有需要暴露给插件的模块
|
||||
import * as editorRuntime from '@esengine/editor-runtime';
|
||||
// 动态导入所有 SDK 模块
|
||||
// Dynamic import all SDK modules
|
||||
import * as ecsFramework from '@esengine/ecs-framework';
|
||||
import * as editorRuntime from '@esengine/editor-runtime';
|
||||
import * as behaviorTree from '@esengine/behavior-tree';
|
||||
import * as engineCore from '@esengine/engine-core';
|
||||
import * as sprite from '@esengine/sprite';
|
||||
import * as camera from '@esengine/camera';
|
||||
import * as audio from '@esengine/audio';
|
||||
|
||||
// 存储服务实例引用(在初始化时设置)
|
||||
let entityStoreInstance: EntityStoreService | null = null;
|
||||
let messageHubInstance: MessageHub | null = null;
|
||||
|
||||
// SDK 模块映射
|
||||
const SDK_MODULES = {
|
||||
'@esengine/editor-runtime': editorRuntime,
|
||||
/**
|
||||
* 模块实例映射
|
||||
* Module instance mapping
|
||||
*
|
||||
* 由于 ES 模块的静态导入限制,我们需要维护一个包名到模块的映射。
|
||||
* Due to ES module static import limitations, we need to maintain a mapping from package name to module.
|
||||
*/
|
||||
const MODULE_INSTANCES: Record<string, any> = {
|
||||
'@esengine/ecs-framework': ecsFramework,
|
||||
'@esengine/editor-runtime': editorRuntime,
|
||||
'@esengine/behavior-tree': behaviorTree,
|
||||
'@esengine/engine-core': engineCore,
|
||||
'@esengine/sprite': sprite,
|
||||
'@esengine/camera': camera,
|
||||
'@esengine/audio': audio,
|
||||
} as const;
|
||||
};
|
||||
|
||||
// 全局变量名称映射(用于插件构建配置)
|
||||
export const SDK_GLOBALS = {
|
||||
'@esengine/editor-runtime': '__ESENGINE__.editorRuntime',
|
||||
'@esengine/ecs-framework': '__ESENGINE__.ecsFramework',
|
||||
'@esengine/behavior-tree': '__ESENGINE__.behaviorTree',
|
||||
'@esengine/engine-core': '__ESENGINE__.engineCore',
|
||||
'@esengine/sprite': '__ESENGINE__.sprite',
|
||||
'@esengine/camera': '__ESENGINE__.camera',
|
||||
'@esengine/audio': '__ESENGINE__.audio',
|
||||
} as const;
|
||||
// 存储服务实例引用(在初始化时设置)
|
||||
// Service instance references (set during initialization)
|
||||
let entityStoreInstance: EntityStoreService | null = null;
|
||||
let messageHubInstance: MessageHub | null = null;
|
||||
|
||||
/**
|
||||
* 插件 API 接口
|
||||
* 为插件提供统一的访问接口,避免模块实例不一致的问题
|
||||
* Plugin API interface
|
||||
*
|
||||
* 为插件提供统一的访问接口,避免模块实例不一致的问题。
|
||||
* Provides unified access interface for plugins, avoiding module instance inconsistency issues.
|
||||
*/
|
||||
export interface IPluginAPI {
|
||||
/** 获取当前场景 */
|
||||
/** 获取当前场景 | Get current scene */
|
||||
getScene(): any;
|
||||
/** 获取 EntityStoreService */
|
||||
/** 获取 EntityStoreService | Get EntityStoreService */
|
||||
getEntityStore(): EntityStoreService;
|
||||
/** 获取 MessageHub */
|
||||
/** 获取 MessageHub | Get MessageHub */
|
||||
getMessageHub(): MessageHub;
|
||||
/** 解析服务 */
|
||||
/** 解析服务 | Resolve service */
|
||||
resolveService<T>(serviceType: any): T;
|
||||
/** 获取 Core 实例 */
|
||||
/** 获取 Core 实例 | Get Core instance */
|
||||
getCore(): typeof Core;
|
||||
}
|
||||
|
||||
// 扩展 Window.__ESENGINE__ 类型(基础类型已在 PluginAPI.ts 中定义)
|
||||
interface ESEngineGlobal {
|
||||
editorRuntime: typeof editorRuntime;
|
||||
ecsFramework: typeof ecsFramework;
|
||||
behaviorTree: typeof behaviorTree;
|
||||
engineCore: typeof engineCore;
|
||||
sprite: typeof sprite;
|
||||
camera: typeof camera;
|
||||
audio: typeof audio;
|
||||
/**
|
||||
* SDK 全局对象类型
|
||||
* SDK global object type
|
||||
*/
|
||||
export interface ISDKGlobal {
|
||||
/** 动态模块加载 | Dynamic module loading */
|
||||
require: (moduleName: string) => any;
|
||||
/** 插件 API | Plugin API */
|
||||
api: IPluginAPI;
|
||||
/** 其他动态注册的模块 | Other dynamically registered modules */
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件 SDK 注册器
|
||||
* Plugin SDK Registry
|
||||
*
|
||||
* 职责:
|
||||
* 1. 将 SDK 模块暴露到全局对象
|
||||
* 2. 提供插件 API
|
||||
* 3. 支持动态模块加载
|
||||
*
|
||||
* Responsibilities:
|
||||
* 1. Expose SDK modules to global object
|
||||
* 2. Provide plugin API
|
||||
* 3. Support dynamic module loading
|
||||
*/
|
||||
export class PluginSDKRegistry {
|
||||
private static initialized = false;
|
||||
|
||||
/**
|
||||
* 初始化 SDK 注册器
|
||||
* 将所有 SDK 模块暴露到全局对象
|
||||
* Initialize SDK registry
|
||||
*
|
||||
* 将所有配置的 SDK 模块暴露到全局对象。
|
||||
* Exposes all configured SDK modules to global object.
|
||||
*/
|
||||
static initialize(): void {
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取服务实例(使用编辑器内部的类型,确保类型匹配)
|
||||
// 获取服务实例
|
||||
// Get service instances
|
||||
entityStoreInstance = Core.services.resolve(EntityStoreService);
|
||||
messageHubInstance = Core.services.resolve(MessageHub);
|
||||
|
||||
@@ -105,8 +131,47 @@ export class PluginSDKRegistry {
|
||||
console.error('[PluginSDKRegistry] MessageHub not registered yet!');
|
||||
}
|
||||
|
||||
// 创建插件 API - 直接返回实例引用,避免类型匹配问题
|
||||
const pluginAPI: IPluginAPI = {
|
||||
// 创建 SDK 全局对象
|
||||
// Create SDK global object
|
||||
const sdkGlobal: ISDKGlobal = {
|
||||
require: this.requireModule.bind(this),
|
||||
api: this.createPluginAPI(),
|
||||
};
|
||||
|
||||
// 从配置自动注册所有启用的模块
|
||||
// Auto-register all enabled modules from config
|
||||
const enabledModules = getEnabledSDKModules();
|
||||
for (const config of enabledModules) {
|
||||
const moduleInstance = MODULE_INSTANCES[config.packageName];
|
||||
if (moduleInstance) {
|
||||
sdkGlobal[config.globalKey] = moduleInstance;
|
||||
} else {
|
||||
console.warn(
|
||||
`[PluginSDKRegistry] Module "${config.packageName}" configured but not imported. ` +
|
||||
`Please add import statement for this module.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置全局对象
|
||||
// Set global object
|
||||
const sdkGlobalName = EditorConfig.globals.sdk;
|
||||
(window as any)[sdkGlobalName] = sdkGlobal;
|
||||
|
||||
this.initialized = true;
|
||||
|
||||
console.log(
|
||||
`[PluginSDKRegistry] Initialized with ${enabledModules.length} modules:`,
|
||||
enabledModules.map(m => m.globalKey)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建插件 API
|
||||
* Create plugin API
|
||||
*/
|
||||
private static createPluginAPI(): IPluginAPI {
|
||||
return {
|
||||
getScene: () => Core.scene,
|
||||
getEntityStore: () => {
|
||||
if (!entityStoreInstance) {
|
||||
@@ -123,36 +188,29 @@ export class PluginSDKRegistry {
|
||||
resolveService: <T>(serviceType: any): T => Core.services.resolve(serviceType) as T,
|
||||
getCore: () => Core,
|
||||
};
|
||||
|
||||
// 创建全局命名空间
|
||||
window.__ESENGINE__ = {
|
||||
editorRuntime,
|
||||
ecsFramework,
|
||||
behaviorTree,
|
||||
engineCore,
|
||||
sprite,
|
||||
camera,
|
||||
audio,
|
||||
require: this.requireModule.bind(this),
|
||||
api: pluginAPI,
|
||||
};
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态获取模块(用于 CommonJS 风格的插件)
|
||||
* Dynamic module loading (for CommonJS style plugins)
|
||||
*
|
||||
* @param moduleName 模块包名 | Module package name
|
||||
*/
|
||||
private static requireModule(moduleName: string): any {
|
||||
const module = SDK_MODULES[moduleName as keyof typeof SDK_MODULES];
|
||||
const module = MODULE_INSTANCES[moduleName];
|
||||
if (!module) {
|
||||
throw new Error(`[PluginSDKRegistry] Unknown module: ${moduleName}`);
|
||||
const availableModules = Object.keys(MODULE_INSTANCES).join(', ');
|
||||
throw new Error(
|
||||
`[PluginSDKRegistry] Unknown module: "${moduleName}". ` +
|
||||
`Available modules: ${availableModules}`
|
||||
);
|
||||
}
|
||||
return module;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已初始化
|
||||
* Check if initialized
|
||||
*/
|
||||
static isInitialized(): boolean {
|
||||
return this.initialized;
|
||||
@@ -160,15 +218,25 @@ export class PluginSDKRegistry {
|
||||
|
||||
/**
|
||||
* 获取所有可用的 SDK 模块名称
|
||||
* Get all available SDK module names
|
||||
*
|
||||
* @deprecated 使用 getSDKPackageNames() 代替 | Use getSDKPackageNames() instead
|
||||
*/
|
||||
static getAvailableModules(): string[] {
|
||||
return Object.keys(SDK_MODULES);
|
||||
return getSDKPackageNames();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局变量映射(用于生成插件构建配置)
|
||||
* Get globals config (for generating plugin build config)
|
||||
*
|
||||
* @deprecated 使用 getSDKGlobalsMapping() 代替 | Use getSDKGlobalsMapping() instead
|
||||
*/
|
||||
static getGlobalsConfig(): Record<string, string> {
|
||||
return { ...SDK_GLOBALS };
|
||||
return getSDKGlobalsMapping();
|
||||
}
|
||||
}
|
||||
|
||||
// 重新导出辅助函数,方便插件构建工具使用
|
||||
// Re-export helper functions for plugin build tools
|
||||
export { getSDKGlobalsMapping, getSDKPackageNames, getEnabledSDKModules };
|
||||
|
||||
Reference in New Issue
Block a user