跳转至

title: [ISSUE-0002] Popup 启动后 loading 骨架屏永久卡住 description: "一直卡在这(截图:popup 顶部骨架屏一直转),如何登录呢?" tags: [issue, auth] created: 2026-05-26 updated: 2026-05-26 type: issue status: fixed severity: critical


[ISSUE-0002] Popup 启动后 loading 骨架屏永久卡住

相关 wikipopup-auth-state-machine.md

用户感知的现象

"一直卡在这(截图:popup 顶部骨架屏一直转),如何登录呢?"

打开扩展 popup → 顶部第一块永远显示骨架屏 + 转圈 + 没有任何登录入口 → 用户无法操作。

根因分析

src/sections/popup/index.tsx v0.10.7 引入的 3 态状态机有死循环:

type AuthState = 'loading' | 'logged-in' | 'logged-out';
useEffect(() => {
  storage.getItem('local:uid').then((storedUid) => {
    if (!storedUid) setAuthState('logged-out');
    // else 留在 loading,等 user 数据返回   ← 死等!
  });
}, []);
useEffect(() => { if (uid) setAuthState('logged-in'); }, [uid]);
  • storage 有旧 uid(之前登过留的)
  • useAuthContext 拉 user 失败(token 过期 / API 挂 / 网络问题)
  • uid 永远是 undefined
  • → authState 永远是 loading
  • → popup 一直显示骨架屏,无登录按钮

修复方案

1. 加第 4 态 stale + 2 秒超时兜底

type AuthState = 'loading' | 'logged-in' | 'logged-out' | 'stale';

useEffect(() => {
  storage.getItem('local:uid').then((storedUid) => {
    if (!storedUid) { setAuthState('logged-out'); return; }
    // 给 user 2 秒;超时切 stale
    timeoutId = setTimeout(() => {
      setAuthState((s) => (s === 'loading' ? 'stale' : s));
    }, 2000);
  });
}, []);

2. loading 也提供「手动登录」入口

骨架屏右侧加小按钮,2 秒等不了的用户能立即操作。

3. 统一 handleLoginClick

先 try-cookie-login(force=true),成功 reload;失败开窗 + 关 popup。

改动文件

文件 改了什么
src/sections/popup/index.tsx 4 态状态机 + 2s 超时 + handleLoginClick

验证方式

  1. 已登录 → 打开 popup → loading 1s 内变 logged-in
  2. 从未登录 → 打开 popup → loading 1s 内变 logged-out,显示登录按钮
  3. stale(手动清空 user API 响应)→ 打开 popup → 2s 后变 stale,显示橙色「会话失效·重新登录」
  4. 网络挂 → 打开 popup → 2s 后切 stale,可点击手动登录

如何避免再犯

  • 任何"等异步结果"的 loading 状态必须有超时(2 秒是经验值)
  • loading 状态不要隐藏所有操作入口 — 给用户主动逃出 loading 的机会
  • stale ≠ logged-out:UI 区分有意义,重新登录路径相同但心理预期不同

相关问题

  • ISSUE-0001 — 同源问题(stale token),background 那层 v0.10.17 才修