Skip to content

Commit 91da209

Browse files
authored
feat: custom global API namespace (didi#193)
* feat: support custom global API namespace (e.g. qd.xxx) Add a mechanism for host apps to register custom API namespaces so that mini-programs can call qd.xxx() equivalently to wx.xxx(). - JS: env.js reads __diminaApiNamespaces and assigns globalApi to each name, with H5 fallback via Worker name - Android: DiminaConfig.addApiNamespace(), JsCore.evaluate(), MiniApp injects namespaces before service.js and exposes registerApi() - iOS: new DiminaConfig singleton, DMPApp injects namespaces in loadBundle() - HarmonyOS: new DiminaConfig singleton, DMPApp injects in startEngineService() - H5: jscore.js passes namespaces via Worker name property * test(service): add unit tests for env.js API namespace registration * refactor: remove standalone DiminaConfig, attach apiNamespaces to existing SDK entry points - iOS: add apiNamespaces to DMPAppManager with setup() method - HarmonyOS: add optional apiNamespaces param to DMPApp.init() - JS: merge dd/wx and custom namespace registration into one loop - Delete DiminaConfig.swift and DiminaConfig.ets * fix(service): replace self with globalThis to pass oxlint
1 parent 466a0e8 commit 91da209

9 files changed

Lines changed: 152 additions & 7 deletions

File tree

android/dimina/src/main/kotlin/com/didi/dimina/Dimina.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,22 @@ class Dimina private constructor(context: Context) {
4444
// 配置类
4545
class DiminaConfig private constructor(builder: Builder) {
4646
val debugMode: Boolean = builder.debugMode
47+
val apiNamespaces: List<String> = builder.apiNamespaces
4748

4849
class Builder {
4950
var debugMode: Boolean = false
51+
internal var apiNamespaces: MutableList<String> = mutableListOf()
5052

5153
fun setDebugMode(debugMode: Boolean): Builder {
5254
this.debugMode = debugMode
5355
return this
5456
}
5557

58+
fun addApiNamespace(name: String): Builder {
59+
apiNamespaces.add(name)
60+
return this
61+
}
62+
5663
fun build(): DiminaConfig {
5764
return DiminaConfig(this)
5865
}
@@ -67,6 +74,8 @@ class Dimina private constructor(context: Context) {
6774
return config.debugMode
6875
}
6976

77+
fun getApiNamespaces(): List<String> = config.apiNamespaces
78+
7079
private val appContext: Context = context
7180
private lateinit var config: DiminaConfig
7281

android/dimina/src/main/kotlin/com/didi/dimina/core/JsCore.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ class JsCore {
4343
* @param scriptPath The JavaScript code to evaluate
4444
* @return The result of the evaluation
4545
*/
46+
fun evaluate(script: String): JSValue {
47+
if (!isInitialized()) {
48+
return JSValue.createError("Engine not initialized")
49+
}
50+
return jsEngine.evaluate(script)
51+
}
52+
4653
fun evaluateFromFile(scriptPath: String): JSValue {
4754
if (!isInitialized()) {
4855
return JSValue.createError("Engine not initialized")

android/dimina/src/main/kotlin/com/didi/dimina/core/MiniApp.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import android.content.Context
55
import com.didi.dimina.Dimina
66
import com.didi.dimina.api.ApiRegistry
77
import com.didi.dimina.api.AsyncResult
8+
import com.didi.dimina.api.BaseApiHandler
89
import com.didi.dimina.api.SyncResult
910
import com.didi.dimina.api.ext.ExtModuleHandler
1011
import com.didi.dimina.api.base.AppEventApi
@@ -134,6 +135,13 @@ class MiniApp private constructor() {
134135
} else {
135136
LogUtils.d(tag, "Skipping JSSDK update check, last check was recent")
136137
}
138+
// Inject custom API namespaces before loading service.js
139+
val namespaces = Dimina.getInstance().getApiNamespaces()
140+
if (namespaces.isNotEmpty()) {
141+
val json = namespaces.joinToString(",") { "\"$it\"" }
142+
evaluate("globalThis.__diminaApiNamespaces = [$json]")
143+
}
144+
137145
evaluateFromFile(
138146
File(
139147
context.filesDir.absolutePath,
@@ -369,6 +377,10 @@ class MiniApp private constructor() {
369377
LogUtils.d(tag, "Cleared all Bridge lists")
370378
}
371379

380+
fun registerApi(handler: BaseApiHandler) {
381+
handler.registerWith(apiRegistry)
382+
}
383+
372384
fun destroy() {
373385
// Clear API resources
374386
apiRegistry.clear()

fe/packages/container/src/core/jscore.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ export class JSCore {
1212
// 使用 Web Worker 创建逻辑线程
1313
// 使用下面的形式会使 hash 失效
1414
// this.worker = new Worker(new URL('@dimina/service', import.meta.url));
15-
this.worker = new Worker(serviceURL, { type: 'classic' })
15+
const namespaces = this.parent.getApiNamespaces?.() || []
16+
const workerName = JSON.stringify({ apiNamespaces: namespaces })
17+
this.worker = new Worker(serviceURL, { type: 'classic', name: workerName })
1618

1719
// 监听逻辑线程的消息
1820
this.worker.onmessage = (e) => {
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2+
3+
// Mock all heavy dependencies — we only care about the namespace logic
4+
const mockGlobalApi = { __mock: true }
5+
6+
vi.mock('@dimina/common', () => ({
7+
modDefine: vi.fn(),
8+
modRequire: vi.fn(),
9+
}))
10+
vi.mock('../src/api', () => ({ default: mockGlobalApi }))
11+
vi.mock('../src/instance/component/component-module', () => ({ ComponentModule: { type: 'component' } }))
12+
vi.mock('../src/instance/page/page-module', () => ({ PageModule: { type: 'page' } }))
13+
vi.mock('../src/core/loader', () => ({ default: { createAppModule: vi.fn(), createModule: vi.fn() } }))
14+
vi.mock('../src/core/router', () => ({ default: { stack: vi.fn(() => []) } }))
15+
vi.mock('../src/core/runtime', () => ({ default: { app: null } }))
16+
17+
describe('env.js API namespace registration', () => {
18+
beforeEach(() => {
19+
// Clean up namespace-related globals before each test
20+
delete globalThis.__diminaApiNamespaces
21+
delete globalThis.qd
22+
delete globalThis.myapp
23+
delete globalThis.dd
24+
delete globalThis.wx
25+
// Simulate Worker's self (not available in Node)
26+
globalThis.self = { name: '' }
27+
})
28+
29+
afterEach(() => {
30+
vi.resetModules()
31+
})
32+
33+
it('should set dd and wx to globalApi', async () => {
34+
await import('../src/core/env.js')
35+
36+
expect(globalThis.dd).toBe(mockGlobalApi)
37+
expect(globalThis.wx).toBe(mockGlobalApi)
38+
})
39+
40+
it('should register namespaces from __diminaApiNamespaces', async () => {
41+
globalThis.__diminaApiNamespaces = ['qd', 'myapp']
42+
43+
await import('../src/core/env.js')
44+
45+
expect(globalThis.qd).toBe(mockGlobalApi)
46+
expect(globalThis.myapp).toBe(mockGlobalApi)
47+
// dd and wx should still work
48+
expect(globalThis.dd).toBe(mockGlobalApi)
49+
expect(globalThis.wx).toBe(mockGlobalApi)
50+
})
51+
52+
it('should not create extra globals when __diminaApiNamespaces is empty', async () => {
53+
globalThis.__diminaApiNamespaces = []
54+
55+
await import('../src/core/env.js')
56+
57+
expect(globalThis.qd).toBeUndefined()
58+
})
59+
60+
it('should fall back to self.name when __diminaApiNamespaces is not set', async () => {
61+
globalThis.name = JSON.stringify({ apiNamespaces: ['qd'] })
62+
63+
await import('../src/core/env.js')
64+
65+
expect(globalThis.qd).toBe(mockGlobalApi)
66+
})
67+
68+
it('should ignore invalid JSON in globalThis.name', async () => {
69+
globalThis.name = 'not-json'
70+
71+
await import('../src/core/env.js')
72+
73+
expect(globalThis.qd).toBeUndefined()
74+
// Core globals should still work
75+
expect(globalThis.wx).toBe(mockGlobalApi)
76+
})
77+
78+
it('should prefer __diminaApiNamespaces over self.name', async () => {
79+
globalThis.__diminaApiNamespaces = ['qd']
80+
globalThis.name = JSON.stringify({ apiNamespaces: ['myapp'] })
81+
82+
await import('../src/core/env.js')
83+
84+
expect(globalThis.qd).toBe(mockGlobalApi)
85+
expect(globalThis.myapp).toBeUndefined()
86+
})
87+
})

fe/packages/service/src/core/env.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,17 @@ class Env {
1212
}
1313

1414
init() {
15-
globalThis.dd = globalThis.wx = globalApi
15+
// Register API namespaces (dd, wx are built-in; custom ones from config)
16+
let customNamespaces = globalThis.__diminaApiNamespaces || []
17+
if (customNamespaces.length === 0 && globalThis.name) {
18+
try {
19+
const config = JSON.parse(globalThis.name)
20+
customNamespaces = config.apiNamespaces || []
21+
} catch (e) {}
22+
}
23+
for (const name of ['dd', 'wx', ...customNamespaces]) {
24+
globalThis[name] = globalApi
25+
}
1626
globalThis.modRequire = modRequire
1727
globalThis.modDefine = modDefine
1828
globalThis.global = {}

harmony/dimina/src/main/ets/DApp/DMPApp.ets

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import { DMPNavigatorManager } from '../Navigator/DMPNavigatorManager'
3030
import { DRouter } from '../Navigator/DRouter'
3131
import { DMPAppLifecycle } from './DMPAppLifecycle'
3232
import { DMPDeviceUtil } from '../Utils/DMPDeviceUtils'
33-
3433
export type DMPBundleUpdateCallback = () => void;
3534
//小程序应用实例
3635
export class DMPApp {
@@ -40,6 +39,7 @@ export class DMPApp {
4039
private onUpdateResult?: DMPBundleUpdateCallback
4140
private _container: DMPContainer = new DMPContainer(this)
4241
private static _context: common.UIAbilityContext
42+
private static _apiNamespaces: string[] = []
4343
private _containerBridges: DMPBridges = new DMPBridges()
4444
private static _entryContext: DMPEntryContext
4545
private _appModuleManager: DMPAppModuleManager = new DMPAppModuleManager(this)
@@ -123,9 +123,10 @@ export class DMPApp {
123123
return DMPApp._entryContext.getWindowStage();
124124
}
125125

126-
static init(context: DMPEntryContext) {
126+
static init(context: DMPEntryContext, options?: { apiNamespaces?: string[] }) {
127127
DMPApp._entryContext = context;
128128
DMPApp._context = DMPApp._entryContext.getContext();
129+
DMPApp._apiNamespaces = options?.apiNamespaces ?? [];
129130
ImageKnife.with(DMPApp._entryContext.getContext())
130131
context.getWindowStage().on('windowStageEvent', DMPAppLifecycle.onWindowStageEvent);
131132
DMPDeviceUtil.prepareSafeAreaAndDisplayWH(DMPApp._entryContext.getContext())
@@ -367,6 +368,12 @@ export class DMPApp {
367368

368369
private async startEngineService() {
369370
DMPLogger.d(Tags.LAUNCH, "startEngineService start");
371+
// Inject custom API namespaces before loading service.js
372+
const namespaces = DMPApp._apiNamespaces
373+
if (namespaces.length > 0) {
374+
const json = namespaces.map((n: string) => `"${n}"`).join(",")
375+
this._service.executeScript(`globalThis.__diminaApiNamespaces = [${json}]`)
376+
}
370377
const serviceJsPath = await this._bundleManager.requestServiceJsUri();
371378
await this._service.loadFileUri(serviceJsPath);
372379
DMPLogger.i(Tags.LAUNCH, "startEngineService end");

iOS/dimina/DiminaKit/App/DMPApp.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,12 @@ public class DMPApp {
107107

108108
public func loadBundle() async {
109109
print("loadBundle")
110+
// Inject custom API namespaces before loading service.js
111+
let namespaces = DMPAppManager.sharedInstance().apiNamespaces
112+
if !namespaces.isEmpty {
113+
let json = namespaces.map { "\"\($0)\"" }.joined(separator: ",")
114+
await service?.evaluateScript("globalThis.__diminaApiNamespaces = [\(json)]")
115+
}
110116
await service?.loadFile(path: DMPSandboxManager.sdkServicePath())
111117
await service?.loadFile(path: DMPSandboxManager.appServicePath(appId: appId))
112118

iOS/dimina/DiminaKit/App/DMPAppManager.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,20 @@
77

88
public class DMPAppManager {
99
private static let instance = DMPAppManager()
10-
10+
1111
private var appPools: [Int: DMPApp] = [:]
1212
private var appIndex: Int = 0
13-
13+
public private(set) var apiNamespaces: [String] = []
14+
1415
private init() {}
15-
16+
1617
public static func sharedInstance() -> DMPAppManager {
1718
return instance
1819
}
20+
21+
public func setup(apiNamespaces: [String] = []) {
22+
self.apiNamespaces = apiNamespaces
23+
}
1924

2025
func getApp(appIndex: Int) -> DMPApp? {
2126
return appPools[appIndex]

0 commit comments

Comments
 (0)