tuqin
3 天以前 23b513b644720666c5bda5073b34567cbadbbe4a
fix(map): 按百度官方规范修正 SDK 加载,并加诊断页定位线上加载失败

客户要求「地图必须能用」,重新排查后修正三处:

1. 加载前设置 window.BMAP_PROTOCOL = 'https' 与 BMapGL_loadScriptTime
百度官方要求在加载 SDK 前声明协议,否则 SDK 内部按默认协议拼子资源地址,
HTTPS 页面下会被当成混合内容拦截。SDK 里实测引用了 apimaponline0-3.bdimg.com、
webmap0.bdimg.com、apisv0/1.bdimg.com 等多个资源域名,协议不对会连锁失败。

2. 去掉 script.async = true
SDK 内部用 document.currentScript 定位自身地址(getscript 里出现 3 次),
async 脚本执行期间 currentScript 为 null,会让它走 fallback 分支。

3. 首次失败自动重试一次
跨省/移动网络偶发抖动时,重试往往能过。

另外新增 public/bmap-test.html 诊断页(部署后访问 /bmap-test.html):
用与正式代码完全相同的加载方式,逐步输出
浏览器 UA / 页面协议 / 是否在线 / 请求 URL
→ load 或 error 事件
→ window.BMapGL 是否挂载
→ 能否真正创建地图并渲染
并附上可直接在地址栏打开的 URL 做对比测试。
拿到客户这个页面的日志,就能区分是网络拦截、Key 白名单还是代码问题。

