SSE vs webSocket
sse
- 单向通信 服务端-客户带
- 协议基于http/1.1,轻量
- 连接简单,自动重连
- 数据格式文本utf-8
websocket
- 全双向,客户端和服务可以相互通信
- 独立的ws/wss 协议
- 需要连接管理心跳,断线重连
- 长连接,占用资源更高
前端如何封装ai流式hook
- 发送请求 fetch stream 流
- 接收数据流,解析文本
- 实时更新ui,实现打字机效果
- 支持中断
- 状态管理
import { useState, useRef } from "react";
export function useAIStream() {
const [text, setText] = useState("");
const [loading, setLoading] = useState(false);
const controllerRef = useRef(null);
const start = async (payload) => {
setText("");
setLoading(true);
const controller = new AbortController();
controllerRef.current = controller;
try {
const res = await fetch("/api/ai", {
method: "POST",
body: JSON.stringify(payload),
headers: {
"Content-Type": "application/json",
},
signal: controller.signal,
});
const reader = res.body.getReader();
const decoder = new TextDecoder("utf-8");
let done = false;
while (!done) {
const { value, done: doneReading } = await reader.read();
done = doneReading;
const chunk = decoder.decode(value, { stream: true });
// 👉 解析 SSE 格式(data: xxx)
const lines = chunk.split("\n");
for (let line of lines) {
if (line.startsWith("data:")) {
const content = line.replace("data:", "").trim();
if (content === "[DONE]") {
setLoading(false);
return;
}
setText((prev) => prev + content);
}
}
}
} catch (err) {
if (err.name !== "AbortError") {
console.error(err);
}
} finally {
setLoading(false);
}
};
const stop = () => {
controllerRef.current?.abort();
setLoading(false);
};
return {
text,
loading,
start,
stop,
};
}
import { ref } from "vue";
export function useAIStream() {
const text = ref("");
const loading = ref(false);
let controller = null;
const start = async (payload) => {
text.value = "";
loading.value = true;
controller = new AbortController();
try {
const res = await fetch("/api/ai", {
method: "POST",
body: JSON.stringify(payload),
headers: {
"Content-Type": "application/json",
},
signal: controller.signal,
});
const reader = res.body.getReader();
const decoder = new TextDecoder("utf-8");
let done = false;
while (!done) {
const { value, done: doneReading } = await reader.read();
done = doneReading;
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split("\n");
for (let line of lines) {
if (line.startsWith("data:")) {
const content = line.replace("data:", "").trim();
if (content === "[DONE]") {
loading.value = false;
return;
}
text.value += content;
}
}
}
} catch (err) {
if (err.name !== "AbortError") {
console.error(err);
}
} finally {
loading.value = false;
}
};
const stop = () => {
controller?.abort();
loading.value = false;
};
return {
text,
loading,
start,
stop,
};
}
SSE VS fetch stream
EventSource 简单,不支持post,有专门的api fetch stream 灵活 实际生产中更推荐使用fetch + readableStream 来实现ai流。因为支持post ,鉴权和复杂请求控制,比原生sse更灵活
我会封装一个useAlStream hook。内部通过fetch + readableStream 实现流式读取,逐步解析sse数据,通过状态管理实际实时ui更新。同时结合abortController 实现中断,并通过缓存区渲染优化性能
其他问题
- 上下文携带,每次请求都带上下文,维护一个历史记录
- 文本结构解析,代码高亮是词法标记,本质都是字符串结构展示
- 不要每个token 都 render 使用buffer 然后批量更新ui
对话组件的核心是通过message数组维护上下文,在流式请求中不断更新最后一条消息,实现实时输出。渲染层通过markdown解析和代码高亮提升可读性,打字效果本质是流式数据驱动的增量更新,同时通过abortcontroller实现停止生成,通过状态回滚实现重新生成
虚拟列表实现
动态高度、流式更新和滚动稳定性”问题。通过高度缓存和前缀和实现高效定位,通过锚点机制保证视图稳定,并结合 ResizeObserver 动态修正高度,同时在流式输出过程中通过批量更新和自动滚动状态控制
主题色
主题色通常是基于设计令牌构建,通过将颜色,间距等抽象为语义化变量,并以json的形式管理不同主题配置,在运行时通过css variables 注入,实现无刷新动态切换。同时支持多品牌和多租户,通过主题和模式的组合实现黑暗模式,并结合缓存,过度动画优化体验。
diff
本质是旧虚拟dom和新虚拟dom找出最小更新 react diff 同级对比策略,深度优先遍历,key是diff的索引 fiber 中的diff 可中断和可恢复 ,本质DFS 只是递归链表结构
vue diff 与react diff 相比 添加列表优化,双端对比,最长递增子序列算法,进一步减少操作。
白屏问题
-
查看控制台报错(F12 → Console):90% 的白屏能直接看到报错信息(如语法错误、资源 404、未捕获异常);
-
检查网络请求(F12 → Network):看 JS/CSS/ 接口是否加载失败(404/500)、资源是否跨域;
-
排查渲染流程(F12 → Elements):看
<div id="app"></div>是否有内容,或是否被隐藏; -
兼容性格式化:在低版本浏览器(如 IE)测试,看是否因 ES6+ 语法未转译导致;
-
生产环境排查:如果开发环境正常、生产环境白屏,重点检查打包配置(如路径、压缩、CDN)