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内存监控阈值告警与自动清理


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

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

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

相关推荐
恋猫de小郭1 小时前
Flutter Windows 开始支持 Impeller ,还修复了多窗口 bug
android·前端·flutter
2601_959982211 小时前
从单体架构到微服务,再到云原生:现代软件架构的发展趋势
微服务·云原生·架构
柳杉1 小时前
没写一行代码!我用 ChatGPT 5.6 (Sol) 做了一个智慧充电站 3D 大屏
前端·chatgpt·数据可视化
AI-好学者1 小时前
Redis高可用与分布式架构
redis·分布式·缓存·架构
Henrii_历小海2 小时前
WhatsApp Web 不是手机镜像:多设备协议、本地数据流与出海获客流水线
前端·智能手机
ayqy贾杰2 小时前
Claude Fable 5 提示词泄漏,抓紧学习下
前端·后端·面试
To_OC10 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
vipbic10 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy10 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端