✅ 方式一:new EventSource()(浏览器原生)
一、适用场景
✅ 简单 SSE
✅ GET 请求
✅ 无需 Header / Token
✅ 日志 / 通知 / 监控
二、SSE 消息类型
ts
// types/sse.ts
export interface SSEMessage<T = any> {
event: string
data: T
}
三、EventSource Hook(推荐)
composables/useEventSource.ts
ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useEventSource<T = any>(
url: string,
options?: {
event?: string
}
) {
const data = ref<T[]>([])
const connected = ref(false)
const error = ref<Event | null>(null)
let es: EventSource | null = null
const connect = () => {
es = new EventSource(url)
es.onopen = () => {
connected.value = true
}
es.onerror = (err) => {
error.value = err
connected.value = false
es?.close()
}
if (options?.event) {
es.addEventListener(options.event, (e: MessageEvent<string>) => {
try {
data.value.push(JSON.parse(e.data))
} catch {
data.value.push(e.data as any)
}
})
} else {
es.onmessage = (e: MessageEvent<string>) => {
try {
data.value.push(JSON.parse(e.data))
} catch {
data.value.push(e.data as any)
}
}
}
}
onMounted(connect)
onUnmounted(() => es?.close())
return {
data,
connected,
error,
close: () => es?.close()
}
}
四、组件中使用
vue
<script setup lang="ts">
import { useEventSource } from '@/composables/useEventSource'
const { data, connected } = useEventSource<{ msg: string }>(
'/api/sse',
{ event: 'message' }
)
</script>
<template>
<p>状态:{{ connected ? '已连接' : '断开' }}</p>
<ul>
<li v-for="(item, i) in data" :key="i">
{{ item.msg }}
</li>
</ul>
</template>
✅ 方式二:Axios + Fetch Stream(企业级)
一、适用场景
✅ 大模型对话
✅ 需要 Token
✅ POST
✅ 生产环境
二、Axios SSE 请求封装
services/sseAxios.ts
ts
import axios from 'axios'
export interface SSEMessage<T = any> {
event: string
data: T
}
export async function requestSSE<T = any>(
url: string,
onMessage: (msg: SSEMessage<T>) => void,
options?: {
method?: 'GET' | 'POST'
data?: any
headers?: Record<string, string>
}
) {
const response = await axios({
url,
method: options?.method ?? 'GET',
data: options?.data,
headers: {
Accept: 'text/event-stream',
'Cache-Control': 'no-cache',
...options?.headers
},
responseType: 'stream',
adapter: 'fetch'
})
const reader = response.data.getReader()
const decoder = new TextDecoder('utf-8')
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const parts = buffer.split('\n\n')
buffer = parts.pop() || ''
for (const part of parts) {
const lines = part.split('\n')
let event = 'message'
let data = ''
for (const line of lines) {
if (line.startsWith('event:'))
event = line.replace('event:', '').trim()
if (line.startsWith('data:'))
data = line.replace('data:', '').trim()
}
if (data) {
try {
onMessage({ event, data: JSON.parse(data) })
} catch {
onMessage({ event, data: data as any })
}
}
}
}
}
三、Axios SSE Hook
composables/useSSEAxios.ts
ts
import { ref } from 'vue'
import { requestSSE } from '@/services/sseAxios'
export function useSSEAxios<T = any>(url: string) {
const data = ref<T[]>([])
const loading = ref(false)
const error = ref<Error | null>(null)
const start = (payload?: any) => {
loading.value = true
error.value = null
requestSSE<T>(
url,
(msg) => {
if (msg.event === 'message') {
data.value.push(msg.data)
}
},
{
method: 'POST',
data: payload
}
).catch(err => {
error.value = err
}).finally(() => {
loading.value = false
})
}
return { data, loading, error, start }
}
四、组件中使用(大模型对话)
vue
<script setup lang="ts">
import { useSSEAxios } from '@/composables/useSSEAxios'
const { data, loading, start } = useSSEAxios<{
role: string
content: string
}>('/api/chat')
const send = () => {
start({
messages: [{ role: 'user', content: '你好' }]
})
}
</script>
<template>
<button @click="send" :disabled="loading">
{{ loading ? '生成中...' : '发送' }}
</button>
<div v-for="(msg, i) in data" :key="i">
{{ msg.content }}
</div>
</template>
五、最终选择建议(记住这张表)
| 场景 | 选谁 |
|---|---|
| 简单通知 / 日志 | ✅ EventSource |
| 大模型 / AI | ✅ Axios |
| 需要 Token | ✅ Axios |
| 想省事 | ✅ EventSource |
| 公司项目 | ✅ Axios |