tuqin
2 天以前 adffcb4ac80fdf736bde01eb56711c161bf602ba
src/components/map/src/utils.ts
@@ -1,63 +1,117 @@
/* 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 = window.location.protocol.replace(':', '');
  // v2.0 必需:官方 api?v=2.0 会设置 HOST_TYPE='2',直接用 getscript 时要自己补
  (window as any).HOST_TYPE = '2';
  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;
};