添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

通过fetch,如果要获取流式数据可以如下处理:

async function getRes(content) {
  const res = await fetch(url, {...});
  const reader = res.body.getReader();
  // 读取数据流的第一块数据,done表示数据流是否完成,value表示当前的数
  const {done, value} = await reader.read();
  // 上面读取到的是数据的字节码,还需要处理字节码为文本
  const decoder = new TextDecoder();
  const text = decoder.decode(value);
  // 打印第一块的文本内容
  console.log(text, done);

以上代码指执行了一块数据,还要通过循环获取剩下流式内容:

async function getRes(content) {
  const res = await fetch(url, {...});
  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let content = '';
  let isThinking = true
  while(isThinking) {
    // 读取数据流的第一块数据,done表示数据流是否完成,value表示当前的数
    const {done, value} = await reader.read();
    if (done) {
        isThinking = false
        break;
    const text = decoder.decode(value);
    // 拼接文本内容
    content = content + text
    console.log(content, done);

以上可以实现基本的流式输出

react结合fetch整体可以这么实现

const [history, setHistory] = useState<{ speaker: string; text: string }[]>([
    { speaker: 'bot', text: '我是你的AI助手,有什么问题都可以问我' },
const [isThinking, setIsThinking] = useState(false);
const [inputText, setInputText] = useState('');
const [abortController, setAbortController] = useState<AbortController | null>(null);
const handleSubmit = async (question?: string) => {
    if(inputText==='' && question===undefined){
      return
    const controller = new AbortController();
    setAbortController(controller);
    const newHistory = [
      ...history,
      { speaker: 'user', text: question ? question : inputText },
      { speaker: 'bot', text: '' },
    setHistory(newHistory);
    setIsThinking(true);
    let toBody = {
      'model': 'gpt-3.5-turbo',
      temperature: 0.1,
      stream: true,
      messages: [
          role: 'user',
          content: question ? question : inputText ,
    setInputText('');
    fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: 'Bearer ' + localStorage.getItem('access_token'),
      body: JSON.stringify(toBody),
      signal: controller.signal,
      .then(async (response) => {
        if (response.body!.locked) {
          console.log('流已经被一个读取器锁定。');
          return;
        if (!response.body) throw Error('No response from server');
        const reader = response.body!.getReader();
        const textDecoder = new TextDecoder();
        let result = true;
        while (result) {
          const { done, value } = await reader.read();
          if (done) {
            console.log('Stream ended');
            result = false;
            setIsThinking(false);
            break;
          const chunkText = textDecoder.decode(value);
          let list = chunkText.match(/data: (.+)/g);
          console.log('--list--', list);
          if (list && list.length > 0) {
            list.forEach((element) => {
              let data = element.substring(6);
                data === '[DONE]' ||
                typeof JSON.parse(data) !== 'object' ||
                (typeof JSON.parse(data) === 'object' &&
                  (JSON.parse(data).choices.length === 0 ||
                    JSON.parse(data).choices[0].delta === undefined))
                if (data === '[DONE]') {
                  setIsThinking(false);
                return;
              let content = JSON.parse(data).choices[0].delta.content;
              if (content) {
                setHistory((history) => {
                  let newHistory;
                  // 如果聊天记录最后一条不是机器人,则拼接一条机器人回答对象
                  if (history[history.length - 1].speaker !== 'bot') {
                    newHistory = [...history, { speaker: 'bot', text: content }];
                  } else {
                    // 聊天记录最后一条是机器人,则直接在机器人回答的内容后面拼接新回答
                    history[history.length - 1].text = history[history.length - 1].text + content;
                    // 不能直接history赋值,要加上[... ]生成新对象,否则setState会认为引用地址没变,不执行页面刷新
                    newHistory = [...history];
                  return newHistory;
          console.log('Received chunk:', chunkText);
      .catch((error) => {
        setIsThinking(false);
        console.error('Error:', error);
        if (error instanceof DOMException && error.name === 'AbortError') {
          console.error('AbortError:', error);
    //这里可以更新state,比如更新history
  // 输入问题回车调用gpt
  const handleKeyPress = (event: any) => {
    if (event.key === 'Enter' && !event.shiftKey) {
      event.preventDefault();
      handleSubmit();
  // 停止调用
  const handleStop = () => {
    setIsThinking(false);
    if (abortController) {
      try {
        abortController.abort({
          type: 'USER_ABORT_ACTION',
          msg: '用户终止了操作',
      } catch (e) {
        console.log(e);
<div className={styles.chatBox}>
     {history.map((item, index) => (
            key={index}
            className={item.speaker === 'user' ? styles.chatRight : styles.chatLeft}
            {item.speaker === 'bot' && (
              <div style={{ marginBottom: '10px' }}>
                      src={require('@/assets/images/ai.png')}
                      className={styles.chatLogo}
             {item.text || !isThinking || index !== history.length - 1 ? (
                  <span style={{ display: 'block' }}>{item.text}</span>
             ) : (
               <Spin />
{showPrompts && (
     <div className={styles.promptsWrapper}>
         {prompts.map((prompt, index) => (
                  key={index}
                  className={styles.promptBox}
                  onClick={() => handleSubmit(prompt.question)}
                  <div>{prompt.title}</div>
                  <span>{prompt.subtitle}</span>
{isThinking ? (
   <div className={styles.stopWrapper} onClick={handleStop}>
        <img src={require('@/assets/images/stop.png')} />
        <span style={{ marginLeft: '10px' }}>
        </span>
   ) : (
   <div style={{width: '80%', bottom: '10px', position: 'fixed' }}>
         <div style={{ display: 'flex' }}>
              <TextArea
                  rows={1}
                  value={inputText}
                  onChange={handleInputChange}
                  onKeyPress={handleKeyPress}
                  src={require('@/assets/images/send.png')}
                  className={styles.chatSend}
                  onClick={() => handleSubmit()}

聊天框样式实现

.chatBox {
  flex: 1;
  height: 100%;
  .chatRight {
    display: flex;
    justify-content: right;
    margin: 10px 0;
    margin-left: 20%;
    > span {
      padding: 15px;
      line-height: 20px;
      background-color: #4eccc0;
      border-radius: 10px;
  .chatLeft {
    display: flex;
    flex-flow: column;
    align-items: flex-start;
    margin-right: 20%;
    > span {
      padding: 15px;
      background-color: #41454F;
      border-radius: 10px;
      line-height: 20px;
.chatWrapper {
  display: flex;
  flex-flow: column;
  height: 100%;
  overflow-y: scroll;
  scrollbar-width: thin;
  scrollbar-color: rgba(136, 136, 136, 0.3) transparent;
  color: #fff;
  margin-bottom: 30px;
.chatSend {
  align-self: center;
  height: 20px;
  padding-left: 5px;
  cursor: pointer;
.chatLogo {
  height: 20px;
  padding-right: 5px;
.promptsWrapper {
  display: flex;
  flex-flow: column;
  align-items: flex-start;
  .promptBox{
    background: #000000;
    padding: 10px;
    border-radius: 10px;
    margin-bottom: 5px;
      margin-bottom: 5px;
    span {
      color: #8a93a3;
      font-size: 13px;
.stopWrapper {
  text-align: center;
  img {
    width: 20px;
  bottom: 10px;
  position: fixed;
最近有个云栖大会的demo展示需求,要实现个类似的打字效果,所以我找了找相关的库。找到一个还不错的~叫iTyped.js。但是最终的效果和我想要的不太一样,会有回删效果,看了下源码,所以就自己写了一个~
再夸奖下 iTyped.js 只有3K,非常小而美,完全靠JS实现的效果!
边播放语音,边出现文字的打字效果,gif 效果如下~
				
React 是一个基于组件化的 JavaScript 库,它通过虚拟 DOM 的方式实现高效的页面渲染。React实现原理可以分为以下几个步骤: 1. 初始化阶段:React 通过调用 ReactDOM.render() 方法将组件渲染到页面上,并创建虚拟 DOM 树。 2. 更新阶段:当组件的状态发生变化时,React 会重新渲染组件,并生成新的虚拟 DOM 树。 3. 对比阶段:React 会对比新旧虚拟 DOM 树的差异,找出需要更新的部分。 4. 渲染阶段:React 会将需要更新的部分重新渲染到页面上。 React实现原理主要依赖于虚拟 DOM 和组件化思想,通过将页面抽象成组件的方式,实现了高效的页面渲染和更新。