This commit is contained in:
125
src/services/WechatMiniProgramBridgeService.ts
Normal file
125
src/services/WechatMiniProgramBridgeService.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { isEmbeddedInWechatMiniProgram } from '@/utils/hostEnvironment'
|
||||
import {
|
||||
buildWechatOpenLocationPageUrl,
|
||||
validateWechatOpenLocationTarget,
|
||||
type WechatOpenLocationTarget
|
||||
} from '@/utils/wechatOpenLocationProtocol'
|
||||
|
||||
interface MiniProgramBridge {
|
||||
getEnv?: (callback: (result: { miniprogram?: boolean }) => void) => void
|
||||
navigateTo?: (options: { url: string; success?: () => void; fail?: (error: { errMsg?: string }) => void }) => void
|
||||
}
|
||||
|
||||
export type WechatMiniProgramBridgeResult =
|
||||
| { status: 'opened' }
|
||||
| { status: 'not-embedded' }
|
||||
| { status: 'failed'; reason: 'invalid-target' | 'bridge-unavailable' | 'not-in-mini-program' | 'navigation-failed' | 'page-not-found' | 'timeout' }
|
||||
|
||||
const SDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.3.2.js'
|
||||
const BRIDGE_WAIT_TIMEOUT = 2500
|
||||
const BRIDGE_POLL_INTERVAL = 50
|
||||
const GET_ENV_TIMEOUT = 1500
|
||||
const NAVIGATION_TIMEOUT = 3000
|
||||
|
||||
let pendingNavigation: Promise<WechatMiniProgramBridgeResult> | null = null
|
||||
let sdkLoadPromise: Promise<void> | null = null
|
||||
|
||||
const getBridge = (): MiniProgramBridge | null => {
|
||||
if (typeof window === 'undefined') return null
|
||||
return (window as Window & { wx?: { miniProgram?: MiniProgramBridge } }).wx?.miniProgram || null
|
||||
}
|
||||
|
||||
const wait = (duration: number) => new Promise<void>((resolve) => setTimeout(resolve, duration))
|
||||
|
||||
const loadWechatJsSdk = () => {
|
||||
if (typeof document === 'undefined' || getBridge()) return Promise.resolve()
|
||||
if (sdkLoadPromise) return sdkLoadPromise
|
||||
sdkLoadPromise = new Promise<void>((resolve) => {
|
||||
const existing = document.querySelector<HTMLScriptElement>('script[data-wechat-js-sdk="true"]')
|
||||
if (existing) {
|
||||
existing.addEventListener('load', () => resolve(), { once: true })
|
||||
existing.addEventListener('error', () => resolve(), { once: true })
|
||||
return
|
||||
}
|
||||
const script = document.createElement('script')
|
||||
script.src = SDK_URL
|
||||
script.async = true
|
||||
script.dataset.wechatJsSdk = 'true'
|
||||
script.onload = () => resolve()
|
||||
script.onerror = () => { script.remove(); resolve() }
|
||||
document.head.appendChild(script)
|
||||
}).finally(() => { sdkLoadPromise = null })
|
||||
return sdkLoadPromise
|
||||
}
|
||||
|
||||
const waitForBridge = async (): Promise<MiniProgramBridge | null> => {
|
||||
const immediate = getBridge()
|
||||
if (immediate) return immediate
|
||||
void loadWechatJsSdk()
|
||||
const deadline = Date.now() + BRIDGE_WAIT_TIMEOUT
|
||||
while (Date.now() < deadline) {
|
||||
await wait(BRIDGE_POLL_INTERVAL)
|
||||
const bridge = getBridge()
|
||||
if (bridge) return bridge
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const isMiniProgramWebView = (bridge: MiniProgramBridge) => new Promise<boolean>((resolve) => {
|
||||
if (!bridge.getEnv) return resolve(isEmbeddedInWechatMiniProgram())
|
||||
let settled = false
|
||||
const finish = (value: boolean) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
clearTimeout(timeout)
|
||||
resolve(value)
|
||||
}
|
||||
const timeout = setTimeout(() => finish(isEmbeddedInWechatMiniProgram()), GET_ENV_TIMEOUT)
|
||||
try {
|
||||
bridge.getEnv((result) => finish(Boolean(result?.miniprogram)))
|
||||
} catch (error) {
|
||||
console.error('微信小程序环境识别失败:', error)
|
||||
finish(false)
|
||||
}
|
||||
})
|
||||
|
||||
const navigateToHostLocationPage = (bridge: MiniProgramBridge, target: WechatOpenLocationTarget) => new Promise<WechatMiniProgramBridgeResult>((resolve) => {
|
||||
if (!bridge.navigateTo) return resolve({ status: 'failed', reason: 'bridge-unavailable' })
|
||||
let settled = false
|
||||
const finish = (result: WechatMiniProgramBridgeResult) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
clearTimeout(timeout)
|
||||
resolve(result)
|
||||
}
|
||||
const timeout = setTimeout(() => finish({ status: 'failed', reason: 'timeout' }), NAVIGATION_TIMEOUT)
|
||||
try {
|
||||
bridge.navigateTo({
|
||||
url: buildWechatOpenLocationPageUrl(target),
|
||||
success: () => finish({ status: 'opened' }),
|
||||
fail: (error) => {
|
||||
console.error('跳转宿主地图页失败:', error)
|
||||
finish({ status: 'failed', reason: /page.*not found|not found.*page/i.test(error?.errMsg || '') ? 'page-not-found' : 'navigation-failed' })
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('调用微信小程序导航桥接失败:', error)
|
||||
finish({ status: 'failed', reason: 'navigation-failed' })
|
||||
}
|
||||
})
|
||||
|
||||
const openOnce = async (target: WechatOpenLocationTarget): Promise<WechatMiniProgramBridgeResult> => {
|
||||
const validation = validateWechatOpenLocationTarget(target)
|
||||
if (!validation.ok) return { status: 'failed', reason: 'invalid-target' }
|
||||
if (!isEmbeddedInWechatMiniProgram()) return { status: 'not-embedded' }
|
||||
const bridge = await waitForBridge()
|
||||
if (!bridge) return { status: 'failed', reason: 'bridge-unavailable' }
|
||||
if (!await isMiniProgramWebView(bridge)) return { status: 'failed', reason: 'not-in-mini-program' }
|
||||
return navigateToHostLocationPage(bridge, validation.value)
|
||||
}
|
||||
|
||||
export const openWechatMiniProgramLocation = (target: WechatOpenLocationTarget) => {
|
||||
if (pendingNavigation) return pendingNavigation
|
||||
pendingNavigation = openOnce(target).finally(() => { pendingNavigation = null })
|
||||
return pendingNavigation
|
||||
}
|
||||
Reference in New Issue
Block a user