组件发现和动态加载系统

This commit is contained in:
YHH
2025-10-15 00:40:27 +08:00
parent b757c1d06c
commit cbfe09b5e9
8 changed files with 861 additions and 13 deletions

View File

@@ -0,0 +1,102 @@
import type { IService } from '@esengine/ecs-framework';
import { Injectable } from '@esengine/ecs-framework';
import { createLogger } from '@esengine/ecs-framework';
import { MessageHub } from './MessageHub';
const logger = createLogger('ComponentDiscoveryService');
export interface ComponentFileInfo {
path: string;
fileName: string;
className: string | null;
}
export interface ComponentScanOptions {
basePath: string;
pattern: string;
scanFunction: (path: string, pattern: string) => Promise<string[]>;
readFunction: (path: string) => Promise<string>;
}
@Injectable()
export class ComponentDiscoveryService implements IService {
private discoveredComponents: Map<string, ComponentFileInfo> = new Map();
private messageHub: MessageHub;
constructor(messageHub: MessageHub) {
this.messageHub = messageHub;
}
public async scanComponents(options: ComponentScanOptions): Promise<ComponentFileInfo[]> {
try {
logger.info('Scanning for components', {
basePath: options.basePath,
pattern: options.pattern
});
const files = await options.scanFunction(options.basePath, options.pattern);
logger.info(`Found ${files.length} component files`);
const componentInfos: ComponentFileInfo[] = [];
for (const filePath of files) {
try {
const fileContent = await options.readFunction(filePath);
const componentInfo = this.parseComponentFile(filePath, fileContent);
if (componentInfo) {
componentInfos.push(componentInfo);
this.discoveredComponents.set(filePath, componentInfo);
}
} catch (error) {
logger.warn(`Failed to parse component file: ${filePath}`, error);
}
}
await this.messageHub.publish('components:discovered', {
count: componentInfos.length,
components: componentInfos
});
logger.info(`Successfully parsed ${componentInfos.length} components`);
return componentInfos;
} catch (error) {
logger.error('Failed to scan components', error);
throw error;
}
}
public getDiscoveredComponents(): ComponentFileInfo[] {
return Array.from(this.discoveredComponents.values());
}
public clearDiscoveredComponents(): void {
this.discoveredComponents.clear();
logger.info('Cleared discovered components');
}
private parseComponentFile(filePath: string, content: string): ComponentFileInfo | null {
const fileName = filePath.split(/[\\/]/).pop() || '';
const classMatch = content.match(/export\s+class\s+(\w+)\s+extends\s+Component/);
if (classMatch) {
const className = classMatch[1];
logger.debug(`Found component class: ${className} in ${fileName}`);
return {
path: filePath,
fileName,
className
};
}
logger.debug(`No valid component class found in ${fileName}`);
return null;
}
public dispose(): void {
this.discoveredComponents.clear();
logger.info('ComponentDiscoveryService disposed');
}
}

View File

@@ -0,0 +1,141 @@
import type { IService } from '@esengine/ecs-framework';
import { Injectable, Component } from '@esengine/ecs-framework';
import { createLogger } from '@esengine/ecs-framework';
import { MessageHub } from './MessageHub';
import type { ComponentFileInfo } from './ComponentDiscoveryService';
import { ComponentRegistry } from './ComponentRegistry';
const logger = createLogger('ComponentLoaderService');
export interface LoadedComponentInfo {
fileInfo: ComponentFileInfo;
componentClass: typeof Component;
loadedAt: number;
}
@Injectable()
export class ComponentLoaderService implements IService {
private loadedComponents: Map<string, LoadedComponentInfo> = new Map();
private messageHub: MessageHub;
private componentRegistry: ComponentRegistry;
constructor(messageHub: MessageHub, componentRegistry: ComponentRegistry) {
this.messageHub = messageHub;
this.componentRegistry = componentRegistry;
}
public async loadComponents(
componentInfos: ComponentFileInfo[],
modulePathTransform?: (filePath: string) => string
): Promise<LoadedComponentInfo[]> {
logger.info(`Loading ${componentInfos.length} components`);
const loadedComponents: LoadedComponentInfo[] = [];
for (const componentInfo of componentInfos) {
try {
const loadedComponent = await this.loadComponent(componentInfo, modulePathTransform);
if (loadedComponent) {
loadedComponents.push(loadedComponent);
}
} catch (error) {
logger.error(`Failed to load component: ${componentInfo.fileName}`, error);
}
}
await this.messageHub.publish('components:loaded', {
count: loadedComponents.length,
components: loadedComponents
});
logger.info(`Successfully loaded ${loadedComponents.length} components`);
return loadedComponents;
}
public async loadComponent(
componentInfo: ComponentFileInfo,
modulePathTransform?: (filePath: string) => string
): Promise<LoadedComponentInfo | null> {
try {
const modulePath = modulePathTransform
? modulePathTransform(componentInfo.path)
: this.convertToModulePath(componentInfo.path);
logger.debug(`Loading component from: ${modulePath}`);
const module = await import(/* @vite-ignore */ modulePath);
if (!componentInfo.className) {
logger.warn(`No class name found for component: ${componentInfo.fileName}`);
return null;
}
const componentClass = module[componentInfo.className];
if (!componentClass || !(componentClass.prototype instanceof Component)) {
logger.error(`Invalid component class: ${componentInfo.className}`);
return null;
}
this.componentRegistry.register({
name: componentInfo.className,
type: componentClass
});
const loadedInfo: LoadedComponentInfo = {
fileInfo: componentInfo,
componentClass,
loadedAt: Date.now()
};
this.loadedComponents.set(componentInfo.path, loadedInfo);
logger.info(`Component loaded and registered: ${componentInfo.className}`);
return loadedInfo;
} catch (error) {
logger.error(`Failed to load component: ${componentInfo.fileName}`, error);
return null;
}
}
public getLoadedComponents(): LoadedComponentInfo[] {
return Array.from(this.loadedComponents.values());
}
public unloadComponent(filePath: string): boolean {
const loadedComponent = this.loadedComponents.get(filePath);
if (!loadedComponent || !loadedComponent.fileInfo.className) {
return false;
}
this.componentRegistry.unregister(loadedComponent.fileInfo.className);
this.loadedComponents.delete(filePath);
logger.info(`Component unloaded: ${loadedComponent.fileInfo.className}`);
return true;
}
public clearLoadedComponents(): void {
for (const [filePath] of this.loadedComponents) {
this.unloadComponent(filePath);
}
logger.info('Cleared all loaded components');
}
private convertToModulePath(filePath: string): string {
const normalizedPath = filePath.replace(/\\/g, '/');
if (normalizedPath.startsWith('http://') || normalizedPath.startsWith('https://')) {
return normalizedPath;
}
return `file:///${normalizedPath}`;
}
public dispose(): void {
this.clearLoadedComponents();
logger.info('ComponentLoaderService disposed');
}
}

View File

@@ -15,5 +15,7 @@ export * from './Services/ComponentRegistry';
export * from './Services/LocaleService';
export * from './Services/PropertyMetadata';
export * from './Services/ProjectService';
export * from './Services/ComponentDiscoveryService';
export * from './Services/ComponentLoaderService';
export * from './Types/UITypes';