点击开始动手实验


问题背景:一登录就白屏,用户直接“失联”

把 ChatGPT 能力嵌进自家产品后,最常收到的工单不是“回答不准”,而是“页面白屏”。
体验路径很直接:用户点击“使用 AI 功能”→ 跳到登录 → 授权成功 → 回调回来只剩一片白。
刷新、清缓存、换浏览器都没用,只能关掉标签页。
对业务的影响是实打实的:30% 的新增用户在首次登录环节流失,客服压力飙升,老板天天问“到底哪里崩了”。
白屏不是前端“没渲染”,而是请求链路上某个环节直接断掉,浏览器拿不到数据,React 根节点挂载失败,于是“什么都不敢画”。。
下面把我在两个 SaaS 项目里踩出来的完整诊断流程摊开,照着做,基本能在 15 分钟内定位是谁“掐断”了流量。

诊断流程:先画地图再拆炸弹

先给出一张“白屏排查地图”,看一眼就能知道走到哪一步该查什么。

graph TD
    A[登录回调] --> B{URL 带 code?}
    B -->|否| C[授权失败,重走 OAuth]
    B -->|是| D[请求 /api/token]
    D --> E{HTTP 200?}
    E -->|否| F[看 CORS/状态码]
    E -->|是| G[JWT 解析]
    G --> H{exp 有效?}
    H -->|否| I[触发刷新或重新登录]
    H -->|是| J[进业务页]
    J --> K{React 渲染?}
    K -->|否| L[Console 报错/边界]
  1. 打开 DevTools → Network,过滤 api/token,看有没有红色
    红色且是 CORS error:大概率是后端没把 Access-Control-Allow-Origin 写对;
    红色 401/403:令牌无效或接口没配好路由。

  2. 如果 Network 里直接没有这条请求,九成是前端跳转逻辑把 code 吃掉了。
    常见写法:window.location.replace() 把参数冲掉,或 React Router 的 <Navigate> 把 search 清空。
    解决:在跳转前先把 window.location.search 存到 sessionStorage,再手动拼回。

  3. 拿到 200 以后,立刻在 Application → Local Storage 里看 access_token
    把值粘到 jwt.io,看 exp 时间。
    若当前时间戳 > exp,后端却还给 200,说明颁发逻辑没校验刷新令牌,属于后端 Bug,直接甩锅。

  4. 令牌 OK 仍白屏 → 切到 Console。
    React 项目 90% 是 React.createElement: type is invalid,原因是动态 import 失败
    Vue 项目则可能是 Failed to resolve module,都是代码分割把 chunk 打到需要登录态的路由,结果第一次渲染就 404。
    修复:把 import() 提前到公共路由,或给 webpack publicPath 写死绝对路径。

解决方案:三板斧砍下去,白屏基本消失

1. Axios 拦截器:一次写好,终身省心

// src/utils/request.ts
import axios, { AxiosError } from 'axios';
import { refreshToken } from '@/services/auth';

const instance = axios.create({
  baseURL: import.meta.env.VITE_API_BASE,
  timeout: 8000,
  withCredentials: true, // 关键:带 cookie 走 refresh
});

// 响应拦截
instance.interceptors.response.use(
  (res) => res,
  async (error: AxiosError) => {
    const originalRequest = error.config!;
    // 仅对 401 做自动刷新
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      try {
        const { accessToken } = await refreshToken();
        // 把新 token 写回 header
        originalRequest.headers.Authorization = `Bearer ${accessToken}`;
        return instance(originalRequest);
      } catch {
        // 刷新也失败 → 踢回登录
        window.location.href = '/login';
        return Promise.reject(error);
      }
    }
    return Promise.reject(error);
  }
);

export default instance;

要点注释

  • _retry 防循环;
  • withCredentials 让 refresh 接口能带 httpOnly cookie,避免 XSS 偷令牌;
  • 刷新失败立刻硬跳转,防止用户卡在白屏。

2. CORS & CSP 配置:抄作业即可

Nginx 示例

