前言
首先听到服务端消息推送一般大多数可能都是使用到的websocket
,但是websocket
一般主要是用于聊天室,而SSE(Server-Sent Events)
也是可以基于服务器来与Web页面来进行数据和消息的推送的,但是与websocket
不同的是,服务器发送事件是单项的,数据只能从服务端发送到客户端。例如我们可以看一下常用的chatGtp
他的文本输出就是基于服务器的消息推送进行输出的。下面我们见看如何来建立一个通讯连接
EventSource
EventSource
是一个实例,专门用来建立与服务器的连接接收服务器的消息推送的,他会与服务器建立一个HTTP
的长连接,他会一致保持开启,直到调用close
关闭连接。ps:他是无法使用axios
的因为axios
内部其实使用的是XMLHttpRequest,他是无法支持接收服务端推送的消息的。
我们先实现一个简单的后台服务,然后前台来使用这个实例看一下效果是怎么样的。
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) })
这里我们简单实现了一个node服务,主要的就是Content-Type
这里我们把他设置为了text/event-stream
,也就是服务端的消息推送,然后我们每100毫秒推送一个字符过去,然后我们再前台看一下接收的效果是什么样子的
<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> <div> <button @click="OpenSSE">开启SSE</button> </div> <div>{{ article }}</div> </div> </template>
前台内我们就是声明了一个EventSource
实例,然后点击开启的时候监听他的message
,拿到服务器推送过来的消息,进行组装展示。就实现了如下的效果
这里我们可以看到他确实是开启了一个长连接,一直在接收服务器推送过来的数据,且请求类型也变成了eventsource
,我们在某些情况下需要关闭这个连接只需要调用实例方法内的close
方法即可对其进行关闭
const CloseSSE = () => { source.close() }
优化连接
EventSource
虽然可以让我们实现一些简单场景下的服务器消息推送的接收,但是他只支持get
请求,并且只能通过路径拼接的方式进行参数的携带。但是我们如果遇到像chat
一样会携带上下文进行数据传输的时候那么就不太适合了,因为处理起来比较麻烦,并且有着一定长度的限制。那么我们就可以使用fetch
来进行连接,下面我们看一下如何使用fetch
来建立连接获取服务器推送过来的消息。
const OpenSSE = async () => { const res = await fetch('http://localhost:3000/chat_typing') console.log(res) }
这里我们呢就实现了连接,那么如何去停呢,我们需要使用到 AbortController
来终止请求
const OpenSSE = async () => { abort = new AbortController() const res = await fetch('http://localhost:3000/chat_typing', { signal: abort.signal, }) console.log(res) } const CloseSSE = () => { abort.abort() }
abort
可以用来终止一个未完成的异步操作,可以用来终止所有的响应和流。正好我们现在使用fetch
返回过来的就是一个流,那么解决了请求和终止操作,接下来就需要解析返回的流了
解析流
我们首先打印res.body
可以看到内部存在一个方法,getReader,他呢就是用来获取流数据的,我们是用res.body.getReader
可以获取到一个一个Promise
,那么我们就可以进行执行拿到Promise
的值。
const res = await fetch('http://localhost:3000/chat_typing', { signal: abort.signal, }) const content = res.body.getReader() const decode = new TextDecoder() while (content) { const { done, value } = await content.read() if (done) break console.log(done, decode.decode(value)) }
这里呢我们逐步读取流,content.read()
是一个异步操作,从流中读取下一块数据。它返回一个包含两个属性的对象,done
和value
分别是读取到的值和是否读取完毕,当读取完毕我们直接结束循环,但是此刻的value
还是一个Uint8Array
的数据格式,我们需要使用TextDecoder
进行转换为字符串,到这里就可以拿到数据流推过来的东西了。此刻我们可以再修改一下后台,修改为post
请求来实验一下,并且可以直接返回要推送的值。
app.post('/chat_typing') res.write({data}) const res = await fetch('http://localhost:3000/chat_typing', { method: 'POST', signal: abort.signal, })
这样我们也就实现了可以使用fetch
并且写道参数来读取数据流了,并且使用到了POST请求
结尾
这里呢主要介绍的就是如何与后台进行连接实现消息推送,并且传递参数接收数据流,像这样的消息推送一般都是用来消息订阅,消息推送,提醒等功能上。