connectEventSource请求sse流在uni-app中频繁更新会各种报错导致整个软件不可用比如点击没效果点击其他tabBar也无效

connectEventSource请求sse流在uni-app中频繁更新会各种报错导致整个软件不可用比如点击没效果点击其他tabBar也无效

产品分类:

uniapp/App

PC开发环境操作系统:

Windows

PC开发环境操作系统版本号:

win10

HBuilderX类型:

正式

HBuilderX版本号:

4.66

手机系统:

Android

手机系统版本号:

Android 14

手机厂商:

小米

页面类型:

nvue

vue版本:

vue3

打包方式:

离线

项目创建方式:

HBuilderX

示例代码:

<template>
  <text class="message-content-text" style="color: #fff;font-size: 28rpx;line-height: 1.5;">
    {{contents}}
  </text>
  <text style="color: #fff;" :style="{ opacity }" v-if="isSend">|</text>
</template>

<script setup>
const props = defineProps({
  inputMessage: {
    type: String,
    default: ''
  }
})
import { HTTP_REQUEST_URL } from '@/config';
let eventSource : UniEventSource | null = null
let once = true
const contents = ref('')
const getContents = computed(() => contents.value)
const isSend = ref(true)
const opacity = ref(1)
let timer : number | null = null
const setOpacity = () => {
  if (timer !== null) clearInterval(timer as number)
  timer = setInterval(() => {
    opacity.value = opacity.value > 0 ? 0 : 1
  }, 500)
}
const emit = defineEmits(['stop', 'updateScrollTop'])
setOpacity()
onUnmounted(() => {
  eventSource?.close()
  if (timer !== null) clearInterval(timer as number)
})
const handleAPICall = () => {
  let displayMessage = '';
  eventSource = uni.connectEventSource({ url: HTTP_REQUEST_URL + '/api/chat/stream' });
  eventSource?.onMessage(({ data }) => {
    try {
      const jsonData = JSON.parseObject(data as string);
      const output = jsonData?.get("output") as UTSJSONObject | null;
      const rawText = output?.get("text");
      const newText = typeof rawText === 'string' ? rawText : '';
      displayMessage += newText
      nextTick(() => {
        if ((displayMessage != '' && displayMessage != null) || (newText != '' && newText != null) && (displayMessage != newText)) {
          contents.value = displayMessage
          if (isSend.value) {
            isSend.value = false;
            if (timer !== null) clearInterval(timer as number)
          }
          nextTick(() => {
            emit('updateScrollTop')
          })
        }
      })
      // throttledSetContent(displayMessage)
      // 检查是否是最后一次数据
      if (output?.get("finish_reason") == 'stop') {
        emit('updateScrollTop')
        emit('stop')
        eventSource?.close(); // 关闭 SSE
        console.log('displayMessage:', displayMessage);
      }
    } catch (e) {
      console.log(e, '<<>><>><><<<');
      uni.showToast({
        title: '连接错误,已中断连接',
        icon: 'none'
      });
      emit('stop')
      emit('updateScrollTop')
      eventSource?.close();
    }
  });
  eventSource?.onError((error : any) => {
    emit('stop')
    eventSource?.close();
    isSend.value = false;
  });
};
watch(() => props.inputMessage, (t : string) => {
  if (t == '' && once) {
    setTimeout(() => {
      handleAPICall()
      once = false
    }, 100)
  }
})
</script>

<style>
</style>

操作步骤:

<template>
  <text class="message-content-text" style="color: #fff;font-size: 28rpx;line-height: 1.5;">
    {{contents}}
  </text>
  <text style="color: #fff;" :style="{ opacity }" v-if="isSend">|</text>
</template>

