解密AI流式输出和网页呈现

解密AI流式输出和网站页面呈现

效果速看

1. SSE简介

  • SSE(Server-Sent Events)译为服务器推送事件,通过EventSource接口实现服务器推送通信。与webSocket不同的是,SSE基于http连接,为单向通信,在单向推送场景下(如各大AI网站)得到很好的应用(有关ws和SSE的对比本文不再深入)
  • 一个 EventSource 实例会对 HTTP 服务器开启一个持久化的连接,以 text/event-stream 格式发送事件,此连接会一直保持开启直到通过调用 EventSource.close() 关闭。客户端开启EventSource连接后通过监听特定的事件(如notice,update,meessage)来处理相关的逻辑

2. 原生EventSource不足

信息推送的简单代码示例如下 客户端,通过message事件来监听data类型的信息,然后对信息进行处理

javascript 复制代码
let count = ref("");
const sse = new EventSource("http://localhost:8080/sse");
sse.addEventListener("message", (e) => {
  console.log("---数据", e.data);
  count.value += e.data + " ";
  //页面渲染
});

服务器端,通过配置请求头Content-Typetext/event-stream来开启SSE,另外实时推送需要设置Cache-Controlno-cahce停用缓存,连接状态Connectionkeep-alive

javascript 复制代码
const express = require('express')
const cors = require('cors')
const app = express()

// 允许跨域
app.use(cors())

// SSE端点
app.get('/sse', (req, res) => {
    // 设置SSE需要的headers
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive'
    })
    // 定时发送消息
    let counter = 0
    const intervalId = setInterval(() => {
        counter++
        res.write(`event: message\n`)
        res.write(`data: 这是第${counter}次推送\n\n`)
        if (counter > 3) {
            clearInterval(intervalId);
        }
    }, 1000)

    // 客户端断开连接时清理
    req.on('close', () => {
        clearInterval(intervalId)
        console.log('客户端断开连接')
    })
})

// 启动服务器
const PORT = 8080
app.listen(PORT, () => {
    console.log(`Server running on http://localhost:${PORT}`)
})
    

效果如下

通过上面简单的示例代码细心的小伙伴可以发现:不对呀,别人的AI网站是post请求,可以携带参数信息,你的怎么是get请求。

是的,post请求可以携带大量的信息,比如问的问题,理论上get请求也可以通过query参数携带,一般服务器和浏览器会限制你的url字符,但是你想想你问一大堆问题,万一超过了url字符限制怎么办,另外从携带信息便捷性和安全性来说,在这种场景下使用post无疑是最优解,但是我们的EventSoure没有支持post 请求怎么办,于是我们解密用别的思路,也就是本文主角:用fetch发送post请求,使用ReadableStream和mardown实现流式传输和页面渲染

3.fetch创建可读流

3.1 使用ReadableStream

在fetch的response.body中,暴露了一个可读流的reader,调用response.body.getReader这个方法创建一个reader同时进行锁定,接着通过reader.read这个方法就可以读取到流。

javascript 复制代码
const response = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Accept: "text/event-stream",
    },
    body: JSON.stringify(data),
  }).catch((err) => {
    console.log("err报错了", err);
  });
  // 获取 ReadableStream 并创建读取器
  const reader = response.body.getReader();

}

3.2 使用TextDecoder 解码

EventSource发送来的是utf-8编码的信息,通过reader.read()得到的value为字节流,我们还需要使用TextDecoder 解码器器将字节流作为输入,并提供码位流作为输出,这个过程可以称为翻译

javascript 复制代码
  const decoder = new TextDecoder();
  // 持续读取流数据
  while (true) {
    const { done, value } = await reader.read();
    if (done) {
      reader.releaseLock();
      break;
    } // 流结束
    console.log("字节流", value);
    const chunk = decoder.decode(value);
    console.log("解码后数据为", chunk);
  }

然后我们将解析出来的数据做一些处理拼接就可以

4. markdown页面渲染