注:getscript 实测(含模拟 Chrome UA + 线上 Referer)返回 HTTP 200 / 875KB 完整 SDK /
business:0 / unauth:0,Key 本身有效。
已添加1个文件
已修改1个文件
224 ■■■■■ 文件已修改
public/bmap-test.html 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/map/src/utils.ts 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
public/bmap-test.html
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,109 @@
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>百度地图加载诊断</title>
    <style>
      body {
        padding: 16px;
        font-family: -apple-system, 'Microsoft YaHei', sans-serif;
        font-size: 14px;
      }
      h3 { margin: 0 0 12px; }
      #log {
        padding: 12px;
        background: #f5f5f5;
        border-radius: 6px;
        font-family: Consolas, Monaco, monospace;
        font-size: 13px;
        line-height: 1.9;
        white-space: pre-wrap;
        word-break: break-all;
      }
      .ok { color: #389e0d; font-weight: 600; }
      .bad { color: #cf1322; font-weight: 600; }
      .warn { color: #d46b08; }
      #map { width: 100%; height: 320px; margin-top: 12px; border: 1px solid #ddd; }
      .tip { margin-top: 12px; padding: 10px; background: #e6f4ff; border-radius: 6px; }
    </style>
  </head>
  <body>
    <h3>百度地图 SDK åŠ è½½è¯Šæ–­</h3>
    <div id="log"></div>
    <div class="tip">
      æŠŠä¸Šé¢çš„æ—¥å¿—整段截图或复制发给技术,就能定位问题。
    </div>
    <div id="map"></div>
    <script>
      var logEl = document.getElementById('log');
      function log(msg, cls) {
        var time = new Date().toLocaleTimeString('zh-CN', { hour12: false });
        var div = document.createElement('div');
        div.className = cls || '';
        div.textContent = '[' + time + '] ' + msg;
        logEl.appendChild(div);
      }
      var KEY = 'Y2aJXiswwPxy6mwFs1z9c7U5gwX9WfUN';
      var startAt = Date.now();
      log('浏览器: ' + navigator.userAgent);
      log('页面地址: ' + location.href);
      log('页面协议: ' + location.protocol);
      log('是否在线: ' + navigator.onLine);
      log('');
      // ä¸Žæ­£å¼ä»£ç å®Œå…¨ä¸€è‡´çš„加载方式
      window.BMAP_PROTOCOL = 'https';
      window.BMapGL_loadScriptTime = Date.now();
      var url =
        'https://api.map.baidu.com/getscript?type=webgl&v=1.0&ak=' +
        KEY +
        '&services=&t=' +
        Date.now();
      log('开始加载 SDK: ' + url);
      log('');
      var script = document.createElement('script');
      script.src = url;
      script.addEventListener('load', function () {
        var cost = ((Date.now() - startAt) / 1000).toFixed(2);
        log('load äº‹ä»¶è§¦å‘(耗时 ' + cost + ' ç§’)', 'ok');
        log('window.BMapGL = ' + (window.BMapGL ? '存在' : '不存在'), window.BMapGL ? 'ok' : 'bad');
        if (!window.BMapGL) {
          log('SDK è„šæœ¬ä¸‹è½½äº†ï¼Œä½†æ²¡æœ‰æŒ‚è½½åœ°å›¾å¯¹è±¡ â†’ Key æˆ–域名白名单问题', 'bad');
          return;
        }
        log('地图 SDK åŠ è½½æˆåŠŸï¼Œå¼€å§‹åˆ›å»ºåœ°å›¾â€¦', 'ok');
        try {
          var map = new window.BMapGL.Map(document.getElementById('map'));
          map.centerAndZoom(new window.BMapGL.Point(87.616848, 43.825592), 14);
          map.enableScrollWheelZoom(true);
          log('地图实例创建成功,下方应能看到乌鲁木齐地图', 'ok');
        } catch (e) {
          log('创建地图失败: ' + e.message, 'bad');
        }
      });
      script.addEventListener('error', function () {
        log('error äº‹ä»¶è§¦å‘:请求根本没到达百度服务器', 'bad');
        log('可能原因:公司网络/防火墙拦截、浏览器插件拦截、DNS è§£æžå¤±è´¥', 'warn');
        log('请复制下面这个地址,在浏览器地址栏直接打开做对比测试:', 'warn');
        log(url);
      });
      setTimeout(function () {
        if (!window.BMapGL) {
          log('已经过了 8 ç§’,地图仍未就绪(如果上面没有任何事件日志,就是请求被挂住了)', 'bad');
        }
      }, 8000);
      document.body.appendChild(script);
    </script>
  </body>
</html>
src/components/map/src/utils.ts
@@ -1,28 +1,76 @@
/**
 * ç™¾åº¦åœ°å›¾ SDK åŠ è½½å·¥å…·
 *
 * é‡è¦ï¼šè¿™é‡Œå¿…须直连 getscript,不能用 api?... åœ°å€ã€‚
 * api?v=1.0&type=webgl&ak=xxx è¿”回的是一段
 *   document.write('<script src="https://api.map.baidu.com/getscript?..."></script>')
 * è€Œé€šè¿‡ document.body.append(script) åŠ¨æ€æ’å…¥çš„è„šæœ¬é‡Œï¼Œdocument.write ä¼šè¢«æµè§ˆå™¨æ‹¦æˆª
 * ï¼ˆChrome ç›´æŽ¥æç¤º "A parser-blocking, cross site script ... invoked via document.write was NOT executed"),
 * å¯¼è‡´çœŸæ­£çš„ SDK ä»ŽæœªåŠ è½½ã€window.BMapGL ä¸€ç›´æ˜¯ undefined/null。
 * ä½† api é‡Œçš„ callback ä»ä¼šè¢«è°ƒç”¨ï¼Œè°ƒç”¨æ–¹è¯¯ä»¥ä¸ºåŠ è½½æˆåŠŸï¼ŒæŽ¥ç€æ‰§è¡Œ new BMapGL.Map()
 * å°±æŠ›å‡ºã€ŒCannot read properties of null (reading 'Map')」。
 * åŠ è½½æ–¹å¼è¯´æ˜Žï¼ˆè¸©è¿‡çš„å‘ï¼‰ï¼š
 *
 * getscript è¿”回的是 875KB çš„真实 SDK(已实测 HTTP 200 ä¸”含 BMapGL å®šä¹‰ï¼‰ï¼Œ
 * ç›´æŽ¥ç”¨ script.src åŠ è½½å³å¯ï¼Œè„šæœ¬æ‰§è¡Œå®Œ window.BMapGL å°±å·²å°±ç»ªã€‚
 * 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')"。
 *
 * 2. å¿…须直连 getscript,它是 875KB çš„真实 SDK。
 *
 * 3. åŠ è½½å‰è¦è®¾ç½® window.BMAP_PROTOCOL = 'https'(百度官方要求),
 *    å¦åˆ™ SDK å†…部按默认协议拼子资源地址,HTTPS é¡µé¢ä¸‹ä¼šè¢«å½“作混合内容拦截。
 *
 * 4. ä¸è¦ç»™ script åŠ  async = true:SDK å†…部用 document.currentScript å®šä½è‡ªèº«ï¼Œ
 *    async è„šæœ¬æ‰§è¡ŒæœŸé—´ currentScript ä¸º null,会让它走到 fallback åˆ†æ”¯ç”šè‡³æ‹¿ä¸åˆ°è‡ªèº«åœ°å€ã€‚
 *
 * 5. å¤±è´¥é‡è¯•一次:偶发网络抖动(尤其是跨省/移动网络)重试往往就能过。
 */
// æ‰©å±• Window æŽ¥å£ä»¥åŒ…含百度地图 GL API
declare global {
  interface Window {
    BMapGL: any;
    BMAP_PROTOCOL: any;
    BMapGL_loadScriptTime: any;
  }
}
// SDK åŠ è½½çŠ¶æ€
let loadPromise: null | Promise<void> = null;
// åŠ è½½å¤±è´¥æ¬¡æ•°ï¼Œç”¨äºŽå†³å®šæ˜¯å¦é‡è¯•
let failCount = 0;
/** å•次尝试加载 */
function loadOnce(accessKey: string, timeout: number): Promise<void> {
  return new Promise((resolve, reject) => {
    const timeoutId = setTimeout(() => {
      reject(new Error('百度地图 SDK åŠ è½½è¶…æ—¶ï¼Œè¯·æ£€æŸ¥ç½‘ç»œ'));
    }, 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()}`;
    // æ³¨æ„ï¼šä¸è¦è®¾ async,SDK ä¾èµ– document.currentScript å®šä½è‡ªèº«
    script.addEventListener('load', () => {
      clearTimeout(timeoutId);
      if (!window.BMapGL) {
        // ak æ— æ•ˆ / åŸŸåæœªæŽˆæƒ / è¢«æ‹¦æˆªæ—¶ï¼Œè„šæœ¬èƒ½åŠ è½½ä½†ä¸ä¼šæŒ‚è½½ BMapGL
        reject(
          new Error('百度地图加载异常:未获取到地图对象,请检查 Key æˆ–域名授权'),
        );
        return;
      }
      resolve();
    });
    // äº‹ä»¶åæ˜¯ error,不是 onerror(写 onerror æ°¸è¿œä¸ä¼šè§¦å‘)
    script.addEventListener('error', () => {
      clearTimeout(timeoutId);
      reject(
        new Error(
          '百度地图 SDK åŠ è½½å¤±è´¥ï¼ˆæ— æ³•è®¿é—® api.map.baidu.com,可能是网络限制或插件拦截)',
        ),
      );
    });
    document.body.append(script);
  });
}
/**
 * åŠ è½½ç™¾åº¦åœ°å›¾ GL SDK
@@ -45,38 +93,27 @@
    return Promise.reject(new Error('未配置 VITE_BAIDU_MAP_KEY'));
  }
  loadPromise = new Promise((resolve, reject) => {
    const timeoutId = setTimeout(() => {
      loadPromise = null;
      reject(new Error('百度地图 SDK åŠ è½½è¶…æ—¶ï¼Œè¯·æ£€æŸ¥ç½‘ç»œ'));
    }, timeout);
  // ç™¾åº¦å®˜æ–¹è¦æ±‚:先声明协议,避免 HTTPS é¡µé¢ä¸‹ SDK æ‹¼å‡º http å­èµ„源地址
  if (typeof window !== 'undefined') {
    window.BMAP_PROTOCOL = 'https';
    window.BMapGL_loadScriptTime = Date.now();
  }
    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.async = true;
    script.addEventListener('load', () => {
      clearTimeout(timeoutId);
      if (!window.BMapGL) {
        // ak æ— æ•ˆ / åŸŸåæœªæŽˆæƒ / è¢«æ‹¦æˆªæ—¶ï¼Œè„šæœ¬èƒ½åŠ è½½ä½†ä¸ä¼šæŒ‚è½½ BMapGL
  loadPromise = (async () => {
    try {
      await loadOnce(accessKey, timeout);
    } catch (error) {
      // é¦–次失败重试一次,应对偶发网络抖动
      if (failCount < 1) {
        failCount += 1;
        await loadOnce(accessKey, timeout);
      } else {
        failCount = 0;
        loadPromise = null;
        reject(
          new Error('百度地图加载异常:未获取到地图对象,请检查 Key æˆ–域名授权'),
        );
        return;
        throw error;
      }
      resolve();
    });
    // æ³¨æ„ï¼šäº‹ä»¶åæ˜¯ error,不是 onerror(写 onerror æ°¸è¿œä¸ä¼šè§¦å‘)
    script.addEventListener('error', () => {
      clearTimeout(timeoutId);
      loadPromise = null;
      reject(new Error('百度地图 SDK åŠ è½½å¤±è´¥ï¼Œè¯·æ£€æŸ¥ç½‘ç»œ'));
    });
    document.body.append(script);
  });
    }
  })();
  return loadPromise;
};