From 94163f6b73c0b52367209e6bf8739f780b898302 Mon Sep 17 00:00:00 2001 From: "fenge\\flying" Date: Mon, 13 Jul 2026 11:48:56 +0800 Subject: [PATCH] refactor(scene): expose scene services via the lib interface, drop browser globals Scene editor and preview pages reached the scene services through ad-hoc browser globals (window.cli.Scene / window.cli.SceneEvents) and read the server address from another global (window.WebEnv). That is not a real interface, leaks globals, and cannot be shared with external IDEs that consume the src/lib layer. src/lib/cli.ts only re-exported types and had no importers. window.cli.SceneEvents was also wired to messageManager (an isolated emitter), while service events such as editor:open are emitted on ServiceEvents (a GlobalEventManager); listeners on the former never fired. ServiceManager (single typed entry): - Add init(serverURL), getServices(): IServiceManager and getServiceEvents(): GlobalEventManager. getServiceEvents() returns ServiceEvents, where service events are actually emitted (fixes the dead editor:open listener). Public lib interface: - Delete the unused src/lib/cli.ts. - Re-export Services (IServiceManager), IPublicServiceManager and GlobalEventManager from src/lib/scene/scene.ts. Drop browser globals, pass the address through the boot interface: - engine-bootstrap.ts: remove the globalThis.cli assignment; startup() calls serviceManager.init(serverURL). - scene-editor-boot.js / game-boot.js: boot({ ip, port, https }) composes the server URL (falls back to location.origin) and returns { services, events, serverURL } from serviceManager.getServices() / getServiceEvents(). - engine-loader.js: takes serverURL as an argument (new composeServerURL) instead of reading window.WebEnv; editor-stub-preload.js becomes initEditorStub(serverURL). - load-scene.js / preview-app.js: consume the boot context instead of window.cli. - scene-editor.ejs / preview.ejs / game.ejs: call boot({ ip, port, https }); the scene editor page uses the returned context via window.SceneCtx. Removed window.WebEnv. - scene.scripting.middleware.ts / game-preview.middleware.ts: inject ip / port / https into the templates instead of a combined serverURL. - main.ts (native scene worker): use serviceManager.init(...); the server URL still arrives via the --serverURL fork argument. The engine-required window.CC_EDITOR / window.CC_PREVIEW flags and globalThis.cce.Script are intentionally kept. --- src/core/preview/game-preview.middleware.ts | 7 +- .../scene/scene-process/engine-bootstrap.ts | 6 +- src/core/scene/scene-process/main.ts | 2 +- .../scene/scene-process/service/engine.ts | 2 +- .../scene-process/service/service-manager.ts | 26 ++++++- src/core/scene/scene.scripting.middleware.ts | 14 +++- src/lib/cli.ts | 9 --- src/lib/scene/scene.ts | 13 ++++ static/web/editor-stub-preload.js | 14 +++- static/web/engine-loader.js | 24 ++++-- static/web/game-boot.js | 7 +- static/web/game.ejs | 3 +- static/web/load-scene.js | 18 +++-- static/web/preview-app.js | 29 ++++--- static/web/preview.ejs | 7 +- static/web/scene-editor-boot.js | 25 ++++-- static/web/scene-editor.ejs | 76 +++++++++---------- 17 files changed, 177 insertions(+), 105 deletions(-) delete mode 100644 src/lib/cli.ts diff --git a/src/core/preview/game-preview.middleware.ts b/src/core/preview/game-preview.middleware.ts index 6182718bb..72456fdd1 100644 --- a/src/core/preview/game-preview.middleware.ts +++ b/src/core/preview/game-preview.middleware.ts @@ -167,12 +167,15 @@ export default { async handler(req: Request, res: Response, next: NextFunction) { try { const { default: scripting } = await import('../../core/scripting'); - const serverBaseUrl = `${req.protocol}://${req.get('host')}`; + const host = req.get('host') || ''; + const colon = host.lastIndexOf(':'); const scene = typeof req.query.scene === 'string' ? req.query.scene : ''; const sceneQuery = scene ? `?scene=${encodeURIComponent(scene)}` : ''; const renderData = { title: `Cocos Creator - ${basename(scripting.projectPath)}`, - serverURL: serverBaseUrl, + ip: colon >= 0 ? host.slice(0, colon) : host, + port: colon >= 0 ? host.slice(colon + 1) : '', + https: req.protocol === 'https', settingsJs: `/preview/settings.js${sceneQuery}`, sceneQuery, }; diff --git a/src/core/scene/scene-process/engine-bootstrap.ts b/src/core/scene/scene-process/engine-bootstrap.ts index 38421ebe8..6a231155c 100644 --- a/src/core/scene/scene-process/engine-bootstrap.ts +++ b/src/core/scene/scene-process/engine-bootstrap.ts @@ -2,7 +2,6 @@ import * as EditorExtends from '../../engine/editor-extends'; import { Rpc } from './rpc'; import { serviceManager } from './service/service-manager'; import { Service as DecoratorService } from './service/core/decorator'; -import { messageManager } from './service/message'; import { initLocalI18n } from './i18n'; import './service'; @@ -30,7 +29,7 @@ export async function startup(options: { const features = (await modules.json()) as string[]; const { serverURL } = options; - serviceManager.initialize(serverURL); + serviceManager.init(serverURL); const requiredModules = [ 'cc', @@ -84,9 +83,6 @@ export async function startup(options: { (globalThis as any).cce = (globalThis as any).cce || {}; (globalThis as any).cce.Script = DecoratorService.Script; - (globalThis as any).cli = {}; - (globalThis as any).cli.Scene = DecoratorService; - (globalThis as any).cli.SceneEvents = messageManager; if (EditorExtends.init) { await EditorExtends.init(); diff --git a/src/core/scene/scene-process/main.ts b/src/core/scene/scene-process/main.ts index a4d128672..f341a4401 100644 --- a/src/core/scene/scene-process/main.ts +++ b/src/core/scene/scene-process/main.ts @@ -30,7 +30,7 @@ async function startup() { } // 初始化 service-manager - serviceManager.initialize(serverURL ?? ''); + serviceManager.init(serverURL ?? ''); await Engine.init(enginePath); // 这里 importBase 与 nativeBase 用服务器是为了让服务器转换资源真实存放的路径 diff --git a/src/core/scene/scene-process/service/engine.ts b/src/core/scene/scene-process/service/engine.ts index 6735e8811..4ac37514b 100644 --- a/src/core/scene/scene-process/service/engine.ts +++ b/src/core/scene/scene-process/service/engine.ts @@ -93,7 +93,7 @@ export class EngineService extends BaseService implements IEngine * 渲染调试视图(DebugView):单一通道调试 / 组合光照项开关 / 纯光照带固有色 / 级联阴影染色。 * 与 cocos-editor scene-facade-manager.changeDebugOption 对齐。 * 注意:不对外暴露为公共 API(未加入 IEngineService / EngineProxy,不生成到 cocos-cli-types); - * 目前仅由场景编辑器页面(scene-editor.ejs)在浏览器内通过 window.cli.Scene.Engine 直接调用。 + * 目前仅由场景编辑器页面(scene-editor.ejs)在浏览器内通过 serviceManager.getServices().Engine 直接调用。 * @param key 'single' | 'composite' | 'LIGHTING_WITH_BASE_COLOR' | 'CSM_LAYER_COLORATION' * @param value single: DebugViewSingleType 数值;composite: { key: DebugViewCompositeType | 10000(=ALL), value: boolean };其余: boolean */ diff --git a/src/core/scene/scene-process/service/service-manager.ts b/src/core/scene/scene-process/service/service-manager.ts index 6cf5e31a4..41f691873 100644 --- a/src/core/scene/scene-process/service/service-manager.ts +++ b/src/core/scene/scene-process/service/service-manager.ts @@ -1,7 +1,9 @@ -import { getServiceAll, IServiceEvents, ServiceEvents } from './core'; +import { getServiceAll, IServiceEvents, ServiceEvents, Service } from './core'; import { InternalServiceEvents } from './core/internal-events'; import { IEditorEvents, INodeEvents, IComponentEvents, IScriptEvents, IAssetEvents, ISelectionEvents } from '../../common'; import { messageManager } from './message'; +import type { IServiceManager } from './interfaces'; +import type { GlobalEventManager } from './core/global-events'; type AllEvents = IEditorEvents & INodeEvents & IComponentEvents & IScriptEvents & IAssetEvents & ISelectionEvents; @@ -99,10 +101,32 @@ export class ServiceManager { this.registerAutoForwardEvents(); } + /** Alias of {@link initialize}; the public entry used by callers/boot. */ + init(serverUrl: string) { + this.initialize(serverUrl); + } + getServerUrl() { return this.serverUrl; } + /** + * The typed service map (Engine / Editor / Camera / Preview / ...). + * Both cli's own browser pages and external IDEs go through this instead of + * the former `window.cli.Scene` global. + */ + getServices(): IServiceManager { + return Service; + } + + /** + * The global service event bus where service events (e.g. `editor:open`) + * are actually emitted (see base-service emit -> ServiceEvents). + */ + getServiceEvents(): GlobalEventManager { + return ServiceEvents; + } + /** * Camera/Gizmo 依赖的编辑器内置 effect UUID */ diff --git a/src/core/scene/scene.scripting.middleware.ts b/src/core/scene/scene.scripting.middleware.ts index 076b77c70..c94ab64ae 100644 --- a/src/core/scene/scene.scripting.middleware.ts +++ b/src/core/scene/scene.scripting.middleware.ts @@ -18,10 +18,13 @@ export default { return res.redirect(302, '/scene-editor/'); } const { default: scripting } = await import('../../core/scripting'); - const serverBaseUrl = `${req.protocol}://${req.get('host')}`; + const host = req.get('host') || ''; + const colon = host.lastIndexOf(':'); const renderData = { title: `Cocos Creator Preview - ${basename(scripting.projectPath)}`, - serverURL: serverBaseUrl + ip: colon >= 0 ? host.slice(0, colon) : host, + port: colon >= 0 ? host.slice(colon + 1) : '', + https: req.protocol === 'https', }; const templatePath = join(GlobalPaths.workspace, 'static', 'web', 'scene-editor.ejs'); const html = await ejs.renderFile(templatePath, renderData); @@ -36,10 +39,13 @@ export default { async handler(req: Request, res: Response, next: NextFunction) { try { const { default: scripting } = await import('../../core/scripting'); - const serverBaseUrl = `${req.protocol}://${req.get('host')}`; + const host = req.get('host') || ''; + const colon = host.lastIndexOf(':'); const renderData = { title: `Resource Preview - ${basename(scripting.projectPath)}`, - serverURL: serverBaseUrl + ip: colon >= 0 ? host.slice(0, colon) : host, + port: colon >= 0 ? host.slice(colon + 1) : '', + https: req.protocol === 'https', }; const templatePath = join(GlobalPaths.workspace, 'static', 'web', 'preview.ejs'); const html = await ejs.renderFile(templatePath, renderData); diff --git a/src/lib/cli.ts b/src/lib/cli.ts deleted file mode 100644 index ae8dcf536..000000000 --- a/src/lib/cli.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { IServiceManager } from '../core/scene/scene-process/service/interfaces'; -import type { GlobalEventManager } from '../core/scene/scene-process/service/core/global-events'; - -export interface ICLI { - Scene: IServiceManager; - SceneEvents: GlobalEventManager; -} - -export type { IServiceManager, GlobalEventManager }; diff --git a/src/lib/scene/scene.ts b/src/lib/scene/scene.ts index 338843cd3..cc57b8c07 100644 --- a/src/lib/scene/scene.ts +++ b/src/lib/scene/scene.ts @@ -1,6 +1,19 @@ import { init as sceneInit } from '../../core/scene'; import { GlobalPaths } from '../../global'; +/** + * Public scene service interface, shared by external IDEs and cli's own browser + * pages (scene editor / preview). `Services` is the typed service map + * (Engine / Editor / Camera / Preview / ...); obtain the live instance at + * runtime via `serviceManager.getServices()`. `GlobalEventManager` is the + * service event bus, obtained via `serviceManager.getServiceEvents()`. + */ +export type { + IServiceManager as Services, + IPublicServiceManager, +} from '../../core/scene/scene-process/service/interfaces'; +export type { GlobalEventManager } from '../../core/scene/scene-process/service/core/global-events'; + /** * Initialize the scene module. * Registers the scene middleware and initializes scene config. diff --git a/static/web/editor-stub-preload.js b/static/web/editor-stub-preload.js index 1a6a5a637..766779cab 100644 --- a/static/web/editor-stub-preload.js +++ b/static/web/editor-stub-preload.js @@ -1,9 +1,16 @@ /* global window */ -window.CC_EDITOR = true; -const serverUrl = window.WebEnv.serverURL; +/** + * Install the editor stub (window.Editor + fs/require mocks) used by the engine + * editor modules in the browser. The server address is passed in explicitly + * instead of read from a global. + * @param {string} serverURL + */ +export function initEditorStub(serverURL) { + window.CC_EDITOR = true; + const serverUrl = serverURL; -window.Editor = { + window.Editor = { Message: { request: async function (target, method, uuid) { if (method === 'query-asset-info') { @@ -104,4 +111,5 @@ if (typeof window.require === 'undefined') { throw new Error('Module ' + name + ' not found in editor-stub-preload require mock'); }; window.require.cache = {}; + } } diff --git a/static/web/engine-loader.js b/static/web/engine-loader.js index f1492a946..ee92cd2c4 100644 --- a/static/web/engine-loader.js +++ b/static/web/engine-loader.js @@ -1,5 +1,17 @@ /* global window, document, System, fetch */ +/** + * 由 ip / port 组装服务地址;未提供 ip 时回退到当前页面 origin。 + * @param {{ ip?: string, port?: number|string, https?: boolean }} [addr] + * @returns {string} + */ +export function composeServerURL(addr = {}) { + const { ip, port, https } = addr; + if (!ip) return window.location.origin; + const protocol = https ? 'https' : 'http'; + return port ? `${protocol}://${ip}:${port}` : `${protocol}://${ip}`; +} + /** * 引擎加载公共流程。 * @@ -8,12 +20,13 @@ * 预览以 PREVIEW 模式(EDITOR=false / PREVIEW=true)加载,场景编辑器以默认编辑器模式加载。 * 加载完成后各调用方自行 System.import('cc') 跑游戏,或 import scene-bundle 启服务。 * + * @param {string} serverURL 服务地址(http[s]://ip:port),由调用方(boot)传入。 * @param {{ preview?: boolean }} [options] preview=true 时以 PREVIEW 模式加载引擎。 - * @returns {Promise} 填充后的 window.WebEnv(含 serverURL / enginePath 等)。 + * @returns {Promise} 加载环境(含 serverURL / enginePath 等)。 */ -export async function loadEngine(options = {}) { - const env = window.WebEnv; - const envRes = await fetch(`${env.serverURL}/scripting/web-env`); +export async function loadEngine(serverURL, options = {}) { + const env = { serverURL }; + const envRes = await fetch(`${serverURL}/scripting/web-env`); Object.assign(env, await envRes.json()); await import('/static/web/polyfills.bundle.js'); @@ -41,7 +54,8 @@ export async function loadEngine(options = {}) { return fetch(url).then((response) => response.json()).then((json) => ({ json, url: url.href })); }); - await import('/static/web/editor-stub-preload.js'); + const { initEditorStub } = await import('/static/web/editor-stub-preload.js'); + initEditorStub(serverURL); if (options.preview) { // 游戏预览必须以 PREVIEW 模式运行,而不是编辑器编辑模式。 // editor-stub-preload 会设置 window.CC_EDITOR=true(场景编辑器预览需要), diff --git a/static/web/game-boot.js b/static/web/game-boot.js index eea805c28..b68f3fe2e 100644 --- a/static/web/game-boot.js +++ b/static/web/game-boot.js @@ -1,6 +1,6 @@ /* global window, document, System, globalThis, fetch, location */ -import { loadEngine } from '/static/web/engine-loader.js'; +import { loadEngine, composeServerURL } from '/static/web/engine-loader.js'; /** * 浏览器游戏预览运行时引导。 @@ -9,7 +9,7 @@ import { loadEngine } from '/static/web/engine-loader.js'; * 共用 engine-loader.js,区别在于这里以 PREVIEW 模式加载,并在结尾调用 cc.game.init(settings) * 运行启动场景,而不是加载场景编辑器 bundle。流程对齐编辑器 preview-app/src/main.ts。 */ -export default async function gameBoot() { +export default async function gameBoot(addr = {}) { const showError = (e) => { const el = document.getElementById('error'); if (el) { @@ -20,7 +20,8 @@ export default async function gameBoot() { try { // 以 PREVIEW 模式加载引擎(EDITOR=false / PREVIEW=true) - const env = await loadEngine({ preview: true }); + const serverURL = composeServerURL(addr); + const env = await loadEngine(serverURL, { preview: true }); const _originalSystem = System; const cc = await System.import('cc'); diff --git a/static/web/game.ejs b/static/web/game.ejs index 91a5a2caa..ae3765825 100644 --- a/static/web/game.ejs +++ b/static/web/game.ejs @@ -53,7 +53,6 @@
@@ -62,7 +61,7 @@ diff --git a/static/web/load-scene.js b/static/web/load-scene.js index 16d3849ed..ba7b96aeb 100644 --- a/static/web/load-scene.js +++ b/static/web/load-scene.js @@ -1,6 +1,14 @@ -/* global window, cc, fetch */ +/* global cc, fetch */ + +/** + * 加载并打开场景/预制体。 + * + * @param {{ services: object, events: object, serverURL: string }} ctx boot() 返回的场景服务上下文 + * @param {string} [urlOrUUID] 场景/预制体 uuid 或 db:// url;缺省时自动挑选第一个用户场景 + */ +export async function loadScene(ctx, urlOrUUID) { + const { services, events, serverURL } = ctx; -window.loadScene = async function (serverURL, urlOrUUID) { if (!urlOrUUID) { const sceneListPromise = await fetch(`${serverURL}/query-asset-infos/cc.SceneAsset`); const sceneList = await sceneListPromise.json(); @@ -20,8 +28,8 @@ window.loadScene = async function (serverURL, urlOrUUID) { return; } - cli.SceneEvents.on('editor:open', () => { + events.on('editor:open', () => { console.log('editor:open onCalled'); }); - await cli.Scene.Editor.open({ urlOrUUID }); -}; + await services.Editor.open({ urlOrUUID }); +} diff --git a/static/web/preview-app.js b/static/web/preview-app.js index b59e90835..0a829752f 100644 --- a/static/web/preview-app.js +++ b/static/web/preview-app.js @@ -1,17 +1,21 @@ /* global window, document, cc */ +// Scene services, provided by boot() via initPreviewApp(ctx). Replaces the +// former window.cli.Scene global. +let services = null; + function log(msg, level) { if (level === 'err') console.error('[Preview]', msg); else if (level === 'warn') console.warn('[Preview]', msg); else console.log('[Preview]', msg); } +function repaint() { + try { services && services.Engine && services.Engine.repaintInEditMode(); } catch (e) { /* ignore */ } +} + function getPreviewService() { - try { - return window.cli && window.cli.Scene && window.cli.Scene.Preview; - } catch (e) { - return null; - } + return services && services.Preview; } function getActive() { @@ -59,7 +63,7 @@ function switchPrimitive(type) { var active = getActive(); if (active && active.switchPrimitive) { active.switchPrimitive(type); - window.cli.Scene.Engine.repaintInEditMode(); + repaint(); log('Switched primitive: ' + type); } } @@ -70,7 +74,7 @@ function toggleLight() { var light = active.lightComp; var on = light ? !light.enabled : true; active.setLightEnable(on); - window.cli.Scene.Engine.repaintInEditMode(); + repaint(); log('Light: ' + (on ? 'ON' : 'OFF')); } @@ -78,7 +82,7 @@ function toggle2D3D() { var active = getActive(); if (active && active.viewToggle) { active.viewToggle(); - window.cli.Scene.Engine.repaintInEditMode(); + repaint(); log('Toggled 2D/3D view'); } } @@ -96,7 +100,7 @@ function bindPreviewMouseEvents(canvas) { if (!active) return; active.onMouseMove(e); if (active._isMouseDown) { - window.cli.Scene.Engine.repaintInEditMode(); + repaint(); } }); @@ -112,7 +116,7 @@ function bindPreviewMouseEvents(canvas) { active.onMouseWheel({ wheelDeltaY: -e.deltaY, }); - window.cli.Scene.Engine.repaintInEditMode(); + repaint(); }, { passive: false }); canvas.addEventListener('contextmenu', function(e) { @@ -122,7 +126,8 @@ function bindPreviewMouseEvents(canvas) { // ── Initialization ── -export default function initPreviewApp() { +export default function initPreviewApp(ctx) { + services = ctx && ctx.services; var status = document.getElementById('pvStatus'); var preview = getPreviewService(); @@ -133,7 +138,7 @@ export default function initPreviewApp() { } try { - window.cli.Scene.Engine.resume(); + services && services.Engine && services.Engine.resume(); } catch (e) { log('Engine resume failed: ' + e.message, 'warn'); } diff --git a/static/web/preview.ejs b/static/web/preview.ejs index 990d4cd30..496fbd148 100644 --- a/static/web/preview.ejs +++ b/static/web/preview.ejs @@ -99,14 +99,11 @@ Initializing... - diff --git a/static/web/scene-editor-boot.js b/static/web/scene-editor-boot.js index 04d6a1222..935f85ff4 100644 --- a/static/web/scene-editor-boot.js +++ b/static/web/scene-editor-boot.js @@ -1,31 +1,44 @@ /* global System, globalThis */ -import { loadEngine } from '/static/web/engine-loader.js'; +import { loadEngine, composeServerURL } from '/static/web/engine-loader.js'; /** - * 场景编辑器预览引导。 + * 场景编辑器 / 资源预览引导。 * * 引擎加载流程与浏览器游戏预览的 game-boot.js 共用 engine-loader.js;区别在于这里以默认 * 编辑器模式加载(不覆盖 CC_EDITOR/CC_PREVIEW),并在结尾加载 scene-bundle 启动场景服务, * 而不是运行游戏。 + * + * 服务地址通过入参传入(ip/port),不再依赖 window.WebEnv 全局。 + * + * @param {{ ip?: string, port?: number|string, https?: boolean }} [addr] + * @returns {Promise<{ services: object, events: object, serverURL: string } | null>} + * 场景服务上下文:`services` 为服务集(Engine/Editor/Camera/...),`events` 为服务事件总线。 */ -export default async function boot() { +export default async function boot(addr = {}) { try { - const env = await loadEngine(); + const serverURL = composeServerURL(addr); + const env = await loadEngine(serverURL); const _originalSystem = System; console.log('[Scene] loading scene bundle'); // SystemJS natively awaits the attached import maps above const SceneBundle = await System.import('/static/web/scene-bundle.js'); - const { startup } = SceneBundle; + const { startup, serviceManager } = SceneBundle; globalThis.System = _originalSystem; await startup({ enginePath: env.enginePath, - serverURL: env.serverURL, + serverURL, }); + + const services = serviceManager.getServices(); + const events = serviceManager.getServiceEvents(); + services?.Engine?.resume?.(); console.log('Cocos Engine and Scene Services loaded successfully'); + return { services, events, serverURL }; } catch (err) { console.error('Failed to load Cocos Engine or Services:', err.stack || err); + return null; } } diff --git a/static/web/scene-editor.ejs b/static/web/scene-editor.ejs index 405c82aff..6c3263ff7 100644 --- a/static/web/scene-editor.ejs +++ b/static/web/scene-editor.ejs @@ -405,17 +405,11 @@ -