refactor(arch): 改进 ServiceToken 设计,统一服务获取模式 (#300)

* refactor(arch): 移除全局变量,使用 ServiceToken 模式

- 创建 PluginServiceRegistry 类,提供类型安全的服务注册/获取
- 添加 ProfilerServiceToken 和 CollisionLayerConfigToken
- 重构所有 __PROFILER_SERVICE__ 全局变量访问为 getProfilerService()
- 重构 __PHYSICS_RAPIER2D__ 全局变量访问为 CollisionLayerConfigToken
- 在 Core 类添加 pluginServices 静态属性
- 添加 getService.ts 辅助模块简化服务获取

这是 ServiceToken 模式重构的第一阶段,移除了最常用的两个全局变量。
后续可继续应用到其他模块(Camera/Audio 等)。

* refactor(arch): 改进 ServiceToken 设计,移除重复常量

- tokens.ts: 从 engine-core 导入 createServiceToken(符合规范)
- tokens.ts: Token 使用接口 IProfilerService 而非具体类
- 移除 AssetPickerDialog 和 ContentBrowser 中重复的 MANAGED_ASSET_DIRECTORIES
- 统一从 editor-core 导入 MANAGED_ASSET_DIRECTORIES

* fix(type): 修复 IProfilerService 接口与实现类型不匹配

- 将 ProfilerData 等数据类型移到 tokens.ts 以避免循环依赖
- ProfilerService 显式实现 IProfilerService 接口
- 更新使用方使用 IProfilerService 接口类型而非具体类

* refactor(type): 移除类型重导出,改进类型安全

- 删除 ProfilerService.ts 中的类型重导出,消费方直接从 tokens.ts 导入
- PanelDescriptor 接口添加 titleZh 属性,移除 App.tsx 中的 as any
- 改进 useDynamicIcon.ts 的类型安全,使用正确的 Record 类型

* refactor(arch): 为模块添加 ServiceToken 支持

- Material System: 创建 tokens.ts,定义 IMaterialManager 接口和 MaterialManagerToken
- Audio: 创建预留 tokens.ts 文件,为未来 AudioManager 服务扩展做准备
- Camera: 创建预留 tokens.ts 文件,为未来 CameraManager 服务扩展做准备

遵循"谁定义接口,谁导出 Token"原则,统一服务访问模式
This commit is contained in:
YHH
2025-12-09 11:07:44 +08:00
committed by GitHub
parent c71a47f2b0
commit 995fa2d514
31 changed files with 1024 additions and 210 deletions

View File

@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
import { X, BarChart3, Maximize2, Minimize2 } from 'lucide-react';
import { ProfilerService } from '../services/ProfilerService';
import { Core } from '@esengine/ecs-framework';
import { ProfilerServiceToken, type IProfilerService } from '../services/tokens';
import { AdvancedProfiler } from './AdvancedProfiler';
import '../styles/ProfilerWindow.css';
@@ -8,19 +9,19 @@ interface AdvancedProfilerWindowProps {
onClose: () => void;
}
interface WindowWithProfiler extends Window {
__PROFILER_SERVICE__?: ProfilerService;
}
export function AdvancedProfilerWindow({ onClose }: AdvancedProfilerWindowProps) {
const [profilerService, setProfilerService] = useState<ProfilerService | null>(null);
const [profilerService, setProfilerService] = useState<IProfilerService | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
useEffect(() => {
const service = (window as WindowWithProfiler).__PROFILER_SERVICE__;
if (service) {
setProfilerService(service);
try {
const service = Core.pluginServices.get(ProfilerServiceToken);
if (service) {
setProfilerService(service);
}
} catch {
// Core 可能还没有初始化
}
}, []);

View File

@@ -40,22 +40,13 @@ import {
AlertTriangle
} from 'lucide-react';
import { Core } from '@esengine/ecs-framework';
import { MessageHub, FileActionRegistry, AssetRegistryService, type FileCreationTemplate } from '@esengine/editor-core';
import { MessageHub, FileActionRegistry, AssetRegistryService, MANAGED_ASSET_DIRECTORIES, type FileCreationTemplate } from '@esengine/editor-core';
import { TauriAPI, DirectoryEntry } from '../api/tauri';
import { SettingsService } from '../services/SettingsService';
import { ContextMenu, ContextMenuItem } from './ContextMenu';
import { PromptDialog } from './PromptDialog';
import '../styles/ContentBrowser.css';
/**
* Directories managed by asset registry (GUID system)
* 被资产注册表GUID 系统)管理的目录
*
* Note: This is duplicated from AssetRegistryService to avoid build dependency issues.
* Keep in sync with MANAGED_ASSET_DIRECTORIES in AssetRegistryService.ts
*/
const MANAGED_ASSET_DIRECTORIES = ['assets', 'scripts', 'scenes'] as const;
interface AssetItem {
name: string;
path: string;

View File

@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { X, Server, WifiOff, Wifi } from 'lucide-react';
import { SettingsService } from '../services/SettingsService';
import { ProfilerService } from '../services/ProfilerService';
import { getProfilerService } from '../services/getService';
import '../styles/PortManager.css';
interface PortManagerProps {
@@ -58,7 +58,7 @@ export function PortManager({ onClose }: PortManagerProps) {
const handleStopServer = async () => {
setIsStopping(true);
try {
const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined;
const profilerService = getProfilerService();
if (profilerService) {
await profilerService.manualStopServer();
setIsServerRunning(false);
@@ -73,7 +73,7 @@ export function PortManager({ onClose }: PortManagerProps) {
const handleStartServer = async () => {
setIsStarting(true);
try {
const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined;
const profilerService = getProfilerService();
if (profilerService) {
await profilerService.manualStartServer();
await new Promise((resolve) => setTimeout(resolve, 500));

View File

@@ -1,9 +1,10 @@
import { useState, useEffect } from 'react';
import { Activity, Cpu, Layers, Package, Wifi, WifiOff, Maximize2, Pause, Play, BarChart3 } from 'lucide-react';
import { ProfilerService, ProfilerData } from '../services/ProfilerService';
import type { ProfilerData } from '../services/tokens';
import { SettingsService } from '../services/SettingsService';
import { Core } from '@esengine/ecs-framework';
import { MessageHub } from '@esengine/editor-core';
import { getProfilerService } from '../services/getService';
import '../styles/ProfilerDockPanel.css';
export function ProfilerDockPanel() {
@@ -32,7 +33,7 @@ export function ProfilerDockPanel() {
}, []);
useEffect(() => {
const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined;
const profilerService = getProfilerService();
if (!profilerService) {
console.warn('[ProfilerDockPanel] ProfilerService not available - plugin may be disabled');

View File

@@ -3,6 +3,7 @@ import { Core } from '@esengine/ecs-framework';
import { Activity, BarChart3, Clock, Cpu, RefreshCw, Pause, Play, X, Wifi, WifiOff, Server, Search, Table2, TreePine } from 'lucide-react';
import { ProfilerService } from '../services/ProfilerService';
import { SettingsService } from '../services/SettingsService';
import { getProfilerService } from '../services/getService';
import '../styles/ProfilerWindow.css';
interface SystemPerformanceData {
@@ -59,7 +60,7 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
// Check ProfilerService connection status
useEffect(() => {
const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined;
const profilerService = getProfilerService();
if (!profilerService) {
return;
@@ -186,7 +187,7 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
useEffect(() => {
if (dataSource !== 'remote') return;
const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined;
const profilerService = getProfilerService();
if (!profilerService) {
console.warn('[ProfilerWindow] ProfilerService not available');

View File

@@ -8,7 +8,8 @@ import {
Eye, Star, Lock, Settings, Filter, Folder, Sun, Cloud, Mountain, Flag,
SquareStack, FolderPlus
} from 'lucide-react';
import { ProfilerService, RemoteEntity } from '../services/ProfilerService';
import type { RemoteEntity } from '../services/tokens';
import { getProfilerService } from '../services/getService';
import { confirm } from '@tauri-apps/plugin-dialog';
import { CreateEntityCommand, DeleteEntityCommand, ReparentEntityCommand, DropPosition } from '../application/commands/entity';
import '../styles/SceneHierarchy.css';
@@ -264,7 +265,7 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
// Subscribe to remote entity data from ProfilerService
useEffect(() => {
const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined;
const profilerService = getProfilerService();
if (!profilerService) {
return;
@@ -444,7 +445,7 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
const handleRemoteEntityClick = (entity: RemoteEntity) => {
setSelectedIds(new Set([entity.id]));
const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined;
const profilerService = getProfilerService();
if (profilerService) {
profilerService.requestEntityDetails(entity.id);
}

View File

@@ -1,18 +1,10 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { X, Search, Folder, FolderOpen, File, Image, FileText, Music, Video, Database, AlertTriangle } from 'lucide-react';
import { Core } from '@esengine/ecs-framework';
import { ProjectService, AssetRegistryService } from '@esengine/editor-core';
import { ProjectService, AssetRegistryService, MANAGED_ASSET_DIRECTORIES } from '@esengine/editor-core';
import { TauriFileSystemService } from '../../services/TauriFileSystemService';
import './AssetPickerDialog.css';
/**
* Directories managed by asset registry (GUID system)
* Only files in these directories can be selected
*
* Note: Keep in sync with MANAGED_ASSET_DIRECTORIES in AssetRegistryService.ts
*/
const MANAGED_ASSET_DIRECTORIES = ['assets', 'scripts', 'scenes'] as const;
interface AssetPickerDialogProps {
isOpen: boolean;
onClose: () => void;

View File

@@ -4,18 +4,15 @@
*/
import React, { useState, useEffect, useRef, useCallback } from 'react';
/**
* 碰撞层配置接口(用于获取自定义层名称)
*/
interface CollisionLayerConfigAPI {
getLayers(): Array<{ name: string }>;
addListener(callback: () => void): void;
removeListener(callback: () => void): void;
}
import { Core } from '@esengine/ecs-framework';
import {
CollisionLayerConfigToken,
type ICollisionLayerConfig
} from '@esengine/physics-rapier2d';
/**
* 默认层名称(当 CollisionLayerConfig 不可用时使用)
* Default layer names (used when CollisionLayerConfig is unavailable)
*/
const DEFAULT_LAYER_NAMES = [
'Default', 'Player', 'Enemy', 'Projectile',
@@ -24,25 +21,18 @@ const DEFAULT_LAYER_NAMES = [
'Layer12', 'Layer13', 'Layer14', 'Layer15',
];
let cachedConfig: CollisionLayerConfigAPI | null = null;
/**
* 尝试获取 CollisionLayerConfig 实例
* Try to get CollisionLayerConfig instance
*/
function getCollisionConfig(): CollisionLayerConfigAPI | null {
if (cachedConfig) return cachedConfig;
function getCollisionConfig(): ICollisionLayerConfig | undefined {
try {
// 动态导入以避免循环依赖
const physicsModule = (window as any).__PHYSICS_RAPIER2D__;
if (physicsModule?.CollisionLayerConfig) {
cachedConfig = physicsModule.CollisionLayerConfig.getInstance();
return cachedConfig;
}
return Core.pluginServices.get(CollisionLayerConfigToken);
} catch {
// 忽略错误
// Core 可能还没有初始化
// Core might not be initialized yet
return undefined;
}
return null;
}
interface CollisionLayerFieldProps {

View File

@@ -1,4 +1,6 @@
import { Core } from '@esengine/ecs-framework';
import { ComponentData } from './types';
import { ProfilerServiceToken, type IProfilerService } from '../../services/tokens';
export function formatNumber(value: number, decimalPlaces: number): string {
if (decimalPlaces < 0) {
@@ -10,13 +12,21 @@ export function formatNumber(value: number, decimalPlaces: number): string {
return value.toFixed(decimalPlaces);
}
export interface ProfilerService {
requestEntityDetails(entityId: number): void;
subscribe(callback: () => void): () => void;
}
export function getProfilerService(): ProfilerService | undefined {
return (window as any).__PROFILER_SERVICE__;
/**
* 获取 ProfilerService 实例
* Get ProfilerService instance
*
* 使用 ServiceToken 从 Core.pluginServices 获取服务。
* Uses ServiceToken to get service from Core.pluginServices.
*/
export function getProfilerService(): IProfilerService | undefined {
try {
return Core.pluginServices.get(ProfilerServiceToken);
} catch {
// Core 可能还没有初始化
// Core might not be initialized yet
return undefined;
}
}
export function isComponentData(value: unknown): value is ComponentData {