接入移动壳返回栈事件
移动壳声明 host.events 和 navigation.canGoBack 能力 Expo WebView 导航状态变化时向 H5 注入返回栈事件 H5 native_app transport 支持订阅 HostBridge 事件 补充事件订阅测试、移动壳能力测试和配置守卫 更新宿主壳方案和团队共享决策记录
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
canUseTauriHostBridge,
|
||||
requestNativeAppHostBridge,
|
||||
resetNativeAppHostBridgeForTest,
|
||||
subscribeNativeAppHostBridgeEvent,
|
||||
} from './nativeAppHostBridge';
|
||||
|
||||
afterEach(() => {
|
||||
@@ -139,4 +140,43 @@ describe('nativeAppHostBridge', () => {
|
||||
|
||||
await assertion;
|
||||
});
|
||||
|
||||
test('订阅 React Native WebView 注入的宿主事件', () => {
|
||||
window.ReactNativeWebView = {
|
||||
postMessage: vi.fn(),
|
||||
};
|
||||
const listener = vi.fn();
|
||||
const unsubscribe = subscribeNativeAppHostBridgeEvent<{
|
||||
canGoBack: boolean;
|
||||
}>('navigation.canGoBack', listener);
|
||||
|
||||
window.dispatchEvent(
|
||||
new MessageEvent('message', {
|
||||
data: JSON.stringify({
|
||||
bridge: HOST_BRIDGE_PROTOCOL,
|
||||
version: HOST_BRIDGE_VERSION,
|
||||
event: 'navigation.canGoBack',
|
||||
payload: {
|
||||
canGoBack: true,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
unsubscribe();
|
||||
window.dispatchEvent(
|
||||
new MessageEvent('message', {
|
||||
data: JSON.stringify({
|
||||
bridge: HOST_BRIDGE_PROTOCOL,
|
||||
version: HOST_BRIDGE_VERSION,
|
||||
event: 'navigation.canGoBack',
|
||||
payload: {
|
||||
canGoBack: false,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
expect(listener).toHaveBeenCalledWith({ canGoBack: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
HOST_BRIDGE_PROTOCOL,
|
||||
HOST_BRIDGE_VERSION,
|
||||
type HostBridgeError,
|
||||
type HostBridgeEvent,
|
||||
type HostBridgeMethod,
|
||||
type HostBridgeRequest,
|
||||
type HostBridgeResponse,
|
||||
@@ -31,6 +32,7 @@ type PendingNativeRequest = {
|
||||
};
|
||||
|
||||
const pendingNativeRequests = new Map<string, PendingNativeRequest>();
|
||||
const nativeEventListeners = new Map<string, Set<(payload: unknown) => void>>();
|
||||
|
||||
let nativeBridgeListenerInstalled = false;
|
||||
let nextNativeRequestSequence = 0;
|
||||
@@ -77,6 +79,19 @@ function isHostBridgeResponse(value: unknown): value is HostBridgeResponse {
|
||||
);
|
||||
}
|
||||
|
||||
function isHostBridgeEvent(value: unknown): value is HostBridgeEvent {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const candidate = value as Partial<HostBridgeEvent>;
|
||||
return (
|
||||
candidate.bridge === HOST_BRIDGE_PROTOCOL &&
|
||||
candidate.version === HOST_BRIDGE_VERSION &&
|
||||
typeof candidate.event === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
function parseNativeMessage(data: unknown) {
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
@@ -106,6 +121,17 @@ function settleNativeResponse(response: HostBridgeResponse) {
|
||||
pending.reject(createHostBridgeError(response.error));
|
||||
}
|
||||
|
||||
function dispatchNativeEvent(event: HostBridgeEvent) {
|
||||
const listeners = nativeEventListeners.get(event.event);
|
||||
if (!listeners) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const listener of listeners) {
|
||||
listener(event.payload);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureNativeBridgeListener() {
|
||||
const nativeWindow = resolveNativeWindow();
|
||||
if (!nativeWindow || nativeBridgeListenerInstalled) {
|
||||
@@ -116,6 +142,10 @@ function ensureNativeBridgeListener() {
|
||||
const value = parseNativeMessage(event.data);
|
||||
if (isHostBridgeResponse(value)) {
|
||||
settleNativeResponse(value);
|
||||
return;
|
||||
}
|
||||
if (isHostBridgeEvent(value)) {
|
||||
dispatchNativeEvent(value);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -139,6 +169,24 @@ export function canUseNativeAppHostBridge() {
|
||||
return canUseReactNativeHostBridge() || canUseTauriHostBridge();
|
||||
}
|
||||
|
||||
export function subscribeNativeAppHostBridgeEvent<Payload = unknown>(
|
||||
eventName: string,
|
||||
listener: (payload: Payload | undefined) => void,
|
||||
) {
|
||||
ensureNativeBridgeListener();
|
||||
|
||||
const listeners = nativeEventListeners.get(eventName) ?? new Set();
|
||||
listeners.add(listener as (payload: unknown) => void);
|
||||
nativeEventListeners.set(eventName, listeners);
|
||||
|
||||
return () => {
|
||||
listeners.delete(listener as (payload: unknown) => void);
|
||||
if (listeners.size === 0) {
|
||||
nativeEventListeners.delete(eventName);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export async function requestNativeAppHostBridge<Result = unknown>(
|
||||
method: HostBridgeMethod,
|
||||
payload?: unknown,
|
||||
@@ -213,5 +261,6 @@ export function resetNativeAppHostBridgeForTest() {
|
||||
clearTimeout(pending.timeoutId);
|
||||
}
|
||||
pendingNativeRequests.clear();
|
||||
nativeEventListeners.clear();
|
||||
nextNativeRequestSequence = 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user