为什么sse里面,eventSource.value.onmessage和eventSource.value.onerror都走了,onmessage能正常收到消息,为什么onerror连接失败?

为什么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就不应该报错。

阅读 329
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题