diff --git a/android/dimina/src/main/kotlin/com/didi/dimina/Dimina.kt b/android/dimina/src/main/kotlin/com/didi/dimina/Dimina.kt index cd4fe886..174f50da 100644 --- a/android/dimina/src/main/kotlin/com/didi/dimina/Dimina.kt +++ b/android/dimina/src/main/kotlin/com/didi/dimina/Dimina.kt @@ -43,15 +43,22 @@ class Dimina private constructor(context: Context) { // 配置类 class DiminaConfig private constructor(builder: Builder) { val debugMode: Boolean = builder.debugMode + val apiNamespaces: List = builder.apiNamespaces class Builder { var debugMode: Boolean = false + internal var apiNamespaces: MutableList = mutableListOf() fun setDebugMode(debugMode: Boolean): Builder { this.debugMode = debugMode return this } + fun addApiNamespace(name: String): Builder { + apiNamespaces.add(name) + return this + } + fun build(): DiminaConfig { return DiminaConfig(this) } @@ -66,6 +73,8 @@ class Dimina private constructor(context: Context) { return config.debugMode } + fun getApiNamespaces(): List = config.apiNamespaces + private val appContext: Context = context private lateinit var config: DiminaConfig diff --git a/android/dimina/src/main/kotlin/com/didi/dimina/core/JsCore.kt b/android/dimina/src/main/kotlin/com/didi/dimina/core/JsCore.kt index d5e5ca49..85582797 100644 --- a/android/dimina/src/main/kotlin/com/didi/dimina/core/JsCore.kt +++ b/android/dimina/src/main/kotlin/com/didi/dimina/core/JsCore.kt @@ -43,6 +43,13 @@ class JsCore { * @param scriptPath The JavaScript code to evaluate * @return The result of the evaluation */ + fun evaluate(script: String): JSValue { + if (!isInitialized()) { + return JSValue.createError("Engine not initialized") + } + return jsEngine.evaluate(script) + } + fun evaluateFromFile(scriptPath: String): JSValue { if (!isInitialized()) { return JSValue.createError("Engine not initialized") diff --git a/android/dimina/src/main/kotlin/com/didi/dimina/core/MiniApp.kt b/android/dimina/src/main/kotlin/com/didi/dimina/core/MiniApp.kt index 3b766883..201cca54 100644 --- a/android/dimina/src/main/kotlin/com/didi/dimina/core/MiniApp.kt +++ b/android/dimina/src/main/kotlin/com/didi/dimina/core/MiniApp.kt @@ -5,6 +5,7 @@ import android.content.Context import com.didi.dimina.Dimina import com.didi.dimina.api.ApiRegistry import com.didi.dimina.api.AsyncResult +import com.didi.dimina.api.BaseApiHandler import com.didi.dimina.api.SyncResult import com.didi.dimina.api.base.AppEventApi import com.didi.dimina.api.base.BaseAPI @@ -133,6 +134,13 @@ class MiniApp private constructor() { } else { LogUtils.d(tag, "Skipping JSSDK update check, last check was recent") } + // Inject custom API namespaces before loading service.js + val namespaces = Dimina.getInstance().getApiNamespaces() + if (namespaces.isNotEmpty()) { + val json = namespaces.joinToString(",") { "\"$it\"" } + evaluate("globalThis.__diminaApiNamespaces = [$json]") + } + evaluateFromFile( File( context.filesDir.absolutePath, @@ -352,6 +360,10 @@ class MiniApp private constructor() { LogUtils.d(tag, "Cleared all Bridge lists") } + fun registerApi(handler: BaseApiHandler) { + handler.registerWith(apiRegistry) + } + fun destroy() { // Clear API resources apiRegistry.clear() diff --git a/fe/packages/container/src/core/jscore.js b/fe/packages/container/src/core/jscore.js index 0d737d9c..d8b27c81 100644 --- a/fe/packages/container/src/core/jscore.js +++ b/fe/packages/container/src/core/jscore.js @@ -12,7 +12,9 @@ export class JSCore { // 使用 Web Worker 创建逻辑线程 // 使用下面的形式会使 hash 失效 // this.worker = new Worker(new URL('@dimina/service', import.meta.url)); - this.worker = new Worker(serviceURL, { type: 'classic' }) + const namespaces = this.parent.getApiNamespaces?.() || [] + const workerName = JSON.stringify({ apiNamespaces: namespaces }) + this.worker = new Worker(serviceURL, { type: 'classic', name: workerName }) // 监听逻辑线程的消息 this.worker.onmessage = (e) => { diff --git a/fe/packages/service/__tests__/env.spec.js b/fe/packages/service/__tests__/env.spec.js new file mode 100644 index 00000000..906460b1 --- /dev/null +++ b/fe/packages/service/__tests__/env.spec.js @@ -0,0 +1,87 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +// Mock all heavy dependencies — we only care about the namespace logic +const mockGlobalApi = { __mock: true } + +vi.mock('@dimina/common', () => ({ + modDefine: vi.fn(), + modRequire: vi.fn(), +})) +vi.mock('../src/api', () => ({ default: mockGlobalApi })) +vi.mock('../src/instance/component/component-module', () => ({ ComponentModule: { type: 'component' } })) +vi.mock('../src/instance/page/page-module', () => ({ PageModule: { type: 'page' } })) +vi.mock('../src/core/loader', () => ({ default: { createAppModule: vi.fn(), createModule: vi.fn() } })) +vi.mock('../src/core/router', () => ({ default: { stack: vi.fn(() => []) } })) +vi.mock('../src/core/runtime', () => ({ default: { app: null } })) + +describe('env.js API namespace registration', () => { + beforeEach(() => { + // Clean up namespace-related globals before each test + delete globalThis.__diminaApiNamespaces + delete globalThis.qd + delete globalThis.myapp + delete globalThis.dd + delete globalThis.wx + // Simulate Worker's self (not available in Node) + globalThis.self = { name: '' } + }) + + afterEach(() => { + vi.resetModules() + }) + + it('should set dd and wx to globalApi', async () => { + await import('../src/core/env.js') + + expect(globalThis.dd).toBe(mockGlobalApi) + expect(globalThis.wx).toBe(mockGlobalApi) + }) + + it('should register namespaces from __diminaApiNamespaces', async () => { + globalThis.__diminaApiNamespaces = ['qd', 'myapp'] + + await import('../src/core/env.js') + + expect(globalThis.qd).toBe(mockGlobalApi) + expect(globalThis.myapp).toBe(mockGlobalApi) + // dd and wx should still work + expect(globalThis.dd).toBe(mockGlobalApi) + expect(globalThis.wx).toBe(mockGlobalApi) + }) + + it('should not create extra globals when __diminaApiNamespaces is empty', async () => { + globalThis.__diminaApiNamespaces = [] + + await import('../src/core/env.js') + + expect(globalThis.qd).toBeUndefined() + }) + + it('should fall back to self.name when __diminaApiNamespaces is not set', async () => { + globalThis.name = JSON.stringify({ apiNamespaces: ['qd'] }) + + await import('../src/core/env.js') + + expect(globalThis.qd).toBe(mockGlobalApi) + }) + + it('should ignore invalid JSON in globalThis.name', async () => { + globalThis.name = 'not-json' + + await import('../src/core/env.js') + + expect(globalThis.qd).toBeUndefined() + // Core globals should still work + expect(globalThis.wx).toBe(mockGlobalApi) + }) + + it('should prefer __diminaApiNamespaces over self.name', async () => { + globalThis.__diminaApiNamespaces = ['qd'] + globalThis.name = JSON.stringify({ apiNamespaces: ['myapp'] }) + + await import('../src/core/env.js') + + expect(globalThis.qd).toBe(mockGlobalApi) + expect(globalThis.myapp).toBeUndefined() + }) +}) diff --git a/fe/packages/service/src/core/env.js b/fe/packages/service/src/core/env.js index 12274b8c..ccbff6f7 100644 --- a/fe/packages/service/src/core/env.js +++ b/fe/packages/service/src/core/env.js @@ -12,7 +12,17 @@ class Env { } init() { - globalThis.dd = globalThis.wx = globalApi + // Register API namespaces (dd, wx are built-in; custom ones from config) + let customNamespaces = globalThis.__diminaApiNamespaces || [] + if (customNamespaces.length === 0 && globalThis.name) { + try { + const config = JSON.parse(globalThis.name) + customNamespaces = config.apiNamespaces || [] + } catch (e) {} + } + for (const name of ['dd', 'wx', ...customNamespaces]) { + globalThis[name] = globalApi + } globalThis.modRequire = modRequire globalThis.modDefine = modDefine globalThis.global = {} diff --git a/harmony/dimina/src/main/ets/DApp/DMPApp.ets b/harmony/dimina/src/main/ets/DApp/DMPApp.ets index e5a023ba..f51305dd 100644 --- a/harmony/dimina/src/main/ets/DApp/DMPApp.ets +++ b/harmony/dimina/src/main/ets/DApp/DMPApp.ets @@ -29,7 +29,6 @@ import { DMPNavigatorManager } from '../Navigator/DMPNavigatorManager' import { DRouter } from '../Navigator/DRouter' import { DMPAppLifecycle } from './DMPAppLifecycle' import { DMPDeviceUtil } from '../Utils/DMPDeviceUtils' - export type DMPBundleUpdateCallback = () => void; //小程序应用实例 export class DMPApp { @@ -39,6 +38,7 @@ export class DMPApp { private onUpdateResult?: DMPBundleUpdateCallback private _container: DMPContainer = new DMPContainer(this) private static _context: common.UIAbilityContext + private static _apiNamespaces: string[] = [] private _containerBridges: DMPBridges = new DMPBridges() private static _entryContext: DMPEntryContext private _appModuleManager: DMPAppModuleManager = new DMPAppModuleManager(this) @@ -122,9 +122,10 @@ export class DMPApp { return DMPApp._entryContext.getWindowStage(); } - static init(context: DMPEntryContext) { + static init(context: DMPEntryContext, options?: { apiNamespaces?: string[] }) { DMPApp._entryContext = context; DMPApp._context = DMPApp._entryContext.getContext(); + DMPApp._apiNamespaces = options?.apiNamespaces ?? []; ImageKnife.with(DMPApp._entryContext.getContext()) context.getWindowStage().on('windowStageEvent', DMPAppLifecycle.onWindowStageEvent); DMPDeviceUtil.prepareSafeAreaAndDisplayWH(DMPApp._entryContext.getContext()) @@ -354,6 +355,12 @@ export class DMPApp { private async startEngineService() { DMPLogger.d(Tags.LAUNCH, "startEngineService start"); + // Inject custom API namespaces before loading service.js + const namespaces = DMPApp._apiNamespaces + if (namespaces.length > 0) { + const json = namespaces.map((n: string) => `"${n}"`).join(",") + this._service.executeScript(`globalThis.__diminaApiNamespaces = [${json}]`) + } const serviceJsPath = await this._bundleManager.requestServiceJsUri(); await this._service.loadFileUri(serviceJsPath); DMPLogger.i(Tags.LAUNCH, "startEngineService end"); diff --git a/iOS/dimina/DiminaKit/App/DMPApp.swift b/iOS/dimina/DiminaKit/App/DMPApp.swift index a8ae616d..0977dff9 100644 --- a/iOS/dimina/DiminaKit/App/DMPApp.swift +++ b/iOS/dimina/DiminaKit/App/DMPApp.swift @@ -107,6 +107,12 @@ public class DMPApp { public func loadBundle() async { print("loadBundle") + // Inject custom API namespaces before loading service.js + let namespaces = DMPAppManager.sharedInstance().apiNamespaces + if !namespaces.isEmpty { + let json = namespaces.map { "\"\($0)\"" }.joined(separator: ",") + await service?.evaluateScript("globalThis.__diminaApiNamespaces = [\(json)]") + } await service?.loadFile(path: DMPSandboxManager.sdkServicePath()) await service?.loadFile(path: DMPSandboxManager.appServicePath(appId: appId)) diff --git a/iOS/dimina/DiminaKit/App/DMPAppManager.swift b/iOS/dimina/DiminaKit/App/DMPAppManager.swift index 7e58c143..479d04ae 100644 --- a/iOS/dimina/DiminaKit/App/DMPAppManager.swift +++ b/iOS/dimina/DiminaKit/App/DMPAppManager.swift @@ -7,15 +7,20 @@ public class DMPAppManager { private static let instance = DMPAppManager() - + private var appPools: [Int: DMPApp] = [:] private var appIndex: Int = 0 - + public private(set) var apiNamespaces: [String] = [] + private init() {} - + public static func sharedInstance() -> DMPAppManager { return instance } + + public func setup(apiNamespaces: [String] = []) { + self.apiNamespaces = apiNamespaces + } func getApp(appIndex: Int) -> DMPApp? { return appPools[appIndex]