feat(editor): 完善用户代码热更新和环境检测 (#275)
* fix: 更新 bundle-runtime 脚本使用正确的 platform-web 输出文件 原脚本引用的 runtime.browser.js 不存在,改为使用 index.mjs * feat(editor): 完善用户代码热更新和环境检测 ## 热更新改进 - 添加 hotReloadInstances() 方法,通过更新原型链实现真正的热更新 - 组件实例保留数据,仅更新方法 - ComponentRegistry 支持热更新时替换同名组件类 ## 环境检测 - 启动时检测 esbuild 可用性 - 在启动页面底部显示环境状态指示器 - 添加 check_environment Rust 命令和前端 API ## esbuild 打包 - 将 esbuild 二进制文件打包到应用中 - 用户无需全局安装 esbuild - 支持 Windows/macOS/Linux 平台 ## 文件监视优化 - 添加 300ms 防抖,避免重复编译 - 修复路径分隔符混合问题 ## 资源打包修复 - 修复 Tauri 资源配置,保留 engine 目录结构 - 添加 src-tauri/bin/ 和 src-tauri/engine/ 到 gitignore * fix: 将热更新模式改为可选,修复测试失败 - ComponentRegistry 添加 hotReloadEnabled 标志,默认禁用 - 只有启用热更新模式时才会替换同名组件类 - 编辑器环境自动启用热更新模式 - reset() 方法中重置热更新标志 * test: 添加热更新模式的测试用例
This commit is contained in:
@@ -21,6 +21,14 @@ export class ComponentRegistry {
|
||||
private static maskCache = new Map<string, BitMask64Data>();
|
||||
private static nextBitIndex = 0;
|
||||
|
||||
/**
|
||||
* 热更新模式标志,默认禁用
|
||||
* Hot reload mode flag, disabled by default
|
||||
* 编辑器环境应启用此选项以支持脚本热更新
|
||||
* Editor environment should enable this to support script hot reload
|
||||
*/
|
||||
private static hotReloadEnabled = false;
|
||||
|
||||
/**
|
||||
* 注册组件类型并分配位掩码
|
||||
* @param componentType 组件类型
|
||||
@@ -34,11 +42,27 @@ export class ComponentRegistry {
|
||||
return existingIndex;
|
||||
}
|
||||
|
||||
// 检查是否有同名但不同类的组件已注册
|
||||
if (this.componentNameToType.has(typeName)) {
|
||||
// 检查是否有同名但不同类的组件已注册(热更新场景)
|
||||
// Check if a component with the same name but different class is registered (hot reload scenario)
|
||||
if (this.hotReloadEnabled && this.componentNameToType.has(typeName)) {
|
||||
const existingType = this.componentNameToType.get(typeName);
|
||||
if (existingType !== componentType) {
|
||||
console.warn(`[ComponentRegistry] Component name conflict: "${typeName}" already registered with different class. Existing: ${existingType?.name}, New: ${componentType.name}`);
|
||||
// 热更新:替换旧的类为新的类,复用相同的 bitIndex
|
||||
// Hot reload: replace old class with new class, reuse the same bitIndex
|
||||
const existingIndex = this.componentTypes.get(existingType!)!;
|
||||
|
||||
// 移除旧类的映射
|
||||
// Remove old class mapping
|
||||
this.componentTypes.delete(existingType!);
|
||||
|
||||
// 用新类更新映射
|
||||
// Update mappings with new class
|
||||
this.componentTypes.set(componentType, existingIndex);
|
||||
this.bitIndexToType.set(existingIndex, componentType);
|
||||
this.componentNameToType.set(typeName, componentType);
|
||||
|
||||
console.log(`[ComponentRegistry] Hot reload: replaced component "${typeName}"`);
|
||||
return existingIndex;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,6 +233,32 @@ export class ComponentRegistry {
|
||||
this.maskCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用热更新模式
|
||||
* Enable hot reload mode
|
||||
* 在编辑器环境中调用以支持脚本热更新
|
||||
* Call in editor environment to support script hot reload
|
||||
*/
|
||||
public static enableHotReload(): void {
|
||||
this.hotReloadEnabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用热更新模式
|
||||
* Disable hot reload mode
|
||||
*/
|
||||
public static disableHotReload(): void {
|
||||
this.hotReloadEnabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查热更新模式是否启用
|
||||
* Check if hot reload mode is enabled
|
||||
*/
|
||||
public static isHotReloadEnabled(): boolean {
|
||||
return this.hotReloadEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置注册表(用于测试)
|
||||
*/
|
||||
@@ -219,5 +269,6 @@ export class ComponentRegistry {
|
||||
this.componentNameToId.clear();
|
||||
this.maskCache.clear();
|
||||
this.nextBitIndex = 0;
|
||||
this.hotReloadEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +180,103 @@ describe('ComponentRegistry Extended - 64+ 组件支持', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('热更新模式', () => {
|
||||
it('默认应该禁用热更新模式', () => {
|
||||
expect(ComponentRegistry.isHotReloadEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('应该能够启用和禁用热更新模式', () => {
|
||||
expect(ComponentRegistry.isHotReloadEnabled()).toBe(false);
|
||||
|
||||
ComponentRegistry.enableHotReload();
|
||||
expect(ComponentRegistry.isHotReloadEnabled()).toBe(true);
|
||||
|
||||
ComponentRegistry.disableHotReload();
|
||||
expect(ComponentRegistry.isHotReloadEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('reset 应该重置热更新模式为禁用', () => {
|
||||
ComponentRegistry.enableHotReload();
|
||||
expect(ComponentRegistry.isHotReloadEnabled()).toBe(true);
|
||||
|
||||
ComponentRegistry.reset();
|
||||
expect(ComponentRegistry.isHotReloadEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('启用热更新时应该替换同名组件类', () => {
|
||||
ComponentRegistry.enableHotReload();
|
||||
|
||||
// 模拟热更新场景:两个不同的类但有相同的 constructor.name
|
||||
// Simulate hot reload: two different classes with same constructor.name
|
||||
const createHotReloadComponent = (version: number) => {
|
||||
// 使用 eval 创建具有相同名称的类
|
||||
// Use function constructor to create classes with same name
|
||||
const cls = (new Function('Component', `
|
||||
return class HotReloadTestComponent extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.version = ${version};
|
||||
}
|
||||
}
|
||||
`))(Component);
|
||||
return cls;
|
||||
};
|
||||
|
||||
const TestComponentV1 = createHotReloadComponent(1);
|
||||
const TestComponentV2 = createHotReloadComponent(2);
|
||||
|
||||
// 确保两个类名相同但不是同一个类
|
||||
expect(TestComponentV1.name).toBe(TestComponentV2.name);
|
||||
expect(TestComponentV1).not.toBe(TestComponentV2);
|
||||
|
||||
const index1 = ComponentRegistry.register(TestComponentV1);
|
||||
const index2 = ComponentRegistry.register(TestComponentV2);
|
||||
|
||||
// 应该复用相同的 bitIndex
|
||||
expect(index1).toBe(index2);
|
||||
|
||||
// 新类应该替换旧类
|
||||
expect(ComponentRegistry.isRegistered(TestComponentV2)).toBe(true);
|
||||
expect(ComponentRegistry.isRegistered(TestComponentV1)).toBe(false);
|
||||
});
|
||||
|
||||
it('禁用热更新时不应该替换同名组件类', () => {
|
||||
// 确保热更新被禁用
|
||||
ComponentRegistry.disableHotReload();
|
||||
|
||||
// 创建两个同名组件
|
||||
// Create two classes with same constructor.name
|
||||
const createNoHotReloadComponent = (id: number) => {
|
||||
const cls = (new Function('Component', `
|
||||
return class NoHotReloadComponent extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.id = ${id};
|
||||
}
|
||||
}
|
||||
`))(Component);
|
||||
return cls;
|
||||
};
|
||||
|
||||
const TestCompA = createNoHotReloadComponent(1);
|
||||
const TestCompB = createNoHotReloadComponent(2);
|
||||
|
||||
// 确保两个类名相同但不是同一个类
|
||||
expect(TestCompA.name).toBe(TestCompB.name);
|
||||
expect(TestCompA).not.toBe(TestCompB);
|
||||
|
||||
const index1 = ComponentRegistry.register(TestCompA);
|
||||
const index2 = ComponentRegistry.register(TestCompB);
|
||||
|
||||
// 应该分配不同的 bitIndex(因为热更新被禁用)
|
||||
expect(index2).toBe(index1 + 1);
|
||||
|
||||
// 两个类都应该被注册
|
||||
expect(ComponentRegistry.isRegistered(TestCompA)).toBe(true);
|
||||
expect(ComponentRegistry.isRegistered(TestCompB)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界情况', () => {
|
||||
it('应该正确处理第 64 个组件(边界)', () => {
|
||||
const scene = new Scene();
|
||||
|
||||
Reference in New Issue
Block a user