AI对话前端从入门到崩溃:一个长对话引发的五层优化战争【四】

刷新不丢、断线能续------持久化与恢复机制

Token管理好了,但用户不小心刷新页面,一切归零。在地铁里信号差,AI说了半句话就断了。本文用IndexedDB + SSE断线续传构建坚韧的体验。

01. 场景一:刷新页面,对话消失

用户聊了半小时,误触F5。由于内存数据重置,界面一片空白。用户愤怒地关闭了网页。

解决 :每次AI回复完成,将完整对话序列化存入IndexedDB(不要用localStorage,它只能存5MB且同步阻塞)。

IndexedDB封装(使用Dexie.js):

typescript 复制代码
import Dexie from 'dexie';

const db = new Dexie('AIChatDB');
db.version(1).stores({
  sessions: 'sessionId, lastUpdated',
  messages: 'id, sessionId, timestamp',
});

// 存储消息
async function saveMessages(sessionId: string, messages: Message[]) {
  await db.messages.bulkPut(
    messages.map(m => ({ ...m, sessionId, timestamp: Date.now() }))
  );
  await db.sessions.put({ sessionId, lastUpdated: Date.now() });
}

// 页面加载时恢复
async function loadSession(sessionId: string): Promise<Message[]> {
  const cached = await db.messages.where('sessionId').equals(sessionId).toArray();
  return cached.sort((a, b) => a.timestamp - b.timestamp);
}

// 启动时:快速先用IndexedDB展示,再去服务端拉取最新差异(乐观更新)
useEffect(() => {
  loadSession(sessionId).then(localMsgs => {
    setMessages(localMsgs);
    // 后台静默同步服务端
    fetchFromServer(sessionId).then(serverMsgs => {
      if (serverMsgs.length > localMsgs.length) {
        setMessages(mergeMessages(localMsgs, serverMsgs));
        saveMessages(sessionId, serverMsgs);
      }
    });
  });
}, []);

02. 场景二:SSE断线续传(Last-Event-ID)

大模型流式输出使用SSE(Server-Sent Events)。网络不稳定时连接断开,用户不想重头生成。

SSE原生支持断线续传 :浏览器在重连时会自动发送Last-Event-ID头。

服务端实现:

  • 每个数据块携带id(自增或时间戳+随机数)。
  • 接收到Last-Event-ID后,从该ID之后继续推送剩余内容。

前端实现(EventSource封装):

typescript 复制代码
class ResilientEventSource {
  private url: string;
  private lastId: string = '';
  private retryCount = 0;
  private maxRetries = 5;
  private eventSource: EventSource | null = null;

  connect(onMessage: (chunk: string) => void) {
    // 关键:如果存在lastId,拼接到URL参数中
    const urlWithId = this.lastId ? `${this.url}?lastEventId=${this.lastId}` : this.url;
    this.eventSource = new EventSource(urlWithId);
    
    this.eventSource.onmessage = (event) => {
      // 解析并保存最新的ID
      const data = JSON.parse(event.data);
      if (data.id) this.lastId = data.id;
      
      // 处理内容块(可能是Delta)
      if (data.content) {
        onMessage(data.content);
      }
      
      // 重置重试计数
      this.retryCount = 0;
    };

    this.eventSource.onerror = (e) => {
      this.eventSource?.close();
      // 指数退避重连
      const delay = Math.min(1000 * Math.pow(2, this.retryCount), 30000);
      setTimeout(() => {
        this.retryCount++;
        if (this.retryCount <= this.maxRetries) {
          console.log(`重连中... 第${this.retryCount}次`);
          this.connect(onMessage);
        }
      }, delay);
    };
  }

  disconnect() {
    this.eventSource?.close();
    this.eventSource = null;
  }
}

03. 流式渲染的状态管理进阶(归一化)

SSE高频推送(每几十毫秒一次)。如果直接setMessages([...messages, newChunk]),整个虚拟滚动列表都会重渲染,即使其他消息没变。

使用归一化结构(Normalized State):

typescript 复制代码
// 状态结构
interface ChatState {
  entities: Record<string, Message>; // id -> Message
  order: string[];                  // 仅存ID顺序
}

// 流式更新时
const updateStreamingMessage = (id: string, deltaContent: string) => {
  setState(prev => ({
    ...prev,
    entities: {
      ...prev.entities,
      [id]: {
        ...prev.entities[id],
        content: prev.entities[id].content + deltaContent,
      }
    }
    // order 完全不动!不触发列表顺序重新计算!
  }));
};

结合Valtio(Proxy-based状态管理) : Valtio能自动追踪依赖,只有消费了entities[id].content的组件才会重渲染。

tsx 复制代码
import { proxy, useSnapshot } from 'valtio';

const store = proxy<ChatState>({ entities: {}, order: [] });

// 在MessageItem中
function MessageItem({ id }) {
  const snap = useSnapshot(store);
  const msg = snap.entities[id]; // 只有这个msg变化时才重渲染
  return <div>{msg.content}</div>;
}

04. 跨设备同步

如果用户PC和手机同时登录,本地IndexedDB不互通。必须依赖服务端全量存储,前端通过sessionId拉取。实现WebSocket或轮询增量同步。


📖 下一篇预告(第05篇)

P7的终极答案------服务端会话 + 分层存储

场景预告:前面四层全做了,但遇到上万轮对话的重度用户还是扛不住------问题出在架构上。

核心看点:上下文管理从客户端剥离下沉至Redis / 服务端会话管理完整交互流程 / 前端热/温/冷三层存储架构 / Sentry内存监控阈值告警与自动清理


👋 最后一篇硬核架构篇,关注我别错过!

  • 点击「关注」获取更新推送
  • 已关注的朋友,欢迎转发给需要的同事或朋友

「点赞 👍 + 收藏 ⭐ + 评论 💬」三连走起,让好内容流动起来!

相关推荐
碳基生物猛吃膳食纤维1 分钟前
结合DEEPSEEK和CHATGPT弄了个小脚本,批量检查商品主图
前端
天远Date Lab5 分钟前
零信任架构实战:基于天远企业年报信息核验构建自动化商业保理财务与社保穿透评估网关
人工智能·架构·自动化
绝世唐门三哥5 分钟前
CSS 换行三剑客:overflow-wrap、word-break 与 white-space 完全指南
前端·css
EnCi Zheng10 分钟前
后端怎么把AI接口稳定安全低成本接进业务:超时重试、密钥隔离与成本熔断清单
人工智能·后端·安全·架构·api
救救孩子把10 分钟前
LangGraph 工程化架构:类型、状态、流式通信与可观测性
架构·agent·trace·opentelemetry·langgraph·graphrag
kyriewen21 分钟前
别再用防抖做懒加载了:120 张图只加载出 4 张
前端·javascript·程序员
计算机魔术师26 分钟前
Hugging Face CEO 转评前 DOGE 研究人员用 AI 智能体渗透 Anthropic 系统事件
前端
IT_陈寒34 分钟前
刚解决一个 Java 并发问题,这坑踩得我怀疑人生
前端·人工智能·后端
前端一课35 分钟前
一个晚上,四个“小问题”:我重写了 12 章 ArkUI 笔记
前端
这个DBA有点耶37 分钟前
从 MySQL 到时序数据库:我帮朋友做迁移的完整记录
数据库·架构·dba