| | |
| | | /* eslint-disable @typescript-eslint/no-dynamic-delete */ |
| | | /** |
| | | * 百度地图 SDK 加载工具 |
| | | * |
| | | * ⚠️ 本部署环境(含客户端所在网络)IPv6 不通,而百度所有服务域名的 DNS |
| | | * 都优先返回 AAAA(IPv6)记录,浏览器优先连 IPv6 会立即失败—— |
| | | * 表现为 script 的 error 事件 1 秒内触发,地图完全加载不出来。 |
| | | * 服务器侧实测:curl -4 得到 HTTP 200 / 0.94s;curl -6 直接失败。 |
| | | * |
| | | * 因此这里加载的是「本站改写版 SDK」/bmap-sdk.js: |
| | | * 把 SDK 内部所有百度域名(api.map.baidu.com、apimaponline0-3.bdimg.com、 |
| | | * apisv0/1.bdimg.com、webmap0.bdimg.com、map.baidu.com …) |
| | | * 全部替换为 window.location.host + /bmap-xxx, |
| | | * 由服务器 nginx 用固定 IPv4 回源,客户端只访问本站 9023。 |
| | | * (改写脚本生成,见 nginx bmap-proxy.conf 与 public/bmap-sdk.js) |
| | | * |
| | | * 加载顺序: |
| | | * 1. /bmap-sdk.js —— 本站改写版(瓦片、接口也全部走代理) |
| | | * 2. /bmap-api/getscript —— 本站未改写代理(兜底) |
| | | * 3. https://api.map.baidu.com/getscript —— 直连百度(IPv6 通的环境可用) |
| | | * |
| | | * 其他注意:加载前要设 window.BMAP_PROTOCOL = 'https'; |
| | | * 不要给 script 加 async(SDK 依赖 document.currentScript); |
| | | * script 的事件名是 error 不是 onerror。 |
| | | */ |
| | | |
| | | // 扩展 Window 接口以包含百度地图 GL API |
| | | declare global { |
| | | interface Window { |
| | | BMapGL: any; |
| | | BMAP_PROTOCOL: any; |
| | | BMapGL_loadScriptTime: any; |
| | | } |
| | | } |
| | | |
| | | // 全局回调名称 |
| | | const CALLBACK_NAME = '__BAIDU_MAP_LOAD_CALLBACK__'; |
| | | |
| | | // SDK 加载状态 |
| | | let loadPromise: null | Promise<void> = null; |
| | | |
| | | /** 用指定地址加载一次 */ |
| | | function loadFrom(url: string, timeout: number): Promise<void> { |
| | | return new Promise((resolve, reject) => { |
| | | const timeoutId = setTimeout(() => { |
| | | reject(new Error(`百度地图 SDK 加载超时:${url}`)); |
| | | }, timeout); |
| | | |
| | | const script = document.createElement('script'); |
| | | script.src = url; |
| | | // 不要设 async:SDK 依赖 document.currentScript 定位自身 |
| | | |
| | | script.addEventListener('load', () => { |
| | | clearTimeout(timeoutId); |
| | | if (!window.BMapGL) { |
| | | reject(new Error(`脚本已下载但未挂载地图对象:${url}`)); |
| | | return; |
| | | } |
| | | resolve(); |
| | | }); |
| | | |
| | | // 事件名是 error,不是 onerror |
| | | script.addEventListener('error', () => { |
| | | clearTimeout(timeoutId); |
| | | reject(new Error(`加载失败:${url}`)); |
| | | }); |
| | | |
| | | document.body.append(script); |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 加载百度地图 GL SDK |
| | | * @param timeout 超时时间(毫秒),默认 10000 |
| | | * @param timeout 单次尝试的超时时间(毫秒),默认 10000 |
| | | * @returns Promise<void> |
| | | */ |
| | | export const loadBaiduMapSdk = (timeout = 10_000): Promise<void> => { |
| | | // 已加载完成 |
| | | if (window.BMapGL) { |
| | | return Promise.resolve(); |
| | | } |
| | | |
| | | // 正在加载中,返回同一个 Promise |
| | | if (loadPromise) { |
| | | return loadPromise; |
| | | } |
| | | |
| | | loadPromise = new Promise((resolve, reject) => { |
| | | const timeoutId = setTimeout(() => { |
| | | loadPromise = null; |
| | | reject(new Error('百度地图 SDK 加载超时')); |
| | | }, timeout); |
| | | const accessKey = String(import.meta.env.VITE_BAIDU_MAP_KEY ?? '').trim(); |
| | | if (!accessKey) { |
| | | return Promise.reject(new Error('未配置 VITE_BAIDU_MAP_KEY')); |
| | | } |
| | | |
| | | // 全局回调 |
| | | (window as any)[CALLBACK_NAME] = () => { |
| | | clearTimeout(timeoutId); |
| | | delete (window as any)[CALLBACK_NAME]; |
| | | resolve(); |
| | | }; |
| | | // 百度官方要求:先声明协议,避免 HTTPS 页面下 SDK 拼出 http 子资源地址 |
| | | window.BMAP_PROTOCOL = 'https'; |
| | | window.BMapGL_loadScriptTime = Date.now(); |
| | | |
| | | // 创建 script 标签 |
| | | const script = document.createElement('script'); |
| | | script.src = `https://api.map.baidu.com/api?v=1.0&type=webgl&ak=${ |
| | | import.meta.env.VITE_BAIDU_MAP_KEY |
| | | }&callback=${CALLBACK_NAME}`; |
| | | script.addEventListener('onerror', () => { |
| | | clearTimeout(timeoutId); |
| | | loadPromise = null; |
| | | delete (window as any)[CALLBACK_NAME]; |
| | | reject(new Error('百度地图 SDK 加载失败')); |
| | | }); |
| | | document.body.append(script); |
| | | }); |
| | | const query = `type=webgl&v=1.0&ak=${accessKey}&services=&t=${Date.now()}`; |
| | | const candidates = [ |
| | | // 本站改写版:内部域名全部指向本站代理,瓦片/接口都走 IPv4 |
| | | '/bmap-sdk.js', |
| | | // 本站代理的官方 SDK(未改写,瓦片仍指向百度,仅作兜底) |
| | | `/bmap-api/getscript?${query}`, |
| | | // 直连百度(仅 IPv6 正常的环境有效) |
| | | `https://api.map.baidu.com/getscript?${query}`, |
| | | ]; |
| | | |
| | | loadPromise = (async () => { |
| | | const errors: string[] = []; |
| | | for (const url of candidates) { |
| | | try { |
| | | await loadFrom(url, timeout); |
| | | return; |
| | | } catch (error: any) { |
| | | errors.push(error?.message ?? String(error)); |
| | | } |
| | | } |
| | | loadPromise = null; |
| | | throw new Error(`百度地图 SDK 加载失败:${errors.join(' | ')}`); |
| | | })(); |
| | | |
| | | return loadPromise; |
| | | }; |