前端硬核指南:如何让AI“打字机”效果在浏览器里丝滑跑起来?

🚀 EventSource、fetch流式读取......总有一款适合你

嘿,老朋友!上次咱们把FastAPI后端改造成了"水龙头",AI一个字一个字往外蹦。然后我偷偷懒,说"前端代码网上大把",结果后台好多小伙伴留言:"后端搞定了,前端死活接不上,救救我!"

**明明后端打印得好好的,浏览器就是没反应;**要么收到一堆乱码,要么连接一会儿就断。这感觉就像水龙头打开了,管子却是堵的。今天咱们就把这根"管子"彻底疏通,顺便把那些隐藏的"水垢"全清掉!


📡 前端接流三大主流方式

后端返回的是**text/event-stream** ,也就是 Server-Sent Events (SSE)。前端接收它主要有三种姿势:
🔹 方式1:浏览器原生EventSource ------ 最简单,但功能有限

🔹 方式2:fetch API + 流式读取 ------ 更灵活,可自定义请求头

🔹 方式3:第三方库 @microsoft/fetch-event-source ------ 全能选手,自动重连

🌰 方式1:EventSource ------ 杀鸡用牛刀?够用就行!

如果你不需要自定义请求头(比如不需要带Token),EventSource是最快的接入方式。几行代码搞定:

复制代码
const eventSource = new EventSource('http://localhost:8000/chat?prompt=你好');

eventSource.onmessage = (event) => {
  if (event.data === '[DONE]') {
    console.log('对话结束');
    eventSource.close();
    return;
  }
  // 把内容追加到页面
  document.getElementById('output').innerText += event.data;
};

eventSource.onerror = (err) => {
  console.error('连接出错', err);
  eventSource.close();
};

注意:EventSource只能发送GET请求,且不能添加自定义Headers(比如Authorization)。 如果你需要POST携带复杂参数,就得用下面两种。

🌊 方式2:fetch + 流式读取 ------ 真正的全栈式控制

fetch API 从 Chrome 95 开始完美支持流式响应。我们可以像读小说一样逐段读取数据:

复制代码
async function fetchStream() {
  const response = await fetch('http://localhost:8000/chat', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ prompt: '讲个故事' })
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    const chunk = decoder.decode(value);
    // 假设后端每块返回 "data: xxx\n\n" 格式,需要解析
    const lines = chunk.split('\n');
    for (const line of lines) {
      if (line.startsWith('data: ')) {
        const data = line.slice(6);
        if (data === '[DONE]') return;
        document.getElementById('output').innerText += data;
      }
    }
  }
}
fetchStream();

这种方式自由度超高,可以加Token,可以POST,甚至可以处理二进制流。但要自己手动解析SSE格式,容易在换行符上踩坑。

🛡️ 方式3:@microsoft/fetch-event-source ------ 躺平式接入

微软出品,专治各种SSE不服。它内置了断线重连、自动解析、错误恢复等功能,强烈推荐在生产环境使用

复制代码
import { fetchEventSource } from '@microsoft/fetch-event-source';

await fetchEventSource('http://localhost:8000/chat', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ prompt: '写首诗' }),
  onmessage(ev) {
    if (ev.data === '[DONE]') {
      console.log('完成');
      return;
    }
    document.getElementById('output').innerText += ev.data;
  },
  onerror(err) {
    console.error('大事不妙', err);
  },
  onclose() {
    console.log('连接关闭');
  }
});

它会自动处理SSE格式,只要关注onmessage里的ev.data即可。连[DONE]都得自己判断,它只管传数据。

⚠️ 前方高能:前端最容易翻车的五个坑

💥 坑1:CORS 跨域 ------ 万恶之源

后端就算配了CORS,如果前端用了withCredentials或自定义Header,依然可能触发预检请求(OPTIONS),后端必须处理。用fetch-event-source时,它默认不会发credentials,注意设置。

💥 坑2:数据格式必须严格遵循 SSE 规范

后端每块必须是 data: xxx\n\n,两个换行符不能少。否则EventSource和微软库都可能解析失败。fetch方式自己解析也要注意换行可能跨块。

💥 坑3:连接意外断开与重连机制

EventSource 默认会自动重连,但如果是HTTP 401/500等错误,它会一直重连直到地老天荒。最好监听onerror判断状态码,必要时关闭。微软库提供了onerror回调,可以控制是否终止重连。

💥 坑4:浏览器兼容性

EventSource 和 fetch 的流式读取在 IE 和部分旧手机浏览器上不可用。可以用微软库的 polyfill,或者提示用户升级浏览器。

💥 坑5:内存泄漏与连接未关闭

组件卸载或页面跳转时,一定要调用eventSource.close()或中止fetch的AbortController,否则连接会一直挂起,浪费资源。

🎯 进阶小贴士:让打字机体验更逼真

前端显示时,可以用定时器稍微打散一下文字出现节奏,模拟真人打字。但注意不要过度,否则用户会疯:

复制代码
let buffer = '';
onmessage(ev) {
  buffer += ev.data;
  // 每50ms渲染一个字符
  if (!typingTimer) {
    typingTimer = setInterval(() => {
      if (buffer.length > 0) {
        output.innerText += buffer[0];
        buffer = buffer.slice(1);
      } else {
        clearInterval(typingTimer);
        typingTimer = null;
      }
    }, 50);
  }
}

另外,记得在UI上给个"停止生成"的按钮,调用abort()close(),让用户随时打断。


好啦,前端三大流派和五个大坑都交代清楚了。你现在可以自信地拍着胸脯说:"流式输出,前端后端我全栈!"

如果还遇到怪问题,八成是换行符或者CORS,再不行就在评论区贴代码,咱们一起捉虫。觉得有用的话,点个⭐收藏,下次再遇到SSE,翻出这篇文章复习下再动手~

------ 依然是你那个爱踩坑的一名程序媛 👩‍💻