tuqin
3 天以前 3b8ba54ea299a9febdf3a989a72b4eba311be796
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/**
 * 百度地图 SDK 加载工具
 *
 * 加载地址优先级(按顺序尝试,都失败才报错):
 *   1. /bmap-api/getscript  —— 本站 nginx 反向代理,强制回源百度 IPv4
 *   2. https://api.map.baidu.com/getscript —— 直连百度
 *
 * 为什么要代理(实测结论):
 *   该部署环境(含客户端所在网络)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。
 *
 * 其他踩过的坑:
 *   - 不能用 api?v=1.0&type=webgl(返回 document.write 包装,动态脚本里会被浏览器拦截)
 *   - 加载前要设 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;
  }
}
 
// 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('脚本已下载但未挂载地图对象'));
        return;
      }
      resolve();
    });
 
    // 事件名是 error,不是 onerror(写 onerror 永远不会触发)
    script.addEventListener('error', () => {
      clearTimeout(timeoutId);
      reject(new Error(`加载失败:${url}`));
    });
 
    document.body.append(script);
  });
}
 
/**
 * 加载百度地图 GL SDK
 * @param timeout 单次尝试的超时时间(毫秒),默认 10000
 * @returns Promise<void>
 */
export const loadBaiduMapSdk = (timeout = 10_000): Promise<void> => {
  if (window.BMapGL) {
    return Promise.resolve();
  }
  if (loadPromise) {
    return loadPromise;
  }
 
  const accessKey = String(import.meta.env.VITE_BAIDU_MAP_KEY ?? '').trim();
  if (!accessKey) {
    return Promise.reject(new Error('未配置 VITE_BAIDU_MAP_KEY'));
  }
 
  // 百度官方要求:先声明协议,避免 HTTPS 页面下 SDK 拼出 http 子资源地址
  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 () => {
    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;
};