前端重连机制

在前端的重连机制方案中,最暴力的就是直接使用 window.location.reload() ,但是这可能会导致用户陷入无限刷新的死循环(比如用户断网了,刷新多少次也加载不出来),同时也非常消耗性能和用户流量。

我们可以从重试机制、状态反馈、错误拦截三个维度来进行优化。下面以 mapbox 的底图加载失败为案例进行测试。

1. 指数退避的自动重试

不要立即刷新,而是尝试在代码层面重新加载地图资源。如果重试多次失败,再引导用户手动刷新。

js 复制代码
let retryCount = 0; // vue 组件中的 script 只会在 setup 阶段渲染一次,不是响应式的状态可以直接用 let,react 每次渲染的时候,函数体都会重新执行,需要用 ref 维持
const MAX_RETRIES = 3;

this.mapInstance.on('error', (error) => {
  console.error('地图加载失败', error);

  if (retryCount < MAX_RETRIES) {
    retryCount++;
    const delay = Math.pow(2, retryCount) * 1000; // 2s, 4s, 8s 后重试
    
    console.warn(`正在进行第 ${retryCount} 次尝试重连...`);
    setTimeout(() => {
      // 尝试重新设置样式或触发加载逻辑,而不是刷新整个页面
      this.mapInstance.setStyle(this.currentStyleUrl); 
    }, delay);
  } else {
    // 超过次数后,显示 UI 提示而不是硬刷新
    this.showErrorUI('网络状况不佳,请检查网络后手动刷新');
  }
});

2. 添加 UI 状态遮罩|UI降级(防止白屏感)

与其让页面白着,不如给用户一个明确的反馈。在初始化地图时显示一个 Loading,如果加载失败,切换为"刷新按钮"。

  • 加载中: 显示骨架屏或 Loading 动画。
  • 失败后: 移除 Loading,显示"加载失败,点击重试"按钮。这样避免了自动刷新的突兀感。

3. 检测特定错误类型

MapBox 的 error 事件会捕获各种错误(包括样式错误、Tile 请求失败等)。有些错误不值得刷新页面:

kotlin 复制代码
this.mapInstance.on('error', (e) => {
  // 如果只是某个瓦片加载 404,不应该刷新页面
  if (e.error?.status === 404) return;

  // 如果是鉴权问题,刷新也没用
  if (e.error?.status === 401) {
    console.error('MapBox Token 无效');
    return;
  }

  // 针对核心资源加载失败的处理
  this.handleCriticalError();
});

4. 离线/断网检测

利用浏览器的原生事件,在网络恢复的第一时间尝试重载。

kotlin 复制代码
window.addEventListener('online', () => {
  if (this.mapLoadFailed) {
    this.mapInstance.setStyle(this.currentStyleUrl);
  }
});

5. 综合方案

将逻辑封装成一个状态机

  1. 状态 A (Loading): 页面中心显示转圈。

  2. 状态 B (Success): 监听到 loadidle 事件,隐藏转圈。

  3. 状态 C (Error): 监听到 error

    • 记录重试次数。
    • 若次数未满:微调样式 URL 触发重新请求(或调用 map.remove() 后重新 new Mapboxgl.Map())。
    • 若次数已满:弹出一个浮窗提示:"地图资源加载超时 [重试按钮]"。
js 复制代码
// 状态配置
const MAP_CONFIG = {
  maxRetries: 3,         // 最大自动重试次数
  retryDelay: 3000,      // 基础重试延迟 (ms)
  isRecovering: false,   // 是否正在恢复中
  retryCount: 0
};

this.mapInstance.on('error', (e) => {
  // 1. 过滤掉非致命错误(例如某个瓦片 404 不应该导致重刷)
  if (e.error?.status === 404 || e.error?.status === 403) {
    console.warn('忽略非致命瓦片加载错误');
    return;
  }

  console.error('地图核心资源加载失败:', e.error);

  // 2. 自动重试逻辑
  if (MAP_CONFIG.retryCount < MAP_CONFIG.maxRetries) {
    MAP_CONFIG.retryCount++;
    
    // 使用简单的指数退避,避免频繁冲击服务器
    const delay = MAP_CONFIG.retryCount * MAP_CONFIG.retryDelay;
    
    console.log(`将在 ${delay}ms 后尝试第 ${MAP_CONFIG.retryCount} 次自动修复...`);
    
    setTimeout(() => {
      // 核心技巧:重新触发 style 加载通常比 reload 页面更轻量
      // 如果 mapInstance 还没坏死,尝试 resetStyle
      this.mapInstance.setStyle(this.currentStyleUrl);
    }, delay);

  } else {
    // 3. 最终降级处理:弹出友好的提示层而非强制刷新
    this.showMapErrorOverlay();
  }
});

// 4. 监听网络恢复
window.addEventListener('online', () => {
  if (MAP_CONFIG.retryCount >= MAP_CONFIG.maxRetries) {
    // 网络恢复了,自动尝试最后一次
    this.mapInstance.setStyle(this.currentStyleUrl);
    // 隐藏错误遮罩
    this.hideMapErrorOverlay();
  }
});

更加完善的内部封装

js 复制代码
class MapManager {
  constructor() {
    this.mapInstance = null;
    this.retryCount = 0;
    this.maxRetries = 3;
    this.styleUrl = 'mapbox://styles/mapbox/streets-v11';
  }