add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
add_header Access-Control-Allow-Headers "Authorization,Content-Type";

CSP 头(防内联脚本把页面打白)

add_header Content-Security-Policy
  "default-src 'self'; script-src 'self' https://static.cdn.com; connect-src 'self' https://api.openai.com";

3. 存储策略:sessionStorage vs localStorage

场景 推荐 原因
仅单次会话有效 sessionStorage 关标签即清,降低 XSS 持久化风险
刷新页仍需保持 localStorage 避免重复走 OAuth,但需加 1h 过期兜底
移动端 WKWebView localStorage iOS 重启进程后 sessionStorage 会丢

实战组合:
access_token → 内存变量 + sessionStorage(内存优先,刷新页再读);
refresh_token → httpOnly cookie,前端 0 触碰;
user_profile → localStorage,过期时间写死 1h,定时任务清掉。

生产环境考量:让白屏永远先发“预警”

  1. 令牌刷新机制
    后端下发 双令牌:短 5min 的 access + 长 7d 的 refresh
    前端在第 4 分钟主动调 /refresh,成功则无缝续命;失败则弹 Toast“登录已过期”,2s 后跳转。
    这样用户几乎感知不到,也杜绝“用到一半突然白屏”。

  2. 错误边界(React 示例)

// src/components/ErrorBoundary.tsx
import React, { Component, ReactNode } from 'react';

interface Props {
  fallback: ReactNode;
  children: ReactNodeDimmer
}

export class ErrorBoundary extends Component<Props> {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(err: Error) {
    // 上报 Sentry/Falcon
    window.$log.error('Render_Fatal', err);
  }

  render() {
    return this.state.hasError ? this.props.fallback : this.props.children;
  }
}

根组件包一层,白屏变成**“哎呀,网络开小了一下”**的刷新按钮,留存率能抬 8%。

  1. 监控 & 报警
    • 前端:在 componentDidCatchwindow.onerror 里用 navigator.sendBeaconurl+stack 打到日志服务;
    • 阈值:1 分钟内同一条路由 >5 次报错即触发飞书机器人 Webhook;
    • 后端:对 /api/token 的 5xx 做 P1 报警,防止“后端崩了”背锅给前端。

避坑指南:把坑先填上,就不用再跳

  1. 反模式:把 access_token 存 Redux 持久化插件,页面刷新后仓库清空,直接白屏。
    正确:只把必须持久化的放 Storage,Redux 做运行时内存。

  2. 反模式:登录页和主站不同域,回调带 # 片段,结果 location.hash 被 Nginx 重写掉。
    正确:用 history 模式,或把 hash 换成 ? 查询串,Nginx 配 try_files $uri /index.html

  3. 反模式:把 refreshToken 写成同步轮询,导致页面卡顿。
    正确:只在 401 拦截里惰性刷新,正常请求别碰 refresh 接口。

  4. 监控盲区:只埋点 PV,不埋点首次有意义渲染(FMP)
    白屏用户连 DOM 都没渲染,PV 照样发,结果数据一片祥和。
    解决:把 FMP 上报条件写成 document.querySelector('#root') 存在且高度 > 100

延伸思考

  1. 如果你的产品同时支持 Web & 小程序,双令牌体系在小程序 WebView 里如何无缝复用?
  2. 当刷新接口也 429 限流时,前端该用指数退避还是静默队列
  3. 白屏监控只能告诉你“发生了”,如何结合用户录屏性能指标精确定位是哪一行代码导致渲染中断?

把上面流程跑通,我最大的感受是:白屏不是玄学,只是请求链路上某个环节“没回包”
只要按地图逐步缩小范围,基本都能在用户发飙前修完。
如果你想亲手搭一套“能说话”的 AI 应用,顺便把实时语音链路也跑通,可以试试这个动手实验——
从0打造个人豆包实时通话AI
我跟着做了一遍,语音端到端延迟 600ms 左右,前端部分同样用 React + Axios,把今天这些拦截器直接搬进去就能用,小白也能顺利体验。祝调试愉快,永不白屏!

点击开始动手实验


Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