<script setup>
const props = defineProps({
  inputMessage: {
    type: String,
    default: ''
  }
})
import { HTTP_REQUEST_URL } from '@/config';
let eventSource : UniEventSource | null = null
let once = true
const contents = ref('')
const getContents = computed(() => contents.value)
const isSend = ref(true)
const opacity = ref(1)
let timer : number | null = null
const setOpacity = () => {
  if (timer !== null) clearInterval(timer as number)
  timer = setInterval(() => {
    opacity.value = opacity.value > 0 ? 0 : 1
  }, 500)
}
const emit = defineEmits(['stop', 'updateScrollTop'])
setOpacity()
onUnmounted(() => {
  eventSource?.close()
  if (timer !== null) clearInterval(timer as number)
})
const handleAPICall = () => {
  let displayMessage = '';
  eventSource = uni.connectEventSource({ url: HTTP_REQUEST_URL + '/api/chat/stream' });
  eventSource?.onMessage(({ data }) => {
    try {
      const jsonData = JSON.parseObject(data as string);
      const output = jsonData?.get("output") as UTSJSONObject | null;
      const rawText = output?.get("text");
      const newText = typeof rawText === 'string' ? rawText : '';
      displayMessage += newText
      nextTick(() => {
        if ((displayMessage != '' && displayMessage != null) || (newText != '' && newText != null) && (displayMessage != newText)) {
          contents.value = displayMessage
          if (isSend.value) {
            isSend.value = false;
            if (timer !== null) clearInterval(timer as number)
          }
          nextTick(() => {
            emit('updateScrollTop')
          })
        }
      })
      // throttledSetContent(displayMessage)
      // 检查是否是最后一次数据
      if (output?.get("finish_reason") == 'stop') {
        emit('updateScrollTop')
        emit('stop')
        eventSource?.close(); // 关闭 SSE
        console.log('displayMessage:', displayMessage);
      }
    } catch (e) {
      console.log(e, '<<>><>><><<<');
      uni.showToast({
        title: '连接错误,已中断连接',
        icon: 'none'
      });
      emit('stop')
      emit('updateScrollTop')
      eventSource?.close();
    }
  });
  eventSource?.onError((error : any) => {
    emit('stop')
    eventSource?.close();
    isSend.value = false;
  });
};
watch(() => props.inputMessage, (t : string) => {
  if (t == '' && once) {
    setTimeout(() => {
      handleAPICall()
      once = false
    }, 100)
  }
})
</script>

<style>
</style>

预期结果:

实际结果:

报错

bug描述:

就是做一个ai问答功能,connectEventSource请求sse流,这个接口有可能几毫米返回一条数据,然后我这边拿到数据就更新,就会各种报错,然后整个软件不可用,比如说点击没效果,点击其他tabBar也无效,我这边能保证数据什么的都不是空得但就是报空: java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1029) at java.util.ArrayList$Itr.next(ArrayList.java:982) at io.dcloud.uniapp.vue.IndexKt.initDepMarkers(index.kt:8723) at io.dcloud.uniapp.vue.ReactiveEffect$run$1.invoke(index.kt:363) at io.dcloud.uniapp.vue.IndexKt$setupRenderEffect$2.invoke(index.kt:6535) at io.dcloud.uniapp.vue.IndexKt$setupRenderEffect$2.invoke(index.kt:6534) at io.dcloud.uniapp.vue.IndexKt$flushJobs$1.invoke(index.kt:5162) at java.lang.reflect.Method.invoke(Native Method) at io.dcloud.uts.UTSPromiseKt$callFunction$invoke$1.invoke(UTSPromise.kt:31) at io.dcloud.uts.UTSPromiseKt.callFunction(UTSPromise.kt:42) at io.dcloud.uts.UTSPromiseKt$handleUTSPromise$1.invoke(UTSPromise.kt:439) at java.lang.reflect.Method.invoke(Native Method) at io.dcloud.uts.UTSPromiseKt$callFunction$invoke$1.invoke(UTSPromise.kt:31) at io.dcloud.uts.UTSPromiseKt.callFunction(UTSPromise.kt:42) at io.dcloud.uts.UTSPromise$Companion$_immediateFn$1.invoke(UTSPromise.kt:367) at io.dcloud.uts.UTSPromise$Companion$_immediateFn$1.invoke(UTSPromise.kt:366) at io.dcloud.uts.UTSTimerKt.setTimeout$lambda$0(UTSTimer.kt:95) at io.dcloud.uts.UTSTimerKt.$r8$lambda$UAyJ1gnsCkiOBi4f4GjC1hFRNNA(Unknown Source:0) at io.dcloud.uts.UTSTimerKt$$ExternalSyntheticLambda0.run(D8$$SyntheticClass:0) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:487) at java.util.concurrent.FutureTask.run(FutureTask.java:264) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:307) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1251) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:668) at java.lang.Thread.run(Thread.java:1012) 14:57:05.249 Possible Unhandled Promise Rejection: [java.util.ConcurrentModificationException] {cause: null, message: null} 还有这个: [java.lang.NullPointerException] {cause: null, message: null}


