dynamicDomain.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. import {
  2. getDomainParts,
  3. setDomainParts,
  4. getDefaultRootDomain,
  5. DEFAULT_DOMAIN,
  6. } from '@/config/domainState'
  7. import {
  8. DOMAIN_CONFIG_URL,
  9. DOMAIN_INIT_TIMEOUT,
  10. DOMAIN_PROBE_PATH,
  11. DOMAIN_PROBE_TIMEOUT,
  12. DOMAIN_HEARTBEAT_INTERVAL,
  13. } from '@/config/domainConstants'
  14. let resolving = null
  15. let domainReady = false
  16. let domainReadyPromise = null
  17. let domainReadyResolve = null
  18. let initTimeoutId = null
  19. let heartbeatTimer = null
  20. function createDomainReadyPromise() {
  21. domainReadyPromise = new Promise((resolve) => {
  22. domainReadyResolve = resolve
  23. })
  24. }
  25. function markDomainReady() {
  26. if (domainReady) return
  27. domainReady = true
  28. if (initTimeoutId) {
  29. clearTimeout(initTimeoutId)
  30. initTimeoutId = null
  31. }
  32. domainReadyResolve?.()
  33. }
  34. function buildActiveSecureUrl() {
  35. const { ht, ho, dt } = getDomainParts()
  36. return `${ht}//secure.${ho}.${dt}`
  37. }
  38. function notifyDomainChanged(parts) {
  39. try {
  40. uni.$emit('domain-changed', {
  41. ...parts,
  42. activeSecureUrl: parts.secureUrl || buildActiveSecureUrl(),
  43. })
  44. } catch (e) {
  45. // ignore
  46. }
  47. }
  48. /** 业务请求需等待域名就绪(仅 APP) */
  49. export function whenDomainReady() {
  50. // #ifndef APP-PLUS
  51. return Promise.resolve()
  52. // #endif
  53. if (domainReady) return Promise.resolve()
  54. if (!domainReadyPromise) createDomainReadyPromise()
  55. return domainReadyPromise
  56. }
  57. /** 当前域名状态(调试用) */
  58. export function getDomainStatus() {
  59. return {
  60. ready: domainReady,
  61. parts: getDomainParts(),
  62. activeSecureUrl: buildActiveSecureUrl(),
  63. defaultRoot: getDefaultRootDomain(),
  64. defaultSecureUrl: `https://secure.${DEFAULT_DOMAIN.ho}.${DEFAULT_DOMAIN.dt}`,
  65. }
  66. }
  67. /**
  68. * 解析根域名配置项
  69. * 支持:cwgbroker.club / secure.cwgbroker.club / https://secure.cwgbroker.club
  70. */
  71. export function parseRootDomain(input) {
  72. if (!input || typeof input !== 'string') {
  73. throw new Error('无效的域名')
  74. }
  75. let host = input.trim()
  76. host = host.replace(/^https?:\/\//i, '')
  77. host = host.split('/')[0]
  78. host = host.replace(/^secure\./i, '')
  79. const parts = host.split('.')
  80. if (parts.length < 2) {
  81. throw new Error(`域名格式不正确: ${input}`)
  82. }
  83. const dt = parts[parts.length - 1]
  84. const ho = parts[parts.length - 2]
  85. const ht = 'https:'
  86. return {
  87. ho,
  88. dt,
  89. ht,
  90. root: `${ho}.${dt}`,
  91. secureUrl: `${ht}//secure.${ho}.${dt}`,
  92. }
  93. }
  94. /** @deprecated 兼容旧调用 */
  95. export function parseSecureUrl(url) {
  96. return parseRootDomain(url)
  97. }
  98. function parseResponseData(data) {
  99. if (!data) return null
  100. if (typeof data === 'string') {
  101. try {
  102. return JSON.parse(data)
  103. } catch (e) {
  104. return null
  105. }
  106. }
  107. return data
  108. }
  109. function requestJson(url, timeout = 8000) {
  110. return new Promise((resolve, reject) => {
  111. uni.request({
  112. url,
  113. method: 'GET',
  114. timeout,
  115. success: (res) => {
  116. if (res.statusCode !== 200) {
  117. reject(new Error(`请求失败: ${res.statusCode}`))
  118. return
  119. }
  120. const body = parseResponseData(res.data)
  121. if (!body) {
  122. reject(new Error('配置解析失败'))
  123. return
  124. }
  125. resolve(body)
  126. },
  127. fail: (err) => reject(err || new Error('网络请求失败')),
  128. })
  129. })
  130. }
  131. /** 探测 secure 子域是否可用 */
  132. export function probeSecureUrl(secureUrl, timeout = DOMAIN_PROBE_TIMEOUT) {
  133. const url = `${secureUrl.replace(/\/$/, '')}${DOMAIN_PROBE_PATH}?_t=${Date.now()}`
  134. return new Promise((resolve) => {
  135. uni.request({
  136. url,
  137. method: 'GET',
  138. timeout,
  139. success: (res) => resolve(res.statusCode === 200),
  140. fail: () => resolve(false),
  141. })
  142. })
  143. }
  144. async function pickActiveRootDomain(remoteConfig) {
  145. const candidates = []
  146. if (remoteConfig?.primary) {
  147. candidates.push({ root: remoteConfig.primary, priority: 0 })
  148. }
  149. ;(remoteConfig?.backup || []).forEach((root, index) => {
  150. if (root) candidates.push({ root, priority: index + 1 })
  151. })
  152. if (!candidates.length) {
  153. throw new Error('远程域名配置为空')
  154. }
  155. const checks = await Promise.all(
  156. candidates.map(async (item) => {
  157. const parsed = parseRootDomain(item.root)
  158. return {
  159. ...item,
  160. parsed,
  161. ok: await probeSecureUrl(parsed.secureUrl),
  162. }
  163. })
  164. )
  165. const winner = checks
  166. .filter((item) => item.ok)
  167. .sort((a, b) => a.priority - b.priority)[0]
  168. if (!winner) {
  169. console.warn('[dynamicDomain] primary/backup 均不可用,回退代码默认域名')
  170. return getDefaultRootDomain()
  171. }
  172. return winner.root
  173. }
  174. function applyRootDomain(root, meta = {}) {
  175. const parsed = parseRootDomain(root)
  176. const before = getDomainParts()
  177. setDomainParts(parsed)
  178. const changed = before.ho !== parsed.ho || before.dt !== parsed.dt
  179. if (changed) {
  180. notifyDomainChanged(parsed)
  181. console.log('[dynamicDomain] 域名已切换', {
  182. from: `${before.ho}.${before.dt}`,
  183. to: parsed.root,
  184. secureUrl: parsed.secureUrl,
  185. version: meta.version,
  186. fallback: meta.fallback || false,
  187. })
  188. } else {
  189. console.log('[dynamicDomain] 域名可用', parsed.secureUrl)
  190. }
  191. return parsed
  192. }
  193. /** primary/backup 均不可用或拉配置失败时,回退代码内置默认域名 */
  194. function applyDefaultDomain(meta = {}) {
  195. return applyRootDomain(getDefaultRootDomain(), { ...meta, fallback: true })
  196. }
  197. async function fetchRemoteConfig() {
  198. const data = await requestJson(`${DOMAIN_CONFIG_URL}?_t=${Date.now()}`)
  199. if (!data?.primary) {
  200. throw new Error('远程配置缺少 primary 字段')
  201. }
  202. return data
  203. }
  204. /** 拉取配置并探测可用根域名 */
  205. export async function resolveDynamicDomain() {
  206. if (resolving) return resolving
  207. resolving = (async () => {
  208. try {
  209. const remoteConfig = await fetchRemoteConfig()
  210. const activeRoot = await pickActiveRootDomain(remoteConfig)
  211. return applyRootDomain(activeRoot, { version: remoteConfig.version })
  212. } catch (error) {
  213. console.warn('[dynamicDomain] 解析失败,回退代码默认域名', error)
  214. return applyDefaultDomain()
  215. } finally {
  216. resolving = null
  217. }
  218. })()
  219. return resolving
  220. }
  221. function scheduleInitTimeout() {
  222. if (initTimeoutId) return
  223. initTimeoutId = setTimeout(() => {
  224. console.warn(`[dynamicDomain] 初始化超过 ${DOMAIN_INIT_TIMEOUT}ms,放行业务请求`)
  225. markDomainReady()
  226. }, DOMAIN_INIT_TIMEOUT)
  227. }
  228. /** 启动时解析域名(main.js 调用) */
  229. export function initDynamicDomain() {
  230. // #ifndef APP-PLUS
  231. return Promise.resolve(null)
  232. // #endif
  233. if (!domainReadyPromise) createDomainReadyPromise()
  234. try {
  235. uni.removeStorageSync('app-dynamic-domain')
  236. } catch (e) {
  237. // ignore
  238. }
  239. scheduleInitTimeout()
  240. return resolveDynamicDomain()
  241. .finally(() => {
  242. markDomainReady()
  243. startDomainHeartbeat()
  244. console.log('[dynamicDomain] 域名就绪', buildActiveSecureUrl())
  245. })
  246. }
  247. /** 每次 App 进入前台时重新检测 */
  248. export function refreshDynamicDomainOnShow() {
  249. // #ifndef APP-PLUS
  250. return Promise.resolve(null)
  251. // #endif
  252. if (!domainReady) {
  253. return whenDomainReady()
  254. }
  255. return resolveDynamicDomain().catch((e) => {
  256. console.warn('[dynamicDomain] 前台刷新失败', e)
  257. })
  258. }
  259. /** App 运行期间定时心跳检测(10 分钟) */
  260. export function startDomainHeartbeat() {
  261. // #ifndef APP-PLUS
  262. return
  263. // #endif
  264. stopDomainHeartbeat()
  265. heartbeatTimer = setInterval(() => {
  266. console.log('[dynamicDomain] 心跳检测')
  267. resolveDynamicDomain().catch((e) => {
  268. console.warn('[dynamicDomain] 心跳检测失败', e)
  269. })
  270. }, DOMAIN_HEARTBEAT_INTERVAL)
  271. }
  272. export function stopDomainHeartbeat() {
  273. if (heartbeatTimer) {
  274. clearInterval(heartbeatTimer)
  275. heartbeatTimer = null
  276. }
  277. }