在前端的重连机制方案中,最暴力的就是直接使用 window.location.reload() ,但是这可能会导致用户陷入无限刷新的死循环(比如用户断网了,刷新多少次也加载不出来),同时也非常消耗性能和用户流量。
我们可以从重试机制、状态反馈、错误拦截三个维度来进行优化。下面以 mapbox 的底图加载失败为案例进行测试。
1. 指数退避的自动重试
不要立即刷新,而是尝试在代码层面重新加载地图资源。如果重试多次失败,再引导用户手动刷新。
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 请求失败等)。有些错误不值得刷新页面:
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. 离线/断网检测
利用浏览器的原生事件,在网络恢复的第一时间尝试重载。
window.addEventListener('online', () => {
if (this.mapLoadFailed) {
this.mapInstance.setStyle(this.currentStyleUrl);
}
});
5. 综合方案
将逻辑封装成一个状态机:
-
状态 A (Loading): 页面中心显示转圈。
-
状态 B (Success): 监听到
load或idle事件,隐藏转圈。 -
状态 C (Error): 监听到
error。- 记录重试次数。
- 若次数未满:微调样式 URL 触发重新请求(或调用
map.remove()后重新new Mapboxgl.Map())。 - 若次数已满:弹出一个浮窗提示:“地图资源加载超时 [重试按钮]”。
// 状态配置
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();
}
});
更加完善的内部封装
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)
这是测试“重试逻辑”和“网络恢复自动加载”最直接的方法。
-
操作步骤:
- 打开 Chrome 开发者工具 -> Network (网络) 标签页。
- 找到 No Throttling (不限速) 下拉菜单。
- 选择 Offline (离线) 。
-
结果:此时地图的所有瓦片请求和样式请求都会立即失败,触发
error事件。
2. 模拟特定资源加载失败 (404/403/500)
如果你想模拟 Mapbox 的 Token 失效(401)或者某个图层样式找不到了(404),可以使用Request Blocking。
-
操作步骤:
-
在开发者工具中,按下
Ctrl + Shift + P(Mac:Cmd + Shift + P)。 -
输入 Blocking,选择 Show Network Request Blocking。
-
点击
+号,添加过滤规则或者选中某个请求右键进行block,刷新后会针对性的block- 输入
api.mapbox.com:拦截所有地图数据请求。 - 输入
*.png或*.pbf:只拦截瓦片请求。
- 输入
-
-
结果:被拦截的请求会显示为
(blocked:devtools),触发地图的错误回调。
3. 模拟弱网/高延迟 (Latency)
模拟地图加载极其缓慢,导致超时的场景。
-
操作步骤:
- 在 Network 标签页的下拉菜单中选择 Slow 3G。
- 或者点击 Add... 自定义一个配置(例如延迟 10000ms)。
-
结果:可以观察你的 Loading 遮罩层是否能正常显示,以及在长时间无响应后是否会触发你写的超时重试逻辑。
4. 代码层面手动触发 (Manual Trigger)
如果你只是想调试 UI 样式(比如看那个错误遮罩层好不好看),不需要真的制造网络错误,直接在控制台调你的实例方法:
- 操作步骤: 在 Console 中输入:
// 假设你的 mapInstance 挂载在 window 下或者你能访问到
mapInstance.fire('error', { error: { message: '模拟错误', status: 401 } });
- 结果:这会手动触发你绑定的
.on('error', ...)监听函数。
5. 模拟地理位置权限失败
如果你的地图涉及用户定位:
-
操作步骤:
- 点击地址栏左侧的 锁图标。
- 将 位置信息 (Location) 设置为 屏蔽 (Block) 。
-
结果:测试
geolocate插件报错时的 UI 反馈。