@dino-dev/bridge-core-ts 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/adaptation.d.ts +3 -1
- package/dist/src/adaptation.d.ts.map +1 -1
- package/dist/src/adaptation.js +12 -2
- package/dist/src/adaptation.js.map +1 -1
- package/dist/src/content-protocol.d.ts +43 -0
- package/dist/src/content-protocol.d.ts.map +1 -1
- package/dist/src/content-protocol.js +36 -0
- package/dist/src/content-protocol.js.map +1 -1
- package/dist/src/iframe-host.d.ts +24 -0
- package/dist/src/iframe-host.d.ts.map +1 -0
- package/dist/src/iframe-host.js +138 -0
- package/dist/src/iframe-host.js.map +1 -0
- package/dist/src/index.d.ts +6 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/mock-bridge.d.ts +100 -0
- package/dist/src/mock-bridge.d.ts.map +1 -0
- package/dist/src/mock-bridge.js +247 -0
- package/dist/src/mock-bridge.js.map +1 -0
- package/dist/src/mrp-bridge.d.ts +24 -3
- package/dist/src/mrp-bridge.d.ts.map +1 -1
- package/dist/src/mrp-bridge.js +73 -13
- package/dist/src/mrp-bridge.js.map +1 -1
- package/dist/src/system-module.d.ts +9 -0
- package/dist/src/system-module.d.ts.map +1 -1
- package/dist/src/system-module.js.map +1 -1
- package/package.json +8 -2
- package/src/adaptation.ts +50 -24
- package/src/content-protocol.ts +68 -1
- package/src/iframe-host.ts +169 -0
- package/src/index.ts +37 -22
- package/src/mock-bridge.ts +390 -0
- package/src/mrp-bridge.ts +308 -220
- package/src/system-module.ts +30 -20
package/src/content-protocol.ts
CHANGED
|
@@ -4,6 +4,8 @@ export const bridgeMessageTypes = {
|
|
|
4
4
|
render: 'render',
|
|
5
5
|
interaction: 'interaction',
|
|
6
6
|
error: 'error',
|
|
7
|
+
/** 宿主镜像给 H5 的回合事件(单向:宿主 → H5)。 */
|
|
8
|
+
agent: 'agent',
|
|
7
9
|
} as const;
|
|
8
10
|
|
|
9
11
|
export const renderActions = {
|
|
@@ -91,4 +93,69 @@ export const bridgeError = {
|
|
|
91
93
|
report(payload: BridgeErrorPayload): Promise<unknown> {
|
|
92
94
|
return bridge.send(bridgeMessageTypes.error, 'report', payload);
|
|
93
95
|
},
|
|
94
|
-
} as const;
|
|
96
|
+
} as const;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 回合事件协议:宿主把「现在轮到谁、这一轮发生了什么」镜像给 H5。
|
|
100
|
+
*
|
|
101
|
+
* 用在 H5 页面**作为一个交互步骤**嵌在原生流程里的场景(AI 教学 / 问诊等):
|
|
102
|
+
* 原生题型能收到宿主的回合事件,H5 题型也应该能。宿主只负责把事实发下来,
|
|
103
|
+
* 怎么响应(放开作答、复位重做、演完反馈再收尾)由 H5 自己决定。
|
|
104
|
+
*
|
|
105
|
+
* 线格式:`{ type: 'agent', action, payload: { iid, ... } }`
|
|
106
|
+
*/
|
|
107
|
+
export const agentEvents = {
|
|
108
|
+
/** 作答权交回给用户(宿主的开场播报已结束)。 */
|
|
109
|
+
startLearn: 'start_learn',
|
|
110
|
+
/** 本题重做。 */
|
|
111
|
+
retryLearn: 'retry_learn',
|
|
112
|
+
/** 本题判过;若宿主把收尾交给了 H5,H5 需回发一次收尾动作,否则流程不推进。 */
|
|
113
|
+
passLearn: 'pass_learn',
|
|
114
|
+
/** 用户**绕过这块 H5** 上行了一次作答(原生输入栏、原生选择器等)。 */
|
|
115
|
+
externalAnswer: 'external_answer',
|
|
116
|
+
} as const;
|
|
117
|
+
|
|
118
|
+
export interface AgentEventPayload {
|
|
119
|
+
/**
|
|
120
|
+
* 这条事件属于哪一屏。
|
|
121
|
+
*
|
|
122
|
+
* 内容(`render/set`)和事件通常来自宿主上游的两条独立消息,到达先后不保证。
|
|
123
|
+
* 拿它对齐「这条事件说的是不是我现在这一屏」,不匹配就先扣着。
|
|
124
|
+
*/
|
|
125
|
+
iid?: string;
|
|
126
|
+
[key: string]: unknown;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface ExternalAnswerPayload extends AgentEventPayload {
|
|
130
|
+
/** 那次绕过本页的上行负载(形态由宿主定义)。 */
|
|
131
|
+
answer?: Record<string, unknown>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface AgentEventHandlers {
|
|
135
|
+
startLearn?(payload: AgentEventPayload, message: BridgeMessage<AgentEventPayload>): void;
|
|
136
|
+
retryLearn?(payload: AgentEventPayload, message: BridgeMessage<AgentEventPayload>): void;
|
|
137
|
+
passLearn?(payload: AgentEventPayload, message: BridgeMessage<AgentEventPayload>): void;
|
|
138
|
+
externalAnswer?(payload: ExternalAnswerPayload, message: BridgeMessage<ExternalAnswerPayload>): void;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 只订阅传了 handler 的那几个事件;返回的函数一次性退订。 */
|
|
142
|
+
export function bindAgentProtocol(
|
|
143
|
+
handlers: AgentEventHandlers,
|
|
144
|
+
transport: Pick<ProtocolTransport, 'on'> = bridge,
|
|
145
|
+
): Unsubscribe {
|
|
146
|
+
const pairs: Array<[string, ((payload: never, message: never) => void) | undefined]> = [
|
|
147
|
+
[agentEvents.startLearn, handlers.startLearn],
|
|
148
|
+
[agentEvents.retryLearn, handlers.retryLearn],
|
|
149
|
+
[agentEvents.passLearn, handlers.passLearn],
|
|
150
|
+
[agentEvents.externalAnswer, handlers.externalAnswer],
|
|
151
|
+
];
|
|
152
|
+
const subscriptions = pairs
|
|
153
|
+
.filter(([, handler]) => handler !== undefined)
|
|
154
|
+
.map(([action, handler]) =>
|
|
155
|
+
transport.on(bridgeMessageTypes.agent, action, (message: BridgeMessage<any>) => {
|
|
156
|
+
(handler as (payload: unknown, message: unknown) => void)(message.payload ?? {}, message);
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
return () => subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
161
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iframe 宿主模式:H5 被嵌在网页 iframe 里(如教研后台的实时预览)时,把父窗口当作原生容器。
|
|
3
|
+
*
|
|
4
|
+
* 触发条件:页面在 iframe 内,且 URL 带 `nl_host=iframe&nl_host_origin=<父窗口 origin>`。
|
|
5
|
+
* 父窗口 origin 由宿主显式声明:只向它发消息、只收它发来的消息,其它来源一律忽略。
|
|
6
|
+
*
|
|
7
|
+
* H5 业务代码无需改动:`bridge.whenReady()` / `bindRenderProtocol` / `bindAgentProtocol`
|
|
8
|
+
* 照常工作。原生模块在预览里不可用(`system` 除外),调用返回 `null` 并通知宿主记录。
|
|
9
|
+
*
|
|
10
|
+
* 线格式(postMessage,均带 `__nlBridge: 1`):
|
|
11
|
+
* - H5 → 宿主:`{ kind: 'send', type, action, payload }`
|
|
12
|
+
* - H5 → 宿主:`{ kind: 'module', namespace, method }`(预览里不可用的原生调用)
|
|
13
|
+
* - 宿主 → H5:`{ kind: 'message', type, action, payload }`
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { BridgeMessage } from './mrp-bridge';
|
|
17
|
+
import type { ArAdaptationInfo } from './system-module';
|
|
18
|
+
|
|
19
|
+
export const IFRAME_HOST_PARAM = 'nl_host';
|
|
20
|
+
export const IFRAME_HOST_ORIGIN_PARAM = 'nl_host_origin';
|
|
21
|
+
export const IFRAME_HOST_MARK = '__nlBridge';
|
|
22
|
+
|
|
23
|
+
const DESIGN_LONG_SIDE = 812;
|
|
24
|
+
|
|
25
|
+
interface HostEnvelope {
|
|
26
|
+
[IFRAME_HOST_MARK]: 1;
|
|
27
|
+
kind: string;
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isHostEnvelope(data: unknown): data is HostEnvelope {
|
|
32
|
+
return (
|
|
33
|
+
typeof data === 'object' &&
|
|
34
|
+
data !== null &&
|
|
35
|
+
(data as Record<string, unknown>)[IFRAME_HOST_MARK] === 1
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveHostOrigin(): string | null {
|
|
40
|
+
if (!window.parent || window.parent === window || !window.location) return null;
|
|
41
|
+
const params = new URLSearchParams(window.location.search);
|
|
42
|
+
if (params.get(IFRAME_HOST_PARAM) !== 'iframe') return null;
|
|
43
|
+
const raw = params.get(IFRAME_HOST_ORIGIN_PARAM);
|
|
44
|
+
if (!raw) return null;
|
|
45
|
+
try {
|
|
46
|
+
const origin = new URL(raw).origin;
|
|
47
|
+
return origin === 'null' ? null : origin;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function currentAdaptation(): ArAdaptationInfo {
|
|
54
|
+
const screenWidth = window.innerWidth;
|
|
55
|
+
const screenHeight = window.innerHeight;
|
|
56
|
+
return {
|
|
57
|
+
designLongSide: DESIGN_LONG_SIDE,
|
|
58
|
+
screenWidth,
|
|
59
|
+
screenHeight,
|
|
60
|
+
scale: Math.max(screenWidth, screenHeight) / DESIGN_LONG_SIDE,
|
|
61
|
+
safeArea: { top: 0, bottom: 0, left: 0, right: 0 },
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 满足触发条件时安装 iframe 宿主桥;已在原生容器内或条件不满足时什么都不做。
|
|
67
|
+
*
|
|
68
|
+
* @returns 是否已安装
|
|
69
|
+
*/
|
|
70
|
+
export function installIframeHostBridge(): boolean {
|
|
71
|
+
if (typeof window === 'undefined' || window.__FLUTTER_BRIDGE__) return false;
|
|
72
|
+
const hostOrigin = resolveHostOrigin();
|
|
73
|
+
if (!hostOrigin) return false;
|
|
74
|
+
|
|
75
|
+
const post = (kind: string, body: Record<string, unknown>): void => {
|
|
76
|
+
window.parent.postMessage(
|
|
77
|
+
{ [IFRAME_HOST_MARK]: 1, kind, ...body },
|
|
78
|
+
hostOrigin,
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const dispatch = (
|
|
83
|
+
type: string,
|
|
84
|
+
action: string,
|
|
85
|
+
payload: Record<string, unknown>,
|
|
86
|
+
): void => {
|
|
87
|
+
const message: BridgeMessage = {
|
|
88
|
+
type,
|
|
89
|
+
action,
|
|
90
|
+
payload,
|
|
91
|
+
timestamp: Date.now(),
|
|
92
|
+
};
|
|
93
|
+
window.dispatchEvent(new CustomEvent('flutterMessage', { detail: message }));
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
window.addEventListener('message', (event: MessageEvent) => {
|
|
97
|
+
if (event.source !== window.parent || event.origin !== hostOrigin) return;
|
|
98
|
+
const data = event.data;
|
|
99
|
+
if (!isHostEnvelope(data) || data.kind !== 'message') return;
|
|
100
|
+
if (typeof data.type !== 'string' || typeof data.action !== 'string') return;
|
|
101
|
+
const payload =
|
|
102
|
+
typeof data.payload === 'object' && data.payload !== null
|
|
103
|
+
? (data.payload as Record<string, unknown>)
|
|
104
|
+
: {};
|
|
105
|
+
dispatch(data.type, data.action, payload);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
window.addEventListener('resize', () => {
|
|
109
|
+
dispatch(
|
|
110
|
+
'adaptation',
|
|
111
|
+
'changed',
|
|
112
|
+
currentAdaptation() as unknown as Record<string, unknown>,
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const systemModule: Record<string, () => unknown> = {
|
|
117
|
+
isModuleAllowed: () => ({ allowed: false, namespace: '' }),
|
|
118
|
+
getAvailableModules: () => ({ modules: [] }),
|
|
119
|
+
getContainerInfo: () => ({
|
|
120
|
+
packageId: null,
|
|
121
|
+
registeredModules: [],
|
|
122
|
+
allowedModules: [],
|
|
123
|
+
adaptation: currentAdaptation(),
|
|
124
|
+
isIframeHost: true,
|
|
125
|
+
}),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const moduleProxy = (namespace: string) =>
|
|
129
|
+
new Proxy({} as Record<string, unknown>, {
|
|
130
|
+
// bridge.module() 用 `NativeModules[ns][method]` 做存在性判断,必须恒为真
|
|
131
|
+
has: () => true,
|
|
132
|
+
get: (_target, method: string | symbol) => {
|
|
133
|
+
if (typeof method !== 'string') return undefined;
|
|
134
|
+
return async () => {
|
|
135
|
+
if (namespace === 'system' && method in systemModule) {
|
|
136
|
+
return systemModule[method]();
|
|
137
|
+
}
|
|
138
|
+
post('module', { namespace, method });
|
|
139
|
+
return null;
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
window.NativeModules = new Proxy(
|
|
145
|
+
{} as NonNullable<Window['NativeModules']>,
|
|
146
|
+
{
|
|
147
|
+
has: () => true,
|
|
148
|
+
get: (_target, namespace: string | symbol) =>
|
|
149
|
+
typeof namespace === 'string' ? moduleProxy(namespace) : undefined,
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
window.__FLUTTER_BRIDGE__ = {
|
|
154
|
+
send: async (type, action, payload) => {
|
|
155
|
+
post('send', { type, action, payload });
|
|
156
|
+
return null;
|
|
157
|
+
},
|
|
158
|
+
onMessage: (jsonString: string) => {
|
|
159
|
+
try {
|
|
160
|
+
const message = JSON.parse(jsonString) as BridgeMessage;
|
|
161
|
+
dispatch(message.type, message.action, message.payload);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
console.error('[MRP Bridge] iframe 宿主消息解析失败:', e);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return true;
|
|
169
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -10,24 +10,34 @@
|
|
|
10
10
|
|
|
11
11
|
// 核心 Bridge
|
|
12
12
|
export { bridge, MrpBridge } from './mrp-bridge';
|
|
13
|
-
export type {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
export type { BridgeMessage, MessageHandler, Unsubscribe } from './mrp-bridge';
|
|
14
|
+
|
|
15
|
+
// iframe 宿主模式(网页内嵌预览)的契约常量
|
|
16
|
+
export {
|
|
17
|
+
IFRAME_HOST_MARK,
|
|
18
|
+
IFRAME_HOST_ORIGIN_PARAM,
|
|
19
|
+
IFRAME_HOST_PARAM,
|
|
20
|
+
} from './iframe-host';
|
|
18
21
|
|
|
19
22
|
// 内置系统模块类型 + 工厂函数(容器核心契约)
|
|
20
23
|
export { getSystemModule } from './system-module';
|
|
21
24
|
export type {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
SystemModule,
|
|
26
|
+
ModuleAllowedResult,
|
|
27
|
+
AvailableModulesResult,
|
|
28
|
+
ContainerInfoResult,
|
|
29
|
+
ArAdaptationInfo,
|
|
30
|
+
SafeAreaInsets,
|
|
27
31
|
} from './system-module';
|
|
28
32
|
|
|
29
33
|
// App/H5 统一屏幕适配
|
|
30
|
-
export {
|
|
34
|
+
export {
|
|
35
|
+
ar,
|
|
36
|
+
getArScale,
|
|
37
|
+
getSafeArea,
|
|
38
|
+
initArAdaptation,
|
|
39
|
+
onArAdaptationChange,
|
|
40
|
+
} from './adaptation';
|
|
31
41
|
export type { ArAdaptationListener } from './adaptation';
|
|
32
42
|
|
|
33
43
|
// Generated 模块:类型 + 工厂函数
|
|
@@ -39,17 +49,22 @@ export type { RouteChangedPayload, AppMountedPayload } from './lifecycle';
|
|
|
39
49
|
|
|
40
50
|
// 标准内容渲染、交互与错误协议
|
|
41
51
|
export {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
agentEvents,
|
|
53
|
+
bindAgentProtocol,
|
|
54
|
+
bindRenderProtocol,
|
|
55
|
+
bridgeError,
|
|
56
|
+
bridgeMessageTypes,
|
|
57
|
+
interaction,
|
|
58
|
+
renderActions,
|
|
59
|
+
renderProtocol,
|
|
48
60
|
} from './content-protocol';
|
|
49
61
|
export type {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
AgentEventHandlers,
|
|
63
|
+
AgentEventPayload,
|
|
64
|
+
BridgeErrorPayload,
|
|
65
|
+
ExternalAnswerPayload,
|
|
66
|
+
InteractionPayload,
|
|
67
|
+
ProtocolTransport,
|
|
68
|
+
RenderHandlers,
|
|
69
|
+
RenderPayload,
|
|
55
70
|
} from './content-protocol';
|