在各种AI网站上,我们通常会看到AI回答的内容有列表,有代码块,还有表格等等,难道这是自己手搓生成的嘛?no,通过观察他们的数据格式 这不是markdown语法吗,通过引入markdown库来对返回信息进行解析,再实时更新页面,这就有了页面打字机和丰富的排版效果。

5.实战模拟流式传输和页面呈现(含全部代码)

我们先来看一下最终效果(vue3+node.js 怎么样,看着效果图是不是有那味了

5.1 服务器端实现(node.js)

为了简化数据获取和处理流程和简单模拟流式返回markdown语法,我们写死每次返回的内容。简单来说就是每次请求我们都先读取同目录下的b.txt这个文件,这个文件里面存储了原始markdown语法的文本。然后通过随机slice文件块大小,和 setTimeout随机间隔发送读取出来的块给客户端来模拟流式传输效果。需要注意的是,SSE需要注意通信格式:每条信息后面需以\n\n结束(SSE协议规定的)。

r 复制代码
res.write(`data: 你好呀\n\n`)

当然,如果你不遵守,服务端和客户端维护一套自定义通信也可以,但是在查看请求时候这里面的EventStream浏览器将识别不到,内容将为空(下图就是将\n\n换成 \n所导致) 具体代码如下

javascript 复制代码
const express = require('express');
const fs = require('fs');
const app = express();
const PORT = 8080;
const cors = require('cors');

// 允许跨域
app.use(cors())
    // 发送文件内容的分块逻辑
function sendFileChunks(res, fileContent) {
    let index = 0;
    const sendChunk = () => {
        if (index >= fileContent.length) {
            //  res.write('event: end\ndata: \n\n'); // 发送结束事件
            res.end();
            return;
        }
        // 随机生成 3-5 的块大小
        const chunkSize = 3 + Math.floor(Math.random() * 3);
        const chunk = fileContent.slice(index, index + chunkSize);
        // SSE 格式要求
        res.write(`data: ${JSON.stringify({text:chunk})}\n`);
        index += chunkSize;
        // 随机间隔发送(模拟实时效果)
        setTimeout(sendChunk, 50 + Math.random() * 50);
    };

    sendChunk();
}

app.post('/sse', (req, res) => {
    try {
        //SSE需要
        res.writeHead(200, {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache',
            'Connection': 'keep-alive'
        });

        //读取同同级下的b.txt(里面存放着markdown格式的文档)
        const fileContent = fs.readFileSync('./b.txt', 'utf-8');
        //分块发送
        sendFileChunks(res, fileContent);
        // 处理连接关闭
        req.on('close', () => {
            console.log('Client disconnected');
            res.end();
        });

    } catch (error) {
        console.log('event: error\ndata: File not found\n\n');
        res.end();
    }
});

app.listen(PORT, () => {
    console.log(`Server running on http://localhost:${PORT}`);
});

5.2 前端实现

先要装markdown包将markdown语法转html

复制代码
npm install marked

通过mark.parse("具体内容")来实现文本转html。另外我们除了实现fetch创建可读流外还需要实现中断输出效果,这个我们只需要使用AbortController即可,通过abort()来中断请求,实现打断输出,具体代码如下:

javascript 复制代码
<template>
  <div>
    <button @click="fetchSSE">发送</button>
    <button @click="pause" style="color: red; margin-left: 30px">停止</button>
  </div>
  <div class="markdown-container" v-html="renderedContent"></div>
</template>

<script setup>
import { ref, onMounted } from "vue";
import { marked } from "marked";
const rawContent = ref("");
const renderedContent = ref("");
const controller = new AbortController();
// 配置Markdown解析

// 解析Markdown内容
const parseMarkdown = (raw) => {
  return marked.parse(raw);
};

async function fetchSSE() {
  //请求地址
  let url = "http://localhost:8080/sse";
  let data = { userId: 123, content: "介绍一下vue3", date: "XXXXX" };
  const response = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Accept: "text/event-stream",
    },
    body: JSON.stringify(data),
    signal: controller.signal,
  }).catch((err) => {
    console.log("err报错了", err);
  });
  // 获取 ReadableStream 并创建读取器
  const reader = response.body.getReader();
  //创建解码器
  const decoder = new TextDecoder();
  // 持续读取流数据
  while (true) {
    const { done, value } = await reader.read();
    if (done) {
      reader.releaseLock();
      break;
    } // 流结束
    const chunk = decoder.decode(value);
    const events = chunk.split("\n\n"); // SSE 事件以双换行分隔
    for (const event of events) {
      if (event.trim() === "") continue;
      parseSSEEvent(event);
    }
  }
}

