微前端架构下全局焦点连续性需由Shell层统一调度——因各子应用独立渲染导致原生Tab顺序断裂,必须通过Shell记录焦点锚点、接管Tab键行为、协调子应用聚焦时机并处理兼容性问题。

微前端架构下,全局焦点连续性不是“自动继承”的——每个子应用独立挂载、独立渲染、独立管理 DOM,原生 Tab 顺序天然断裂。必须在容器层(Shell)统一接管焦点调度,否则键盘用户从子应用 A 的按钮按 Tab,会直接跳到 Shell 的页脚链接,完全跳过子应用 B 的可聚焦区域。
子应用切换时焦点丢失的典型表现
用户在子应用 A 中聚焦于 saveBtn,点击跳转到子应用 B 后,焦点停留在 document.body 或首个 input(如果有的话),但更常见的是:焦点根本没移入 B,而是卡在 Shell 的导航栏某处,或直接失效(document.activeElement === body)。屏幕阅读器会播报“空白区域”,键盘操作中断。
- 子应用 B 渲染完成时未触发任何焦点逻辑,浏览器不会主动把焦点“移交”过去
- 各子应用使用不同框架(React/Vue/Angular),各自实现的
useEffect/onMounted/ngAfterViewInit时机不一致,requestAnimationFrame套用位置不同,导致聚焦时机错乱 - Shell 层未保留上一个活跃子应用的“最后聚焦元素引用”,切换后无法还原上下文
Shell 层需统一维护焦点锚点与调度入口
不能依赖子应用自行处理焦点——它们只知道自己内部结构,不知道自己在全局 Tab 链中的位置。Shell 必须作为唯一可信源,记录并调度。
- 在子应用 mount 前,Shell 记录当前
document.activeElement(若为子应用内元素,需打上所属子应用 ID 标签) - 子应用 mount 完成后,Shell 主动调用其暴露的
focusFirstFocusable()方法(而非让子应用自己focus()) - 该方法必须返回 Promise 或接受 callback,确保 DOM 渲染完成且所有动态内容(如异步表单字段)已就位
- 子应用卸载前,Shell 调用其
saveLastFocusedElement(),将当前聚焦元素序列化为 selector 字符串存入缓存
跨子应用的 Tab 键行为必须重写
浏览器原生 Tab 不知道子应用边界。Shell 需监听全局 keydown 事件,在 Tab 按下时拦截并计算下一个合法焦点目标。
立即学习“前端免费学习笔记(深入)”;
- 获取当前子应用所有可聚焦元素列表:
querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])') - 若焦点已在当前子应用末尾元素,不放行给浏览器,而是主动查找下一个子应用的首个可聚焦元素(需提前注册各子应用的“焦点入口点”)
- 对
Shift + Tab同理,但方向相反;需预存每个子应用的“焦点出口点”(通常是最后一个可聚焦元素) - 所有子应用必须禁用自身对
Tab的拦截逻辑,避免与 Shell 冲突
容易被忽略的降级与兼容细节
焦点链断裂最常发生在边缘场景,而非主流程。
-
inert属性在旧版 Safari 不支持,子应用卸载后若仅靠它隐藏内容,键盘仍可 tab 进去——必须配合aria-hidden="true"+tabindex="-1"双重封锁 - 子应用可能动态插入
iframe(如嵌入第三方编辑器),其内部焦点无法被 Shell 直接控制,需监听iframe.contentDocument的focusin并同步更新 Shell 的焦点锚点 - 移动端 VoiceOver 的焦点行为与桌面 Chrome 不同,
focus({ preventScroll: true })在 iOS 上可能被忽略,需 fallback 到scrollIntoView({ block: 'nearest' })
真正难的不是让某个按钮获得焦点,而是让整个微前端系统的焦点像一条绷紧的线——拉哪一端,另一端立刻响应,中间不断裂、不松弛、不打结。这要求 Shell 层有足够强的契约约束力,而不是靠子应用自觉。



