  // 显示错误遮罩
  showErrorOverlay(message) {
    const container = document.getElementById('map'); // 地图容器ID
    let overlay = container.querySelector('.map-error-overlay');
    
    if (!overlay) {
      overlay = document.createElement('div');
      overlay.className = 'map-error-overlay';
      overlay.innerHTML = `
        <p class="error-msg">${message}</p>
        <button class="map-error-btn">立即重试</button>
      `;
      overlay.querySelector('button').onclick = () => window.location.reload();
      container.appendChild(overlay);
    } else {
      overlay.classList.remove('is-hidden');
      overlay.querySelector('.error-msg').innerText = message;
    }
  }

  // 隐藏错误遮罩
  hideErrorOverlay() {
    const overlay = document.querySelector('.map-error-overlay');
    if (overlay) overlay.classList.add('is-hidden');
  }

  initMap() {
    // ... 初始化 mapInstance 的代码 ...

    this.mapInstance.on('error', (e) => {
      // 过滤掉瓦片层级的 404 错误
      if (e.error?.status === 404) return;

      if (this.retryCount < this.maxRetries) {
        this.retryCount++;
        console.warn(`地图加载异常,第 ${this.retryCount} 次重试...`);
        
        // 尝试重置样式来恢复,而不是刷新页面
        setTimeout(() => {
          this.mapInstance.setStyle(this.styleUrl);
        }, this.retryCount * 2000);
      } else {
        // 达到上限,显示手动重试 UI
        this.showErrorOverlay('网络连接超时,请检查网络后重试');
      }
    });

    this.mapInstance.on('load', () => {
      this.retryCount = 0; // 加载成功,重置计数
      this.hideErrorOverlay();
    });
  }
}

6. 偶发网络问题手动重现

模拟各种 error 情况是至关重要的,如果是网络偶发问题,不需要去拔网线,通过修改关键key(将mapbox的style和token修改为错误的)和在浏览器开发者工具(F12)操作模拟网络问题就能覆盖 90% 的场景。

1. 模拟网络彻底断开 (Offline)

这是测试"重试逻辑"和"网络恢复自动加载"最直接的方法。

  • 操作步骤

    1. 打开 Chrome 开发者工具 -> Network (网络) 标签页。
    2. 找到 No Throttling (不限速) 下拉菜单。
    3. 选择 Offline (离线)
  • 结果 :此时地图的所有瓦片请求和样式请求都会立即失败,触发 error 事件。

2. 模拟特定资源加载失败 (404/403/500)

如果你想模拟 Mapbox 的 Token 失效(401)或者某个图层样式找不到了(404),可以使用Request Blocking

  • 操作步骤

    1. 在开发者工具中,按下 Ctrl + Shift + P (Mac: Cmd + Shift + P)。

    2. 输入 Blocking ,选择 Show Network Request Blocking

    3. 点击 + 号,添加过滤规则或者选中某个请求右键进行block,刷新后会针对性的block

      • 输入 api.mapbox.com:拦截所有地图数据请求。
      • 输入 *.png*.pbf:只拦截瓦片请求。
  • 结果 :被拦截的请求会显示为 (blocked:devtools),触发地图的错误回调。

3. 模拟弱网/高延迟 (Latency)

模拟地图加载极其缓慢,导致超时的场景。

  • 操作步骤

    1. Network 标签页的下拉菜单中选择 Slow 3G
    2. 或者点击 Add... 自定义一个配置(例如延迟 10000ms)。
  • 结果:可以观察你的 Loading 遮罩层是否能正常显示,以及在长时间无响应后是否会触发你写的超时重试逻辑。

4. 代码层面手动触发 (Manual Trigger)

如果你只是想调试 UI 样式(比如看那个错误遮罩层好不好看),不需要真的制造网络错误,直接在控制台调你的实例方法:

  • 操作步骤: 在 Console 中输入:
php 复制代码
// 假设你的 mapInstance 挂载在 window 下或者你能访问到
mapInstance.fire('error', { error: { message: '模拟错误', status: 401 } });
  • 结果 :这会手动触发你绑定的 .on('error', ...) 监听函数。

5. 模拟地理位置权限失败

如果你的地图涉及用户定位:

  • 操作步骤

    1. 点击地址栏左侧的 锁图标
    2. 位置信息 (Location) 设置为 屏蔽 (Block)
  • 结果 :测试 geolocate 插件报错时的 UI 反馈。

相关推荐
Cache技术分享2 小时前
355. Java IO API -去除路径中的冗余信息
前端·后端
牛马1112 小时前
Flutter CustomPaint
开发语言·前端·javascript
炽烈小老头2 小时前
函数式编程范式(三)
前端·typescript
ruoyusixian2 小时前
chrome二维码识别查插件
前端·chrome
fengfuyao9852 小时前
一个改进的MATLAB CVA(Change Vector Analysis)变化检测程序
前端·算法·matlab
yuhaiqiang3 小时前
为什么这道初中数学题击溃了所有 AI
前端·后端·面试
djk88883 小时前
支持手机屏幕的layui后台html模板
前端·html·layui
紫_龙3 小时前
最新版vue3+TypeScript开发入门到实战教程之watch详解
前端·javascript·typescript
默默学前端4 小时前
ES6模板语法与字符串处理详解
前端·ecmascript·es6