function pause() {
  controller.abort();
}

// 解析单个 SSE 事件
function parseSSEEvent(event) {
  const lines = event;
  let dataObj = JSON.parse(lines.split(": ")[1]);
  //dtaObj里面为信息对象{text:XXX}
  rawContent.value += dataObj.text;
  //解析语法
  renderedContent.value = parseMarkdown(rawContent.value);
}
</script>
<style scoped>
.markdown-container {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
  text-align: left;
}
</style>

5.4 b.txt里面markdown内容

markdown 复制代码
# Vue 3 简介

Vue 3 是 Vue.js 框架的第三代版本,于 2020 年 9 月正式发布。它在多个方面进行了优化和改进,包括内部架构的重构、性能的提升以及新特性的引入。

## 主要特性

### 1. **Composition API**
Vue 3 引入了 Composition API,这是一种新的代码组织方式,允许开发者通过函数组合的方式来构建组件逻辑。它提供了更高的灵活性和代码复用性,特别适合处理复杂组件的逻辑。

### 2. **Fragment 和 Teleport**
- **Fragment**:允许组件返回多个根节点,解决了 Vue 2 中组件必须有一个根节点的限制。
- **Teleport**:可以将组件的内容渲染到父组件之外的 DOM 结构中,非常适合实现模态框、弹窗等场景。

### 3. **性能优化**
Vue 3 在性能上进行了大幅优化,包括更小的体积、更快的渲染速度和更低的内存占用。它还引入了 Proxy API,替代了 Vue 2 中的 `defineProperty`,从而提升了响应式系统的性能。

### 4. **更好的 TypeScript 支持**
Vue 3 对 TypeScript 提供了原生支持,使得开发者可以更方便地结合 TypeScript 使用 Vue,提升代码的可维护性和开发体验。

## 使用场景

Vue 3 适用于各种规模的前端项目,无论是小型的单页面应用还是大型的企业级系统。它的灵活性和高性能使其成为现代前端开发的热门选择。

## 总结

Vue 3 是一个功能强大且性能卓越的前端框架。它通过引入 Composition API、Fragment 和 Teleport 等新特性,极大地提升了开发效率和代码可维护性。同时,它对 TypeScript 的支持也使得开发体验更加流畅。如果你正在寻找一个现代化的前端框架,Vue 3 绝对值得尝试。

总结

本文还有诸多不清楚或者带待改进的地方,希望大家积极提出和共同进步

相关推荐
鱼樱前端1 分钟前
前端程序员集体破防!AI工具same.dev像素级抄袭你的代码,你还能高傲多久?
前端·javascript·后端
一个处女座的程序猿O(∩_∩)O27 分钟前
Vue 中 this 使用指南与注意事项
前端·javascript·vue.js
程序员大澈43 分钟前
7个 Vue 路由守卫的执行顺序
javascript·vue.js
程序员大澈1 小时前
4个 Vue mixin 的原理拆解
javascript·vue.js
程序员大澈1 小时前
3个 Vue $set 的应用场景
javascript·vue.js
程序员大澈1 小时前
3个 Vue nextTick 原理的关键点
javascript·vue.js
bin91531 小时前
DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加列宽调整功能,示例Table14_10空状态的固定表头表格
前端·javascript·vue.js·ecmascript·deepseek
天马37981 小时前
Vue 概念、历史、发展和Vue简介
前端·javascript·vue.js
moz与京1 小时前
【附JS、Python、C++题解】Leetcode面试150题(9)——三数之和
javascript·c++·leetcode
渔樵江渚上2 小时前
使用 Web Worker 解析 CSV 文件
前端·javascript·面试