| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 |
- import { getDomainParts, setDomainParts } from '@/config/domainState'
- import {
- DOMAIN_CACHE_KEY,
- DOMAIN_CACHE_TTL,
- DOMAIN_CONFIG_URL,
- DOMAIN_INIT_TIMEOUT,
- DOMAIN_PROBE_PATH,
- DOMAIN_PROBE_TIMEOUT,
- DOMAIN_CACHE_PROBE_TIMEOUT,
- } from '@/config/domainConstants'
- let resolving = null
- let domainReady = false
- let domainReadyPromise = null
- let domainReadyResolve = null
- let initTimeoutId = null
- function createDomainReadyPromise() {
- domainReadyPromise = new Promise((resolve) => {
- domainReadyResolve = resolve
- })
- }
- function markDomainReady() {
- if (domainReady) return
- domainReady = true
- if (initTimeoutId) {
- clearTimeout(initTimeoutId)
- initTimeoutId = null
- }
- domainReadyResolve?.()
- }
- function notifyDomainChanged(cache) {
- try {
- uni.$emit('domain-changed', {
- ...getDomainParts(),
- activeSecureUrl: cache?.activeSecureUrl || '',
- })
- } catch (e) {
- // ignore
- }
- }
- /** 业务请求需等待域名就绪(仅 APP) */
- export function whenDomainReady() {
- // #ifndef APP-PLUS
- return Promise.resolve()
- // #endif
- if (domainReady) return Promise.resolve()
- if (!domainReadyPromise) createDomainReadyPromise()
- return domainReadyPromise
- }
- /** 当前域名状态(调试用) */
- export function getDomainStatus() {
- const cache = readCache()
- return {
- ready: domainReady,
- parts: getDomainParts(),
- cache,
- cacheValid: isCacheValid(cache),
- }
- }
- /** 从 secure 完整 URL 解析 ho / dt / ht */
- export function parseSecureUrl(url) {
- if (!url || typeof url !== 'string') {
- throw new Error('无效的域名地址')
- }
- const normalized = url.startsWith('http') ? url : `https://${url}`
- const matched = normalized.match(/^(https?):\/\/([^/]+)/i)
- if (!matched) {
- throw new Error('无法解析域名地址')
- }
- const ht = `${matched[1]}:`
- const hostname = matched[2]
- const parts = hostname.split('.')
- if (parts.length < 3) {
- throw new Error(`域名格式不正确: ${hostname}`)
- }
- const dt = parts[parts.length - 1]
- const ho = parts[parts.length - 2]
- return { ho, dt, ht, secureUrl: `${ht}//${hostname}` }
- }
- export function readCache() {
- try {
- const raw = null
- if (!raw) return null
- return typeof raw === 'string' ? JSON.parse(raw) : raw
- } catch (e) {
- console.warn('[dynamicDomain] 读取缓存失败', e)
- return null
- }
- }
- function writeCache(payload) {
- uni.setStorageSync(DOMAIN_CACHE_KEY, payload)
- }
- export function isCacheValid(cache) {
- if (!cache?.ho || !cache?.dt || !cache?.expiresAt) return false
- return Date.now() < cache.expiresAt
- }
- function applyCacheToState(cache) {
- if (!cache?.ho || !cache?.dt) return false
- setDomainParts({ ho: cache.ho, dt: cache.dt, ht: cache.ht || 'https:' })
- return true
- }
- function parseResponseData(data) {
- if (!data) return null
- if (typeof data === 'string') {
- try {
- return JSON.parse(data)
- } catch (e) {
- return null
- }
- }
- return data
- }
- function requestJson(url, timeout = 8000) {
- return new Promise((resolve, reject) => {
- uni.request({
- url,
- method: 'GET',
- timeout,
- success: (res) => {
- if (res.statusCode !== 200) {
- reject(new Error(`请求失败: ${res.statusCode}`))
- return
- }
- const body = parseResponseData(res.data)
- if (!body) {
- reject(new Error('配置解析失败'))
- return
- }
- resolve(body)
- },
- fail: (err) => reject(err || new Error('网络请求失败')),
- })
- })
- }
- /** 快速探测 secure 域名是否可用 */
- export function probeSecureUrl(secureUrl, timeout = DOMAIN_PROBE_TIMEOUT) {
- const url = `${secureUrl.replace(/\/$/, '')}${DOMAIN_PROBE_PATH}?_t=${Date.now()}`
- return new Promise((resolve) => {
- uni.request({
- url,
- method: 'GET',
- timeout,
- success: (res) => resolve(res.statusCode === 200),
- fail: () => resolve(false),
- })
- })
- }
- async function pickActiveSecureUrl(remoteConfig) {
- const candidates = []
- if (remoteConfig?.primary) {
- candidates.push({ url: remoteConfig.primary, priority: 0 })
- }
- ;(remoteConfig?.backup || []).forEach((url, index) => {
- if (url) candidates.push({ url, priority: index + 1 })
- })
- if (!candidates.length) {
- throw new Error('远程域名配置为空')
- }
- const checks = await Promise.all(
- candidates.map(async (item) => ({
- ...item,
- ok: await probeSecureUrl(item.url),
- }))
- )
- const winner = checks
- .filter((item) => item.ok)
- .sort((a, b) => a.priority - b.priority)[0]
- if (!winner) {
- throw new Error('所有域名均不可用')
- }
- return winner.url
- }
- function applySecureUrl(secureUrl, meta = {}) {
- const parsed = parseSecureUrl(secureUrl)
- setDomainParts(parsed)
- const cache = {
- ho: parsed.ho,
- dt: parsed.dt,
- ht: parsed.ht,
- activeSecureUrl: parsed.secureUrl,
- version: meta.version || 1,
- cachedAt: Date.now(),
- expiresAt: Date.now() + DOMAIN_CACHE_TTL,
- }
- writeCache(cache)
- notifyDomainChanged(cache)
- console.log('[dynamicDomain] 当前生效域名', cache)
- return cache
- }
- async function fetchRemoteConfig() {
- const data = await requestJson(`${DOMAIN_CONFIG_URL}?_t=${Date.now()}`)
- if (!data?.primary) {
- throw new Error('远程配置缺少 primary 字段')
- }
- return data
- }
- /**
- * 解析并应用动态域名
- * @param {{ force?: boolean, verifyCache?: boolean }} options
- */
- export async function resolveDynamicDomain(options = {}) {
- const { force = false, verifyCache = true } = options
- const cache = readCache()
- if (!force && isCacheValid(cache)) {
- applyCacheToState(cache)
- if (verifyCache && cache.activeSecureUrl) {
- const ok = await probeSecureUrl(cache.activeSecureUrl, DOMAIN_CACHE_PROBE_TIMEOUT)
- if (ok) {
- console.log('[dynamicDomain] 缓存域名可用,跳过刷新')
- return cache
- }
- console.warn('[dynamicDomain] 缓存域名不可用,重新探测')
- } else if (!verifyCache) {
- return cache
- }
- } else if (cache?.ho && cache?.dt) {
- // 缓存过期:先用旧域名,再后台刷新
- applyCacheToState(cache)
- }
- if (resolving) return resolving
- resolving = (async () => {
- try {
- const remoteConfig = await fetchRemoteConfig()
- const activeSecureUrl = await pickActiveSecureUrl(remoteConfig)
- return applySecureUrl(activeSecureUrl, { version: remoteConfig.version })
- } catch (error) {
- if (cache?.ho && cache?.dt) {
- applyCacheToState(cache)
- console.warn('[dynamicDomain] 刷新失败,继续使用缓存/默认域名', error)
- return cache
- }
- console.warn('[dynamicDomain] 刷新失败,使用内置默认域名', error)
- throw error
- } finally {
- resolving = null
- }
- })()
- return resolving
- }
- function scheduleInitTimeout() {
- if (initTimeoutId) return
- initTimeoutId = setTimeout(() => {
- console.warn(`[dynamicDomain] 初始化超过 ${DOMAIN_INIT_TIMEOUT}ms,放行业务请求`)
- markDomainReady()
- }, DOMAIN_INIT_TIMEOUT)
- }
- /**
- * 尽早初始化(main.js 调用),避免页面请求早于 onLaunch
- * 探测/切换完成后再放行业务请求,避免缓存域名失效时先发错误请求
- */
- export function initDynamicDomain() {
- // #ifndef APP-PLUS
- return Promise.resolve(null)
- // #endif
- if (!domainReadyPromise) createDomainReadyPromise()
- const cache = readCache()
- scheduleInitTimeout()
- const task = isCacheValid(cache)
- ? resolveDynamicDomain({ force: false, verifyCache: true })
- : resolveDynamicDomain({ force: true, verifyCache: false })
- return task
- .catch((e) => {
- console.warn('[dynamicDomain] 初始化失败,使用默认/过期缓存域名', e)
- if (cache?.ho && cache?.dt) applyCacheToState(cache)
- return cache
- })
- .finally(() => {
- markDomainReady()
- console.log('[dynamicDomain] 域名就绪', buildActiveSecureUrl())
- })
- }
- function buildActiveSecureUrl() {
- const { ht, ho, dt } = getDomainParts()
- return `${ht}//secure.${ho}.${dt}`
- }
- /** App 再次进入前台:仅缓存过期时后台刷新 */
- export function refreshDynamicDomainIfExpired() {
- // #ifndef APP-PLUS
- return Promise.resolve(null)
- // #endif
- const cache = readCache()
- if (isCacheValid(cache)) {
- return Promise.resolve(cache)
- }
- return resolveDynamicDomain({ force: true, verifyCache: false })
- }
|