为什么sse里面,eventSource.value.onmessage和eventSource.value.onerror都走了,onmessage能正常收到消息,为什么onerror连接失败?
sse组件源码
<template>
<div class="max-w-4xl mx-auto p-6">
<!-- <div class="bg-white rounded-xl shadow-lg p-6 mb-8">
<div class="flex flex-col md:flex-row gap-4 mb-6">
<div class="flex-1">
<label for="appName" class="block text-sm font-medium text-gray-700 mb-1">应用名称</label>
<input
v-model="appName"
type="text"
id="appName"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all"
>
</div>
<div class="flex-1">
<label for="appVersions" class="block text-sm font-medium text-gray-700 mb-1">应用版本 (逗号分隔)</label>
<input
v-model="appVersionsInput"
type="text"
id="appVersions"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all"
>
</div>
</div>
<div class="flex justify-center">
<button
@click="connectToSSE"
:disabled="isConnecting || isConnected"
class="px-6 py-3 bg-primary hover:bg-primary/90 text-white font-medium rounded-lg shadow-md hover:shadow-lg transition-all flex items-center gap-2"
>
<i :class="isConnecting ? 'fa fa-circle-o-notch fa-spin' : 'fa fa-refresh'"></i>
<span>{{ isConnecting ? '检查中...' : '检查更新' }}</span>
</button>
</div>
</div>
<div v-if="connectionStatus === 'connected'" class="mb-6">
<div class="p-4 rounded-lg flex items-center gap-3 bg-success/10 border border-success/30">
<i class="fa fa-check-circle text-success"></i>
<div>
<h3 class="font-medium text-success">已连接到更新服务器</h3>
<p class="text-sm text-gray-600">连接时间: {{ connectionTime }}</p>
</div>
</div>
</div>
<div v-if="updateSuggestions.length > 0" class="space-y-4 mb-8">
<h2 class="text-xl font-semibold text-gray-800 mb-4">更新建议</h2>
<div class="space-y-4">
<div
v-for="suggestion in updateSuggestions"
:key="suggestion.requestedVersion"
:class="suggestion.needsUpdate ? 'bg-warning/5 border border-warning/20' : 'bg-success/5 border border-success/20'"
class="p-5 rounded-xl transition-all hover:shadow-md"
>
<div class="flex items-start gap-4">
<div class="w-10 h-10 rounded-full flex items-center justify-center" :class="suggestion.needsUpdate ? 'bg-warning/10' : 'bg-success/10'">
<i :class="suggestion.needsUpdate ? 'fa fa-exclamation-triangle text-warning' : 'fa fa-check-circle text-success'"></i>
</div>
<div class="flex-1">
<div class="flex justify-between items-start mb-2">
<h3 class="font-semibold text-gray-800">版本 {{ suggestion.requestedVersion }}</h3>
<span class="px-2 py-1 rounded-full text-xs font-medium" :class="suggestion.needsUpdate ? 'bg-warning/20 text-warning' : 'bg-success/20 text-success'">
{{ suggestion.needsUpdate ? '需要更新' : '已是最新版本' }}
</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-3">
<div>
<p class="text-sm text-gray-500">当前版本</p>
<p class="font-medium">{{ suggestion.requestedVersion }}</p>
</div>
<div>
<p class="text-sm text-gray-500">最新版本</p>
<p class="font-medium">{{ suggestion.latestVersion }}</p>
</div>
</div>
<div v-if="suggestion.needsUpdate" class="bg-white p-4 rounded-lg border border-gray-200 mb-3">
<h4 class="font-medium text-gray-700 mb-2">更新说明</h4>
<pre class="text-sm text-gray-600 whitespace-pre-wrap">{{ suggestion.releaseNotes }}</pre>
</div>
<div v-if="suggestion.needsUpdate" class="flex justify-end">
<a :href="suggestion.downloadUrl" target="_blank" class="px-4 py-2 bg-primary hover:bg-primary/90 text-white rounded-lg shadow hover:shadow-md transition-all flex items-center gap-1">
<i class="fa fa-download"></i>
<span>下载更新</span>
</a>
</div>
<p v-else class="text-gray-600">你的应用已是最新版本,无需更新。</p>
</div>
</div>
</div>
</div>
</div>
<div v-if="errorMessage" class="mb-6">
<div class="p-4 bg-danger/10 border border-danger/30 rounded-lg flex items-center gap-3">
<i class="fa fa-exclamation-circle text-danger"></i>
<div>
<h3 class="font-medium text-danger">错误</h3>
<p class="text-sm text-gray-700">{{ errorMessage }}</p>
</div>
</div>
</div> -->
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { ElMessage } from 'element-plus'
// 状态管理
const appName = ref('pc-jiniubao');
// const appVersionsInput = ref('1,1,1,4,6');
const appVersionsInput = ref('1.1.1.4.6');
const connectionStatus = ref('idle'); // idle, connecting, connected, error
const connectionTime = ref('');
const updateSuggestions = ref([]);
const errorMessage = ref('');
const eventSource = ref(null);
const isConnecting = ref(false);
const isConnected = ref(false);
const lock = ref(false);
const isProd = import.meta.env.PROD
// 新增props定义
const props = defineProps({
isPC: {
type: Boolean,
default: true
}
});
// 连接到SSE服务器
const connectToSSE = () => {
// 重置状态
resetState();
// 验证输入
if (!appName.value.trim() || !appVersionsInput.value.trim()) {
errorMessage.value = '请输入应用名称和版本号';
return;
}
// 处理版本号
const appVersions = appVersionsInput.value.split('.').map(v => v.trim());
if (appVersions.length === 0) {
errorMessage.value = '请输入有效的版本号';
return;
}
// 构建URL
const queryParams = new URLSearchParams();
appVersions.forEach(version => queryParams.append('appVersion[]', version));
queryParams.append('appName', appName.value);
const url = isProd ? `https://api.jiniubao.com/api2/check/update?${queryParams.toString()}` : `http://localhost:3003/api2/check/update?${queryParams.toString()}`;
// 更新连接状态
isConnecting.value = true;
connectionStatus.value = 'connecting';
try {
// 创建EventSource实例
eventSource.value = new EventSource(url);
// 处理消息事件
eventSource.value.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log('接收到更新数据:', data);
if (data.success) {
console.log('before 有更新可用', lock.value);
if (!lock.value && data.updateSuggestions?.shouldUpdate) {
console.log('有更新可用');
lock.value = true;
// ElMesage.success('有更新可用,请刷新页面');
ElMessage({
message: `有更新可用,请${props.isPC ? ' CTRL + F5 ' : ''}刷新页面,当前版本${appVersionsInput.value},最新版本${data.lastEdition}`,
type: 'info',
showClose: true,
duration: 0,
});
}
updateSuggestions.value = data.updateSuggestions;
connectionStatus.value = 'connected';
isConnected.value = true;
} else {
errorMessage.value = data.error || '获取更新信息失败';
connectionStatus.value = 'error';
}
isConnecting.value = false;
} catch (parseError) {
console.error('解析SSE数据失败:', parseError);
errorMessage.value = '解析更新信息失败';
connectionStatus.value = 'error';
isConnecting.value = false;
}
};
// 处理连接打开事件
eventSource.value.onopen = () => {
connectionTime.value = new Date().toLocaleTimeString();
connectionStatus.value = 'connected';
isConnected.value = true;
isConnecting.value = false;
};
// 处理错误事件
eventSource.value.onerror = (error) => {
console.error('SSE连接错误:', error);
errorMessage.value = '连接到更新服务器失败,请检查服务器是否运行';
connectionStatus.value = 'error';
isConnecting.value = false;
isConnected.value = false;
// 检查是否是连接失败
if (eventSource.value && eventSource.value.readyState === EventSource.CLOSED) {
closeEventSource();
}
};
} catch (error) {
console.error('创建SSE连接失败:', error);
errorMessage.value = '创建更新连接失败';
connectionStatus.value = 'error';
isConnecting.value = false;
}
};
// 关闭SSE连接
const closeEventSource = () => {
if (eventSource.value) {
eventSource.value.close();
eventSource.value = null;
}
};
// 重置状态
const resetState = () => {
closeEventSource();
connectionStatus.value = 'idle';
connectionTime.value = '';
updateSuggestions.value = [];
errorMessage.value = '';
isConnecting.value = false;
isConnected.value = false;
};
onMounted(() => {
connectToSSE();
})
// 组件卸载时关闭连接
onUnmounted(() => {
closeEventSource();
});
</script>
<style scoped>
.danger {
color: #ef4444;
}
</style>
koa实现源码
router.get('/api2/check/update', async (ctx) => {
// 设置SSE响应头
ctx.set({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*', // 根据实际需求设置跨域
});
ctx.status = 200;
try {
// 获取参数
const appName = ctx.query.appName;
// let appVersions = ctx.query['appVersion[]'].split('.') || [];
// let appVersions = ctx.query['appVersion[]'] || '';
let appVersions = ctx.query['appVersion[]'].join('.') || '';
console.log({
appName,
appVersions,
query: ctx.query
})
// 确保appVersions是数组
// if (!Array.isArray(appVersions)) {
// appVersions = [appVersions];
// }
console.log({
appName,
appVersions
})
// 验证参数
if (!appName || appVersions.length === 0) {
throw new Error('Missing required parameters: appName and appVersion');
}
// 检查应用是否存在于更新数据库中
const appsResult = await mysqlQuery(`select * from apps where app_name = "${appName}";`)
const updateInfo = appsResult.data[0] || {}
// const updateInfo = updateDatabase[appName];
if (!appsResult.data.length) {
throw new Error(`Update information not found for app: ${appName}`);
}
// 比较版本号并生成更新建议
// const shouldUpdate = utils.compareVersions(appVersions.join('.'), updateInfo.new_edition);
console.log({
old: appVersions,
new: updateInfo.new_edition,
})
const shouldUpdate = utils.compareVersions(appVersions, updateInfo.new_edition);
console.log({
shouldUpdate,
old: appVersions,
new: updateInfo.new_edition,
})
// const updateSuggestions = appVersions.map(version => {
// const shouldUpdate = utils.compareVersions(version, updateInfo.new_edition) < 0;
// return {
// requestedVersion: version,
// latestVersion: updateInfo.new_edition,
// needsUpdate: shouldUpdate,
// ...(shouldUpdate ? {
// // releaseNotes: updateInfo.releaseNotes,
// // downloadUrl: updateInfo.downloadUrl,
// // releaseDate: updateInfo.releaseDate
// } : {})
// };
// });
// 发送更新信息
const data = JSON.stringify({
success: true,
appName,
lastEdition: updateInfo.new_edition,
updateSuggestions: {
shouldUpdate
}
});
// 发送SSE消息
ctx.res.write(`data: ${data}\n\n`);
// 保持连接打开一段时间,模拟实时更新检查
const keepAliveTimeout = 30 * 1000; // 30秒
const keepAliveInterval = setInterval(() => {
ctx.res.write(': keep-alive\n\n'); // 发送心跳
}, 15 * 1000); // 每15秒发送一次心跳
// 超时后关闭连接
setTimeout(() => {
clearInterval(keepAliveInterval);
ctx.res.end();
}, keepAliveTimeout);
// 监听连接关闭事件
ctx.req.on('close', () => {
clearInterval(keepAliveInterval);
ctx.res.end();
});
} catch (error) {
// 发送错误信息
const errorData = JSON.stringify({
success: false,
error: error.message
});
ctx.res.write(`data: ${errorData}\n\n`);
ctx.res.end();
}
});
不知道如何尝试,期望既然eventSource.value.onmessage能收到消息,那么eventSource.value.onerror就不应该报错。