利用 Server-Sent Events 实现 ChatGPT 风格的实时文本输出

166 阅读3分钟

输出效果

chatGP逐字输出效果.gif

基础知识

Server-Sent Events (SSE)

SSE 是一种允许服务器主动向客户端发送数据的技术。与 WebSocket 相比,SSE 是单向的——只有服务器可以发送数据。

Express 框架

Express 是一个灵活、轻量的 Node.js web 应用框架,广泛用于快速开发 web 应用。

EventSource API

EventSource API 是浏览器中的一个接口,用于接收来自服务器的事件流。

设置项目

安装 Node.js 和 Express

确保你的系统已安装 Node.js。通过运行 npm init 创建一个新项目,并使用 npm install express 安装 Express。客户端文件我们使用 ejs 模板,所以也请安装 npm install ejs

项目结构

chat-dialog/
│
└── views/
    └── home.ejs
│
├── main.js
├── package.json

在本文,我们先使用 Express 创建一个 Node.js 服务端,定义了一个 /events SSE路由来发送事件流,这里这里模拟发送一段文本,每隔 50ms 发送一个字符。

创建服务端(main.js)

// main.js
const express = require("express")
const path = require('path')
const app = express()

// 配置视图引擎和视图的路径
app.set("view engine", "ejs")
app.set("views", path.join(__dirname, "views"))

// 客户端模拟输出的内容
const str = `在 JavaScript 中,Math.max.apply(null, [1, 2, 5, 3, 4]) 这行代码用于找出数组中的最大值。这里使用了 Function.prototype.apply() 方法来调用 Math.max 函数。让我们分解这行代码来理解它是如何工作的:Math.max:这是 JavaScript 中的一个函数,用于返回一组数字中的最大值。通常,它的用法是 Math.max(num1, num2, ..., numN),其中 num1, num2, ..., numN 是需要比较的数字。.apply() 方法:这是 JavaScript 函数对象的一个方法,它允许你调用一个函数,并为它指定 this 值和参数数组。.apply() 的第一个参数是用来设置 this 的值的,第二个参数是一个数组,数组中的元素将被作为单独的参数传递给函数。在 Math.max.apply(null, [1, 2, 5, 3, 4]) 这个表达式中:Math.max 作为函数被调用。null 作为第一个参数传递给 .apply(),在这个特定的情况下,this 的值不重要,因为 Math.max 是一个静态方法,不依赖于 this 的值。因此,传递 null 是安全的。[1, 2, 5, 3, 4] 作为第二个参数,是一个数组,其元素将被作为单独的参数传递给 Math.max。结果,Math.max.apply(null, [1, 2, 5, 3, 4]) 相当于调用 Math.max(1, 2, 5, 3, 4),返回这些数字中的最大值,即 5。`

// SSE 路由
app.get("/events", (req, res) => {
  res.setHeader("Content-Type", "text/event-stream")
  res.setHeader("Cache-Control", "no-cache")
  res.setHeader("Connection", "keep-alive")
  res.flushHeaders()

  let count = 0
  const intervalId = setInterval(() => {
    if (count < str.length) {
      res.write(`data: ${str[count++]}\n\n`)
    } else {
      clearInterval(intervalId)
      res.end()
    }
  }, 50)

  // 处理关闭连接
  req.on("close", () => {
    clearInterval(intervalId)
  })
})

// 首页路由
app.get("/", (req, res) => {
  res.render("home")
})

// 监听端口
app.listen(8685, () => {
  console.log("服务已启动,监听端口8685")
})

创建客户端(views/home.ejs)

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>chatGPT 逐字输出</title>
</head>

<body>
  <h1>ChatGPT 模拟输出</h1>
  <div id="output"></div>
  <script>
    const outputElement = document.getElementById('output')
    const eventSource = new EventSource('/events')

    eventSource.onmessage = function(event) {
      outputElement.textContent += event.data
      
      // 此处硬编码,模拟输出完毕后关闭SSE,不然会一直无限请求输出
      if (outputElement.textContent.length === 687) {
        eventSource.close()
      }
    }

    eventSource.onerror = function() {
      console.error('发生错误,SSE 连接关闭')
      eventSource.close()
    }
  </script>
</body>

</html>

项目依赖(package.json)

{
  "name": "chat-dialog",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "瓶子",
  "license": "ISC",
  "dependencies": {
    "ejs": "^3.1.9",
    "express": "^4.18.2"
  }
}

运行和测试

启动 Express 服务器,并在浏览器中访问 http://localhost:8685 查看实时输出效果。

结语

你已经了解了如何使用 SSE 和 Express 来实现实时数据传输和逐字显示效果。这只是 SSE 和实时通信的冰山一角,你可以进一步探索和扩展这些技术,创造更多有趣的功能。