接入原生壳网络状态能力
新增 network.status 与 network.statusChanged HostBridge 能力 Expo 壳通过 expo-network 查询并订阅真实网络状态 Tauri 壳通过主站可达性查询和 WebView online/offline 事件同步网络状态 更新壳能力检查、测试和架构文档
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
exportHostImageFile,
|
||||
exportHostTextFile,
|
||||
getHostAppearanceColorScheme,
|
||||
getHostNetworkStatus,
|
||||
getHostRuntime,
|
||||
getNativeAppHostRuntime,
|
||||
isWechatMiniProgramWebViewRuntime,
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
setHostAppTitle,
|
||||
setHostShareTarget,
|
||||
subscribeHostAppLifecycle,
|
||||
subscribeHostNetworkStatusChange,
|
||||
subscribeHostRuntimeChange,
|
||||
writeHostClipboardText,
|
||||
} from './hostBridge';
|
||||
@@ -217,6 +219,78 @@ describe('hostBridge', () => {
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('查询并订阅原生 App 网络状态', async () => {
|
||||
const invoke = vi.fn(
|
||||
async (_command: string, args?: Record<string, unknown>) => {
|
||||
const request = (args as { request: { id: string; method: string } })
|
||||
.request;
|
||||
return {
|
||||
bridge: 'GenarrativeHostBridge',
|
||||
version: 1,
|
||||
id: request.id,
|
||||
ok: true,
|
||||
result: {
|
||||
isConnected: true,
|
||||
isInternetReachable: false,
|
||||
connectionType: 'WIFI',
|
||||
nativeType: 'WIFI',
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
const listener = vi.fn();
|
||||
window.history.replaceState(
|
||||
null,
|
||||
'',
|
||||
nativeAppPath(['network.status', 'network.statusChanged']),
|
||||
);
|
||||
window.ReactNativeWebView = {
|
||||
postMessage: vi.fn(),
|
||||
};
|
||||
window.__TAURI__ = {
|
||||
core: {
|
||||
invoke: asTauriInvoke(invoke),
|
||||
},
|
||||
};
|
||||
|
||||
await expect(getHostNetworkStatus()).resolves.toEqual({
|
||||
isConnected: true,
|
||||
isInternetReachable: false,
|
||||
connectionType: 'wifi',
|
||||
nativeType: 'WIFI',
|
||||
});
|
||||
|
||||
const unsubscribe = subscribeHostNetworkStatusChange(listener);
|
||||
window.dispatchEvent(
|
||||
new MessageEvent('message', {
|
||||
data: JSON.stringify({
|
||||
bridge: 'GenarrativeHostBridge',
|
||||
version: 1,
|
||||
event: 'network.statusChanged',
|
||||
payload: {
|
||||
isConnected: false,
|
||||
isInternetReachable: false,
|
||||
connectionType: 'NONE',
|
||||
nativeType: 'NONE',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
unsubscribe();
|
||||
|
||||
expect(listener).toHaveBeenCalledWith({
|
||||
isConnected: false,
|
||||
isInternetReachable: false,
|
||||
connectionType: 'none',
|
||||
nativeType: 'NONE',
|
||||
});
|
||||
expect(invoke).toHaveBeenCalledWith('host_bridge_request', {
|
||||
request: expect.objectContaining({
|
||||
method: 'network.status',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
test('从真实宿主 runtime 回读能力并通知订阅者', async () => {
|
||||
const listener = vi.fn();
|
||||
const unsubscribe = subscribeHostRuntimeChange(listener);
|
||||
@@ -510,6 +584,13 @@ describe('hostBridge', () => {
|
||||
}
|
||||
: request.method === 'appearance.getColorScheme'
|
||||
? { colorScheme: 'dark' }
|
||||
: request.method === 'network.status'
|
||||
? {
|
||||
isConnected: true,
|
||||
isInternetReachable: true,
|
||||
connectionType: 'ethernet',
|
||||
nativeType: 'online',
|
||||
}
|
||||
: true,
|
||||
};
|
||||
});
|
||||
@@ -527,6 +608,7 @@ describe('hostBridge', () => {
|
||||
'app.openExternalUrl',
|
||||
'app.setTitle',
|
||||
'app.setBadgeCount',
|
||||
'network.status',
|
||||
'share.open',
|
||||
'file.exportImage',
|
||||
]),
|
||||
@@ -565,6 +647,12 @@ describe('hostBridge', () => {
|
||||
true,
|
||||
);
|
||||
await expect(setHostAppBadgeCount({ count: 7 })).resolves.toBe(true);
|
||||
await expect(getHostNetworkStatus()).resolves.toEqual({
|
||||
isConnected: true,
|
||||
isInternetReachable: true,
|
||||
connectionType: 'ethernet',
|
||||
nativeType: 'online',
|
||||
});
|
||||
await expect(
|
||||
openHostShare({
|
||||
title: '暖灯猫街',
|
||||
@@ -645,6 +733,11 @@ describe('hostBridge', () => {
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(invoke).toHaveBeenCalledWith('host_bridge_request', {
|
||||
request: expect.objectContaining({
|
||||
method: 'network.status',
|
||||
}),
|
||||
});
|
||||
expect(invoke).toHaveBeenCalledWith('host_bridge_request', {
|
||||
request: expect.objectContaining({
|
||||
method: 'share.open',
|
||||
@@ -716,6 +809,7 @@ describe('hostBridge', () => {
|
||||
await expect(setHostAppBadgeCount({ count: 1 })).resolves.toBe(false);
|
||||
await expect(setHostAppBadgeCount({ count: -1 })).resolves.toBe(false);
|
||||
await expect(setHostAppBadgeCount({ count: 1.5 })).resolves.toBe(false);
|
||||
await expect(getHostNetworkStatus()).resolves.toBe(false);
|
||||
await expect(
|
||||
openHostShare({
|
||||
title: '暖灯猫街',
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
HostBridgeCapability,
|
||||
HostBridgeMethod,
|
||||
HostBridgeRuntimeResult,
|
||||
NetworkStatusResult,
|
||||
OpenExternalUrlPayload,
|
||||
SetBadgeCountPayload,
|
||||
ShareOpenPayload,
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
isHostBridgeCapability,
|
||||
normalizeHostBridgeBadgeCount,
|
||||
normalizeHostBridgeColorScheme,
|
||||
normalizeHostBridgeConnectionType,
|
||||
normalizeHostBridgeExternalUrl,
|
||||
normalizeHostBridgeLifecycleState,
|
||||
} from '../../../packages/shared/src/contracts/hostBridge';
|
||||
@@ -104,6 +106,8 @@ export type HostAppearanceColorSchemeSnapshot = AppearanceColorSchemeResult;
|
||||
|
||||
export type HostAppLifecycleSnapshot = AppLifecycleEventPayload;
|
||||
|
||||
export type HostNetworkStatusSnapshot = NetworkStatusResult;
|
||||
|
||||
const HOST_RUNTIME_REFRESH_TIMEOUT_MS = 3000;
|
||||
|
||||
let cachedNativeHostRuntime: HostBridgeRuntimeResult | null = null;
|
||||
@@ -815,3 +819,56 @@ export function subscribeHostAppLifecycle(
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeHostNetworkStatus(
|
||||
payload: NetworkStatusResult | null | undefined,
|
||||
): HostNetworkStatusSnapshot {
|
||||
const connectionType = normalizeHostBridgeConnectionType(
|
||||
payload?.connectionType ?? payload?.nativeType,
|
||||
);
|
||||
const isConnected =
|
||||
typeof payload?.isConnected === 'boolean'
|
||||
? payload.isConnected
|
||||
: connectionType !== 'none' && connectionType !== 'unknown';
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
isInternetReachable:
|
||||
typeof payload?.isInternetReachable === 'boolean'
|
||||
? payload.isInternetReachable
|
||||
: null,
|
||||
connectionType,
|
||||
...(typeof payload?.nativeType === 'string'
|
||||
? { nativeType: payload.nativeType }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function getHostNetworkStatus() {
|
||||
if (!canUseNativeHostCapability('network.status')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return normalizeHostNetworkStatus(
|
||||
await requestNativeAppHostBridge<NetworkStatusResult>('network.status'),
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function subscribeHostNetworkStatusChange(
|
||||
listener: (payload: HostNetworkStatusSnapshot) => void,
|
||||
) {
|
||||
if (!canUseNativeHostCapability('network.statusChanged')) {
|
||||
return () => undefined;
|
||||
}
|
||||
|
||||
return subscribeNativeAppHostBridgeEvent<NetworkStatusResult>(
|
||||
'network.statusChanged',
|
||||
(payload) => {
|
||||
listener(normalizeHostNetworkStatus(payload));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user