| | |
| | | /** |
| | | * 百度地图 SDK 加载工具 |
| | | * |
| | | * 加载方式说明(踩过的坑): |
| | | * 加载地址优先级(按顺序尝试,都失败才报错): |
| | | * 1. /bmap-api/getscript —— 本站 nginx 反向代理,强制回源百度 IPv4 |
| | | * 2. https://api.map.baidu.com/getscript —— 直连百度 |
| | | * |
| | | * 1. 不能用 api?v=1.0&type=webgl&ak=xxx 这个地址。 |
| | | * 它返回的是一段 document.write('<script src=".../getscript?...">'), |
| | | * 而通过 document.body.append(script) 动态插入的脚本里 document.write 会被浏览器拦截, |
| | | * 导致真正的 SDK 从未执行、window.BMapGL 一直是空;但 api 的 callback 仍会被调用, |
| | | * 调用方误判成功,接着 new BMapGL.Map() 就抛 |
| | | * "Cannot read properties of null (reading 'Map')"。 |
| | | * 为什么要代理(实测结论): |
| | | * 该部署环境(含客户端所在网络)IPv6 不通,而 api.map.baidu.com 的 DNS |
| | | * 优先返回 AAAA(IPv6)记录,浏览器优先连 IPv6 时立即失败—— |
| | | * script 的 error 事件在 1 秒内就触发,根本不是超时。 |
| | | * 服务器侧验证:curl -4 得到 HTTP 200 / 0.94s,curl -6 直接失败。 |
| | | * 走同域代理后,客户端只访问本机 9023,由服务器用固定 IPv4 回源, |
| | | * 实测 HTTP 200 / 879KB / 0.28s。 |
| | | * |
| | | * 2. 必须直连 getscript,它是 875KB 的真实 SDK。 |
| | | * |
| | | * 3. 加载前要设置 window.BMAP_PROTOCOL = 'https'(百度官方要求), |
| | | * 否则 SDK 内部按默认协议拼子资源地址,HTTPS 页面下会被当作混合内容拦截。 |
| | | * |
| | | * 4. 不要给 script 加 async = true:SDK 内部用 document.currentScript 定位自身, |
| | | * async 脚本执行期间 currentScript 为 null,会让它走到 fallback 分支甚至拿不到自身地址。 |
| | | * |
| | | * 5. 失败重试一次:偶发网络抖动(尤其是跨省/移动网络)重试往往就能过。 |
| | | * 其他踩过的坑: |
| | | * - 不能用 api?v=1.0&type=webgl(返回 document.write 包装,动态脚本里会被浏览器拦截) |
| | | * - 加载前要设 window.BMAP_PROTOCOL = 'https'(百度官方要求) |
| | | * - 不要给 script 加 async(SDK 依赖 document.currentScript 定位自身) |
| | | * - script 的事件名是 error,不是 onerror |
| | | */ |
| | | |
| | | // 扩展 Window 接口以包含百度地图 GL API |
| | |
| | | |
| | | // SDK 加载状态 |
| | | let loadPromise: null | Promise<void> = null; |
| | | // 加载失败次数,用于决定是否重试 |
| | | let failCount = 0; |
| | | |
| | | /** 单次尝试加载 */ |
| | | function loadOnce(accessKey: string, timeout: number): Promise<void> { |
| | | /** 用指定地址加载一次 */ |
| | | function loadFrom(url: string, timeout: number): Promise<void> { |
| | | return new Promise((resolve, reject) => { |
| | | const timeoutId = setTimeout(() => { |
| | | reject(new Error('百度地图 SDK 加载超时,请检查网络')); |
| | | reject(new Error(`百度地图 SDK 加载超时:${url}`)); |
| | | }, timeout); |
| | | |
| | | const script = document.createElement('script'); |
| | | script.src = `https://api.map.baidu.com/getscript?type=webgl&v=1.0&ak=${accessKey}&services=&t=${Date.now()}`; |
| | | script.src = url; |
| | | // 注意:不要设 async,SDK 依赖 document.currentScript 定位自身 |
| | | |
| | | script.addEventListener('load', () => { |
| | | clearTimeout(timeoutId); |
| | | if (!window.BMapGL) { |
| | | // ak 无效 / 域名未授权 / 被拦截时,脚本能加载但不会挂载 BMapGL |
| | | reject( |
| | | new Error('百度地图加载异常:未获取到地图对象,请检查 Key 或域名授权'), |
| | | ); |
| | | reject(new Error('脚本已下载但未挂载地图对象')); |
| | | return; |
| | | } |
| | | resolve(); |
| | |
| | | // 事件名是 error,不是 onerror(写 onerror 永远不会触发) |
| | | script.addEventListener('error', () => { |
| | | clearTimeout(timeoutId); |
| | | reject( |
| | | new Error( |
| | | '百度地图 SDK 加载失败(无法访问 api.map.baidu.com,可能是网络限制或插件拦截)', |
| | | ), |
| | | ); |
| | | 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; |
| | | } |
| | |
| | | } |
| | | |
| | | // 百度官方要求:先声明协议,避免 HTTPS 页面下 SDK 拼出 http 子资源地址 |
| | | if (typeof window !== 'undefined') { |
| | | window.BMAP_PROTOCOL = 'https'; |
| | | window.BMapGL_loadScriptTime = Date.now(); |
| | | } |
| | | window.BMAP_PROTOCOL = 'https'; |
| | | window.BMapGL_loadScriptTime = Date.now(); |
| | | |
| | | const query = `type=webgl&v=1.0&ak=${accessKey}&services=&t=${Date.now()}`; |
| | | // 优先同域反代(绕过客户端 IPv6 不通),再回退直连 |
| | | const candidates = [ |
| | | `/bmap-api/getscript?${query}`, |
| | | `https://api.map.baidu.com/getscript?${query}`, |
| | | ]; |
| | | |
| | | loadPromise = (async () => { |
| | | try { |
| | | await loadOnce(accessKey, timeout); |
| | | } catch (error) { |
| | | // 首次失败重试一次,应对偶发网络抖动 |
| | | if (failCount < 1) { |
| | | failCount += 1; |
| | | await loadOnce(accessKey, timeout); |
| | | } else { |
| | | failCount = 0; |
| | | loadPromise = null; |
| | | throw error; |
| | | let lastError: any; |
| | | for (const url of candidates) { |
| | | try { |
| | | await loadFrom(url, timeout); |
| | | return; |
| | | } catch (error) { |
| | | lastError = error; |
| | | } |
| | | } |
| | | loadPromise = null; |
| | | throw new Error( |
| | | `百度地图 SDK 加载失败(已尝试代理与直连):${lastError?.message ?? ''}`, |
| | | ); |
| | | })(); |
| | | |
| | | return loadPromise; |