ai开发相关

7 阅读4分钟

SSE vs webSocket

sse

  1. 单向通信 服务端-客户带
  2. 协议基于http/1.1,轻量
  3. 连接简单,自动重连
  4. 数据格式文本utf-8

websocket

  1. 全双向,客户端和服务可以相互通信
  2. 独立的ws/wss 协议
  3. 需要连接管理心跳,断线重连
  4. 长连接,占用资源更高

前端如何封装ai流式hook

  1. 发送请求 fetch stream 流
  2. 接收数据流,解析文本
  3. 实时更新ui,实现打字机效果
  4. 支持中断
  5. 状态管理
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 实现中断,并通过缓存区渲染优化性能

其他问题

  1. 上下文携带,每次请求都带上下文,维护一个历史记录
  2. 文本结构解析,代码高亮是词法标记,本质都是字符串结构展示
  3. 不要每个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)