更多关于connectEventSource请求sse流在uni-app中频繁更新会各种报错导致整个软件不可用比如点击没效果点击其他tabBar也无效的实战教程也可以访问 https://www.itying.com/category-93-b0.html

5 回复

不用自己研究啦,看这个插件:https://ext.dcloud.net.cn/plugin?id=23902

更多关于connectEventSource请求sse流在uni-app中频繁更新会各种报错导致整个软件不可用比如点击没效果点击其他tabBar也无效的实战教程也可以访问 https://www.itying.com/category-93-b0.html


不行呀,我们这个用不上这个插件

我也是说,用这个插件局限性太大, 我们就想传统服务器自己接入ai服务。这个connectEventSource一点不好用,不支持post。uni.request他会一次性返回所有数据,没有chunk的效果(App上,h5上正常),uniappx

回复 FireFlyTest: 不存在你说的局限性,可以用你的传统服务器

从错误信息来看,问题主要出现在两个方面:

  1. ConcurrentModificationException - 并发修改异常
  2. NullPointerException - 空指针异常

这些错误是由于SSE流数据频繁更新导致的UI线程与数据更新线程冲突。在高频数据更新场景下,Vue的响应式系统与uni-app的渲染机制产生了竞争条件。

主要问题点:

1. 频繁的nextTick调用

nextTick(() => {
  // 内容更新
  nextTick(() => {
    emit('updateScrollTop')
  })
})

双重nextTick在高频更新下会导致渲染队列混乱。

2. 缺少数据更新节流 毫秒级的数据更新直接触发UI重渲染,造成性能瓶颈。

3. 事件源管理不完善 错误处理中多次调用eventSource?.close()可能导致状态不一致。

解决方案:

const handleAPICall = () => {
  let displayMessage = '';
  let updateScheduled = false;
  
  eventSource = uni.connectEventSource({ url: HTTP_REQUEST_URL + '/api/chat/stream' });
  
  eventSource?.onMessage(({ data }) => {
    try {
      const jsonData = JSON.parseObject(data as string);
      const output = jsonData?.get("output") as UTSJSONObject | null;
      const rawText = output?.get("text");
      const newText = typeof rawText === 'string' ? rawText : '';
      displayMessage += newText;

      // 使用requestAnimationFrame进行节流更新
      if (!updateScheduled) {
        updateScheduled = true;
        requestAnimationFrame(() => {
          contents.value = displayMessage;
          if (isSend.value) {
            isSend.value = false;
            if (timer !== null) clearInterval(timer as number);
          }
          emit('updateScrollTop');
          updateScheduled = false;
        });
      }

      if (output?.get("finish_reason") == 'stop') {
        // 确保最终状态更新
        contents.value = displayMessage;
        emit('updateScrollTop');
        emit('stop');
        eventSource?.close();
      }
    } catch (e) {
      console.error('SSE Error:', e);
      handleError();
    }
  });
};

const handleError = () => {
  if (eventSource) {
    eventSource.close();
    eventSource = null;
  }
  isSend.value = false;
  emit('stop');
  emit('updateScrollTop');
};
回到顶部