子应用挂了别急着白屏,先把静态副本顶上去——降级策略和用户提示的几个实践细节
子应用挂了,最差的结果不是 404,是白屏——用户盯着空白页面,不知道发生了什么,也不知道该怎么办。我见过不止一个团队,在微前端架构里把 99% 的精力花在正常链路上,结果子应用某次发版资源 500、CDN 回源失败、或者某个 chunk 加载超时,整个页面直接卡死,连个喘气的机会都没有。
解决这个问题的思路其实不复杂:前端容灾分两层,一层是静态副本快速顶替,另一层是用户提示给出明确出路。 关键是落地细节——副本什么时候触发、怎么切换、用户提示怎么写才不会让人想摔手机。
静态副本不是“降级页面”,是“最小可用版本”
先说清楚一个概念误区。很多人听到“静态副本”,第一反应是做一个纯静态的维护公告页,上面写“系统维护中,请稍后重试”。这当然比白屏强,但远不够。
静态副本应该承载子应用的核心功能闭环,哪怕是最简化的版本。 举个例子,我们一个电商项目里,商品详情页是独立子应用。它的静态副本不是一个公告,而是一个只读版本的商品信息页——从 CDN 拉一份预渲染好的 HTML,里面包含商品标题、价格、主图和库存状态,购买按钮置灰并标注“暂时无法购买”。用户至少能看到关键信息,而不是对着一个“系统繁忙”干瞪眼。
技术实现上,这份副本需要满足两个条件:第一,完全自包含,不依赖任何子应用运行时的 API 请求,所有数据在构建时打入 HTML;第二,体积极小,确保在弱网环境下也能快速加载。我们当时的商品详情页副本,压缩后 18KB,首屏渲染 200ms 以内。
触发切换的机制也有讲究。我见过最常见的做法是在主应用(基座)里做全局错误捕获,一旦检测到子应用资源加载失败就切副本。但这里面有个坑:不能只捕获脚本加载错误,还要覆盖 chunk 动态加载失败的场景。
Webpack 的动态 import 失败会抛出一个 ChunkLoadError,主应用需要在全局 unhandledrejection 或者对 __webpack_public_path__ 做劫持来捕获。我们当时的方案是在基座注册子应用时,对 fetch 和 XMLHttpRequest 做了一层代理,监控所有发往子应用资源域名的请求,连续失败 3 次(排除网络瞬时抖动)就触发降级。
// 基座中对子应用资源请求的监控简化示意
const resourceFailures = new Map();
function monitorResourceLoad(appName, resourceDomain) {
const originalFetch = window.fetch;
window.fetch = function(url, options) {
if (typeof url === 'string' && url.includes(resourceDomain)) {
return originalFetch(url, options).catch(err => {
trackFailure(appName);
throw err;
});
}
return originalFetch(url, options);
};
}
function trackFailure(appName) {
const count = (resourceFailures.get(appName) || 0) + 1;
resourceFailures.set(appName, count);
if (count >= 3) {
activateFallback(appName);
}
}
这里有一个容易被忽略的细节:副本切换的时机必须在用户感知到卡顿之前。 如果等用户看到白屏再切,已经晚了——用户大概率已经刷新页面或者关掉了。所以这个监控逻辑必须前置,在子应用开始加载的前 2 秒内就要做出判断。
用户提示的三个原则:具体、可操作、不甩锅
静态副本顶上去之后,用户提示的设计决定了这件事是“用户能理解”还是“用户想骂人”。
原则一:说清楚当前状态,不要用“加载失败”这种模糊表述。
“加载失败”是开发者的语言,不是用户的语言。用户不知道什么叫加载,也不知道什么东西失败了。应该直接告诉用户现在能用什么、不能用什么。比如:“商品价格和库存是最新数据,但暂时无法下单。我们正在修复,预计 5 分钟内恢复。”这句话里包含了三个信息:当前可用功能、不可用功能、预期恢复时间。
原则二:给用户一个能做的事情,哪怕只是一个刷新按钮。
人在面对故障时最焦虑的是“什么都做不了”。一个有效的缓解方式是给一个明确的操作入口。我们当时在副本顶部放了一个“点击重试”按钮,点击后会重新尝试加载子应用资源,三次重试失败后按钮文案变成“问题仍在,已通知技术团队”,同时上报一条告警。这个按钮的点击率在故障期间达到 40%,说明用户确实需要这种掌控感。
原则三:错误归因明确,但不要把技术细节甩给用户。
“CDN 回源超时”这种东西就不要出现在用户面前了。但也不能说“系统错误”——这会让用户以为是自己的问题。比较好的说法是“服务暂时不可用”,并且明确这是一个临时状态。如果副本能展示部分内容,加上“以下信息可能不是最新”的标注,防止用户误读数据。
副本的部署和维护成本怎么控制
有人会觉得,每个子应用都维护一份静态副本,成本太高。实际上,副本的维护成本取决于你的构建流程是否自动化。
我们的做法是,在 CI/CD 里增加一条副本构建流水线。子应用每次发布时,除了正常构建主版本,还会跑一个 build:fallback 脚本,生成一份精简版 HTML。这份 HTML 不经过灰度发布,直接推到 CDN 的固定路径上(比如 /_fallback/product-detail.html),确保任何时候这个路径上的文件都是可用的。
关键配置在于:副本的上传必须在主版本资源上传之前完成。因为如果主版本发布失败导致 CDN 上资源不完整,副本就是最后的防线。我们在 Jenkins 里把副本上传步骤放在了并行任务的最前面,并且设置了 30 秒超时——如果副本构建或上传超时,整个发布流程中止。
另一个值得注意的点是:副本的缓存策略要和主版本完全隔离。 主版本的 JS/CSS 通常会设置长缓存加 hash 文件名,但副本的 HTML 必须设置极短的缓存时间(我们设的是 5 分钟),并且 CDN 要开启“源站故障时使用过期缓存”的兜底策略。这样即使副本本身出了问题,也能快速回滚。
常见问题
副本展示的数据过时了怎么办?
静态副本的数据是在构建时打入的,确实会有滞后。解决思路是在副本中明确标注数据更新时间,比如“数据更新于 2024-01-15 14:30”。如果业务场景对数据实时性要求极高(比如股票行情),副本可以只展示基础信息框架,核心数据区域留白并提示“实时数据加载失败,请稍后重试”,避免用户基于错误数据做决策。
如果主应用本身挂了,这套方案还有用吗?
没用。这套方案的前提是主应用正常运行,只是某个子应用的资源加载失败。主应用自身的容灾需要另外处理,比如在 Nginx 层做静态化降级,或者利用 Service Worker 做离线缓存。这两层可以叠加:主应用层用 SW 兜底,子应用层用静态副本兜底,形成双层保护。
微前端框架(qiankun/Module Federation)对这套方案有影响吗?
qiankun 的沙箱机制会导致子应用的 window 被代理,静态副本如果包含内联脚本需要注意全局变量冲突。Module Federation 场景下,副本可以单独部署为一个独立入口,不经过联邦模块加载链路,直接在基座中用 iframe 或 web component 挂载,避免远程模块失败导致的连锁反应。