不止 WebSocket 可以实现长连接,它也可以:Server-Sent Events(SSE)
在前端开发中,服务端消息推送通常会使用 WebSocket,特别是在聊天室应用场景中。虽然 WebSocket 提供了双向通信的能力,但并不是唯一的长连接解决方案。Server-Sent Events
(SSE)也可以用于基于服务端的消息推送,但它是单向的,数据只能从服务端发送到客户端。
例如,像 ChatGPT 这样的应用场景下,文本输出就是基于服务器的消息推送来进行数据的实时输出。本文将介绍如何使用 EventSource
实例来实现与服务端的通信,并实现消息推送。
1. 什么是 EventSource
EventSource
是一个用于接收服务端推送消息的实例,它基于 HTTP 的长连接,能够始终保持开启状态,直到调用 close
方法关闭连接。需要注意的是,EventSource
不支持通过 axios
进行实现,因为 axios
使用的是 XMLHttpRequest
,无法处理服务端推送消息。
后端服务示例
首先,我们需要实现一个简单的后端服务,使用 SSE 推送数据给前端。下面是一个基于 Node.js 的示例:
const article = `警告:当不使用 HTTP/2 时.....。`
app.get('/chat_typing', (req, res) => {
// 开启 Server-sent events
res.setHeader('Content-Type', 'text/event-stream')
let index = 0
let timerId = 0
// 模拟每隔 0.1s 向前端推送一次
timerId = setInterval(() => {
// 获取文字
const data = article[index]
// 下标累加
index++
// 响应结果
if (data) {
// data:表示数据内容,\n\n 表示结尾。
res.write(`data: ${data}\n\n`)
} else {
res.end()
clearInterval(timerId)
}
}, 100)
})
在这个示例中,服务器每 100 毫秒向前端推送一个字符,Content-Type
设置为 text/event-stream
,表示这是 SSE 消息推送。
前端接收消息
在前端,我们可以通过 EventSource
实例来建立长连接,并接收服务端的消息推送。以下是基于 Vue.js 的示例:
<script setup>
import { ref } from 'vue'
const article = ref('')
let source
const OpenSSE = () => {
source = new EventSource('http://localhost:3000/chat_typing')
// 接收信息
source.addEventListener('message', (e) => {
// 实时输出字符串
article.value += e.data
})
}
</script>
<template>
<div>
<button @click="OpenSSE">开启SSE</button>
<div>{{ article }}</div>
</div>
</template>
当点击按钮时,前端将会开启与服务器的连接,并通过 EventSource
实例监听服务器发送的消息。在接收到的消息中,每个字符会逐步显示在页面上。
关闭连接
如果我们需要关闭这个长连接,可以调用 EventSource
实例的 close
方法:
const CloseSSE = () => {
source.close()
}
2. 使用 fetch
优化连接
虽然 EventSource
适用于简单的消息推送场景,但它只支持 GET
请求,并且参数只能通过 URL 拼接传递。如果需要传递更多上下文或使用 POST
请求,那么 fetch
可以更好地处理这种情况。
基于 fetch
的长连接
以下是使用 fetch
来代替 EventSource
实现连接和数据推送的代码示例:
const OpenSSE = async () => {
const res = await fetch('http://localhost:3000/chat_typing')
console.log(res)
}
为了终止请求,我们可以使用 AbortController
来控制请求的终止操作:
const abort = new AbortController()
const OpenSSE = async () => {
const res = await fetch('http://localhost:3000/chat_typing', {
signal: abort.signal,
})
console.log(res)
}
const CloseSSE = () => {
abort.abort()
}
AbortController
可以用来终止未完成的异步操作,在长连接中非常有用。
解析返回的流
通过 fetch
获取服务器的流数据,接着我们可以使用 res.body.getReader()
读取数据流。getReader()
返回一个 Promise
,我们可以逐块读取流数据。
const OpenSSE = async () => {
const res = await fetch('http://localhost:3000/chat_typing', {
signal: abort.signal,
})
const content = res.body.getReader()
const decoder = new TextDecoder()
while (content) {
const { done, value } = await content.read()
if (done) break
console.log(decoder.decode(value))
}
}
content.read()
是异步操作,返回的 value
是 Uint8Array
格式的数据,需要通过 TextDecoder
转换为字符串。
支持 POST
请求的示例
在某些场景下,我们可能需要通过 POST
请求来传递更多的上下文信息。以下是修改为 POST
请求的示例:
后端:
app.post('/chat_typing', (req, res) => {
// 处理 POST 请求
res.write({ data: 'some data' })
})
前端:
const OpenSSE = async () => {
const res = await fetch('http://localhost:3000/chat_typing', {
method: 'POST',
signal: abort.signal,
})
const content = res.body.getReader()
const decoder = new TextDecoder()
while (content) {
const { done, value } = await content.read()
if (done) break
console.log(decoder.decode(value))
}
}
3. 总结
本文展示了如何使用 EventSource
和 fetch
来实现服务器消息推送的长连接机制。
- EventSource:适用于简单的消息推送场景,易于使用,但只支持
GET
请求,且参数只能通过 URL 传递。 - fetch:提供了更多的灵活性,支持
POST
请求、传递参数,并可以处理数据流。
SSE 和 fetch
都可以用于实现长连接,常用于消息推送、提醒等功能。