195 lines
7.5 KiB
TypeScript
195 lines
7.5 KiB
TypeScript
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
|
|
navigateBack?: (options: { delta: number; success?: () => void; fail?: (error: { errMsg?: string }) => void }) => void
|
|
postMessage?: (options: { data: { type: string } }) => 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' }
|
|
|
|
export type WechatMiniProgramReturnResult =
|
|
| { status: 'returned' }
|
|
| { status: 'not-embedded' }
|
|
| { status: 'failed'; reason: 'bridge-unavailable' | 'not-in-mini-program' | 'navigation-failed' | 'timeout' }
|
|
|
|
const SDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.3.2.js'
|
|
export const MINI_PROGRAM_RETURN_MESSAGE = 'museum-guide:return'
|
|
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 pendingReturn: Promise<WechatMiniProgramReturnResult> | 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 requestHostReturnFallback = (bridge: MiniProgramBridge) => {
|
|
if (!bridge.postMessage) return false
|
|
|
|
try {
|
|
bridge.postMessage({ data: { type: MINI_PROGRAM_RETURN_MESSAGE } })
|
|
return true
|
|
} catch (error) {
|
|
console.error('请求宿主小程序安全返回失败:', error)
|
|
return false
|
|
}
|
|
}
|
|
|
|
const navigateBackToHost = (bridge: MiniProgramBridge) => new Promise<WechatMiniProgramReturnResult>((resolve) => {
|
|
if (!bridge.navigateBack) return resolve({ status: 'failed', reason: 'bridge-unavailable' })
|
|
|
|
let settled = false
|
|
const finish = (result: WechatMiniProgramReturnResult) => {
|
|
if (settled) return
|
|
settled = true
|
|
clearTimeout(timeout)
|
|
resolve(result)
|
|
}
|
|
const timeout = setTimeout(() => finish({ status: 'failed', reason: 'timeout' }), NAVIGATION_TIMEOUT)
|
|
|
|
try {
|
|
bridge.navigateBack({
|
|
delta: 1,
|
|
success: () => finish({ status: 'returned' }),
|
|
fail: (error) => {
|
|
console.error('返回宿主小程序失败:', error)
|
|
finish({ status: 'failed', reason: '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
|
|
}
|
|
|
|
const returnOnce = async (): Promise<WechatMiniProgramReturnResult> => {
|
|
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' }
|
|
|
|
const result = await navigateBackToHost(bridge)
|
|
if (result.status === 'returned') return result
|
|
|
|
// 宿主页可能由分享或冷启动直接打开,页面栈没有上一页时改由宿主执行安全回退。
|
|
if (requestHostReturnFallback(bridge)) return { status: 'returned' }
|
|
return result
|
|
}
|
|
|
|
export const returnToWechatMiniProgram = () => {
|
|
if (pendingReturn) return pendingReturn
|
|
pendingReturn = returnOnce().finally(() => { pendingReturn = null })
|
|
return pendingReturn
|
|
}
|