From ea4359aa7c9134ea85c93dfeca7785aeccb0145c Mon Sep 17 00:00:00 2001 From: tangkai <1944876319@qq.com> Date: Tue, 14 Apr 2026 21:05:33 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9rpc,=20=E6=98=AF=E5=85=B6?= =?UTF-8?q?=E5=85=BC=E5=AE=B9fetch=E5=92=8Cprocess=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/scene/scene-process/fetch-rpc.ts | 84 +++++++++++++++++++ src/core/scene/scene-process/rpc.ts | 29 +++++-- .../service/core/global-events.ts | 82 ++++++++++++++---- .../scene/scene-process/service/engine.ts | 2 +- .../scene/scene-process/service/script.ts | 11 ++- 5 files changed, 184 insertions(+), 24 deletions(-) create mode 100644 src/core/scene/scene-process/fetch-rpc.ts diff --git a/src/core/scene/scene-process/fetch-rpc.ts b/src/core/scene/scene-process/fetch-rpc.ts new file mode 100644 index 000000000..ff0fea311 --- /dev/null +++ b/src/core/scene/scene-process/fetch-rpc.ts @@ -0,0 +1,84 @@ +/** + * 基于 fetch 的 RPC 客户端 + * 替代 ProcessRPC,将 module.method(args) 调用转为 HTTP POST 请求 + */ +export class FetchRPC> { + private baseURL: string; + private defaultTimeout: number; + private disposed = false; + + constructor(baseURL: string, options?: { timeout?: number }) { + // 确保 baseURL 不以 / 结尾 + this.baseURL = baseURL.replace(/\/+$/, ''); + this.defaultTimeout = options?.timeout ?? 30000; + } + + /** + * 远程方法调用(请求-响应) + * 签名与 ProcessRPC.request 保持一致 + */ + async request( + module: K, + method: M, + args?: any[], + options?: { timeout?: number } + ): Promise { + if (this.disposed) { + throw new Error('[FetchRPC] Instance has been disposed'); + } + + const timeout = options?.timeout ?? this.defaultTimeout; + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeout); + + try { + const response = await fetch(`${this.baseURL}/scene/rpc`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + module: module as string, + method: method as string, + args: args || [], + }), + signal: controller.signal, + }); + + if (!response.ok) { + throw new Error( + `[FetchRPC] HTTP ${response.status}: ${module as string}.${method as string}` + ); + } + + const data = await response.json(); + + if (data.error) { + throw new Error(data.error); + } + + return data.result; + } catch (err: any) { + if (err.name === 'AbortError') { + throw new Error( + `[FetchRPC] Request timeout: ${module as string}.${method as string}` + ); + } + throw err; + } finally { + clearTimeout(timer); + } + } + + /** + * 是否已连接(fetch 模式下始终返回 true,除非 disposed) + */ + isConnect(): boolean { + return !this.disposed; + } + + /** + * 清理 + */ + dispose(): void { + this.disposed = true; + } +} diff --git a/src/core/scene/scene-process/rpc.ts b/src/core/scene/scene-process/rpc.ts index 3a356887a..225c167fd 100644 --- a/src/core/scene/scene-process/rpc.ts +++ b/src/core/scene/scene-process/rpc.ts @@ -1,8 +1,9 @@ import { ProcessRPC } from '../process-rpc'; +import { FetchRPC } from './fetch-rpc'; import type { IMainModule } from '../main-process'; export class RpcProxy { - private rpcInstance: ProcessRPC | null = null; + private rpcInstance: ProcessRPC | FetchRPC | null = null; public getInstance() { if (!this.rpcInstance) { @@ -11,16 +12,32 @@ export class RpcProxy { return this.rpcInstance; } - async startup() { + async startup(serverURL?: string) { // 在创建新实例前,先清理旧实例,防止内存泄漏 this.dispose(); - this.rpcInstance = new ProcessRPC(); - this.rpcInstance.attach(process); - const { Service } = await import('./service/core/decorator'); - this.rpcInstance.register(Service); + const baseURL = serverURL || this._inferBaseURL(); + const isNode = typeof process !== 'undefined' && !!process.versions?.node; + if (isNode) { + this.rpcInstance = new FetchRPC(baseURL); + } + else { + this.rpcInstance = new ProcessRPC(); + const { Service } = await import('./service/core/decorator'); + this.rpcInstance.register(Service); + } console.log('[Scene] Scene Process RPC ready'); } + /** + * 从当前页面 URL 推断 API 基地址 + */ + private _inferBaseURL(): string { + if (typeof location !== 'undefined') { + return location.origin; + } + return 'http://localhost:3000'; + } + /** * 清理 RPC 实例 */ diff --git a/src/core/scene/scene-process/service/core/global-events.ts b/src/core/scene/scene-process/service/core/global-events.ts index 0f7644fda..e96609c30 100644 --- a/src/core/scene/scene-process/service/core/global-events.ts +++ b/src/core/scene/scene-process/service/core/global-events.ts @@ -1,14 +1,71 @@ -import { EventEmitter } from 'events'; -import { SceneProcessEventTag } from '../../../common'; +/** + * 纯 JS 实现的轻量 EventEmitter,替代 Node.js 的 EventEmitter + * 在浏览器/Worker 环境中无需依赖 Node 'events' 模块 + */ +class SimpleEventEmitter { + private _listeners = new Map void>>(); + + on(event: string, listener: (...args: any[]) => void): void { + if (!this._listeners.has(event)) { + this._listeners.set(event, new Set()); + } + this._listeners.get(event)!.add(listener); + } + + once(event: string, listener: (...args: any[]) => void): void { + const wrapper = (...args: any[]) => { + this.off(event, wrapper); + listener(...args); + }; + (wrapper as any)._original = listener; + this.on(event, wrapper); + } + + off(event: string, listener: (...args: any[]) => void): void { + const set = this._listeners.get(event); + if (!set) return; + // 直接移除 + if (set.delete(listener)) return; + // 尝试移除 once 包装 + for (const fn of set) { + if ((fn as any)._original === listener) { + set.delete(fn); + return; + } + } + } + + emit(event: string, ...args: any[]): void { + const set = this._listeners.get(event); + if (!set) return; + for (const listener of set) { + listener(...args); + } + } + + removeAllListeners(event?: string): void { + if (event) { + this._listeners.delete(event); + } else { + this._listeners.clear(); + } + } +} // 全局共享的 EventEmitter 实例(内部使用,不对外暴露) -const globalEventEmitter = new EventEmitter(); +const globalEventEmitter = new SimpleEventEmitter(); /** * 全局事件管理器 * 统一管理所有服务的事件监听,支持类型安全的事件订阅 + * + * Fetch 方案改造: + * - EventEmitter 替换为纯 JS 实现的 SimpleEventEmitter + * - 移除 IMessageTransport 跨线程广播 + * - broadcast 退化为与 emit 相同的本地事件触发 */ class GlobalEventManager { + /** * 监听指定类型的事件(类型安全版本) * @param event 事件名称 @@ -92,9 +149,14 @@ class GlobalEventManager { } /** - * 跨进程广播,传的参数需要能被序列化 - * @param event 事件名称 - * @param args 事件参数 + * 广播事件 + * + * Fetch 方案改造: + * - 移除 process.send()(原实现:发送到 Node 父进程) + * - 移除 IMessageTransport(Worker 方案的产物,Fetch 方案不需要) + * - broadcast 退化为与 emit 相同的本地事件触发 + * + * 如需向服务端推送事件,应由调用方主动 fetch 接口 */ broadcast>( event: keyof TEvents, @@ -102,15 +164,7 @@ class GlobalEventManager { ): void; broadcast(event: string, ...args: any[]): void; broadcast(event: any, ...args: any[]): void { - const message = { - type: SceneProcessEventTag, - event: event as string, - args: [...args] - }; globalEventEmitter.emit(event, ...args); - if ('connected' in process) { - process.send?.(message); - } } /** diff --git a/src/core/scene/scene-process/service/engine.ts b/src/core/scene/scene-process/service/engine.ts index a0e66ee93..15b4de0d4 100644 --- a/src/core/scene/scene-process/service/engine.ts +++ b/src/core/scene/scene-process/service/engine.ts @@ -14,7 +14,7 @@ const tickTime = 1000 / 60; */ @register('Engine') export class EngineService extends BaseService implements IEngineService { - private _setTimeoutId: NodeJS.Timeout | null = null; + private _setTimeoutId: ReturnType | null = null; private _rafId: number | null = null; private _maxDeltaTimeInEM = 1 / 30; private _stateRecord = 0; // 记录当前状态 diff --git a/src/core/scene/scene-process/service/script.ts b/src/core/scene/scene-process/service/script.ts index aa04fdddb..0ff93e693 100644 --- a/src/core/scene/scene-process/service/script.ts +++ b/src/core/scene/scene-process/service/script.ts @@ -166,9 +166,14 @@ export class ScriptService extends BaseService implements IScript importExceptionHandler: (...args) => this._handleImportException(...args), cceModuleMap, }); - // eslint-disable-next-line no-undef - globalThis.self = window; - this._executor.addPolyfillFile(require.resolve('@cocos/build-polyfills/prebuilt/editor/bundle')); + // Web 改造: + // - 移除 globalThis.self = window(Web 环境中 self 已自动指向 window 或 WorkerGlobalScope) + // - require.resolve 替换为构建时注入的常量或动态路径 + // 若使用 bundler(如 vite/webpack),可通过 define 插件注入 __POLYFILL_BUNDLE_URL__ + const polyfillUrl = typeof __POLYFILL_BUNDLE_URL__ !== 'undefined' + ? __POLYFILL_BUNDLE_URL__ + : '@cocos/build-polyfills/prebuilt/editor/bundle'; + this._executor.addPolyfillFile(polyfillUrl); // 同步插件脚本列表 await this._syncPluginScripts.nextIteration(); // 重载项目与插件脚本