refactor(editor): 统一配置管理、完善插件卸载和热更新同步 (#298)

主要变更:

1. 统一配置管理 (EditorConfig)
   - 新增 EditorConfig 集中管理路径、文件名、全局变量等配置
   - 添加 SDK 模块配置系统 (ISDKModuleConfig)
   - 重构 PluginSDKRegistry 使用配置而非硬编码

2. 完善插件卸载机制
   - 扩展 PluginRegisteredResources 追踪运行时资源
   - 实现完整的 deactivatePluginRuntime 清理流程
   - ComponentRegistry 添加 unregister/getRegisteredComponents 方法

3. 热更新同步机制
   - 新增 HotReloadCoordinator 协调热更新过程
   - 热更新期间暂停 ECS 循环避免竞态条件
   - 支持超时保护和失败恢复
This commit is contained in:
YHH
2025-12-09 09:06:29 +08:00
committed by GitHub
parent 40a38b8b88
commit 6c99b811ec
12 changed files with 1270 additions and 172 deletions

View File

@@ -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>;
}
}

View File

@@ -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 };