Cordova中EventSource/fetch处理SseEmitter为何与Chrome行为不同?
问题
在Google Chrome浏览器中,EventSource和fetch可以逐个获取SseEmitter端点返回的数据包,实现类似ChatGPT的流式token生成效果。但在Android和iOS平台的Cordova应用中,EventSource与fetch请求会缓冲后端响应,直到服务器停止发送后才将文本一次性推送给JavaScript代码,无法实现流式效果。请问该行为的原因是什么?能否不使用WebSocket连接在Cordova中实现相同的流式token生成效果?
一、Cordova中响应被缓冲的原因
- WebView默认策略:Cordova基于Android Chrome WebView、iOS WKWebView/UIWebView,这些WebView为优化普通HTTP请求性能,默认会缓冲完整响应后再传递给JavaScript,直接破坏了SSE的流式传输逻辑。
- 头部标识缺失:若后端未正确返回
Content-Type: text/event-stream、Cache-Control: no-cache、Connection: keep-alive等头部,WebView会判定这是普通响应并启用缓冲机制。 - 中间层干扰:Cordova应用可能经过应用内HTTP拦截插件、系统代理等中间层,这些组件会主动缓冲响应内容,导致流式数据无法实时传递到前端。
二、不使用WebSocket实现流式效果的方案
1. 强制关闭WebView响应缓冲
- Android端配置:修改Cordova Android平台代码,针对SSE接口禁用缓冲:
webView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE); webView.setWebViewClient(new WebViewClient() { @Override public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { String url = request.getUrl().toString(); if (url.contains("/your-sse-endpoint")) { try { HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); conn.setRequestProperty("Cache-Control", "no-cache"); conn.setRequestProperty("Connection", "keep-alive"); return new WebResourceResponse( "text/event-stream", "UTF-8", conn.getInputStream() ); } catch (IOException e) { e.printStackTrace(); } } return super.shouldInterceptRequest(view, request); } }); - iOS端配置:针对WKWebView,在请求时添加禁用缓冲的头部:
var request = URLRequest(url: URL(string: "your-sse-endpoint-url")!) request.setValue("no-cache", forHTTPHeaderField: "Cache-Control") request.setValue("keep-alive", forHTTPHeaderField: "Connection") webView.load(request)
2. 优化后端SSE输出配置
在Java后端的SseEmitter代码中,补充必要的响应头并强制刷新输出:
public SseEmitter callApiAsync(HttpRequest request) { SseEmitter emitter = new SseEmitter(); // 添加SSE必备响应头 emitter.setHeader("Content-Type", "text/event-stream"); emitter.setHeader("Cache-Control", "no-cache"); emitter.setHeader("Connection", "keep-alive"); emitter.setHeader("X-Accel-Buffering", "no"); // 禁用Nginx等反向代理的缓冲 executorService.execute(() -> { try { client.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()) .thenAccept(response -> { try (InputStream inputStream = response.body()) { BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); String line; Matcher matcher; while ((line = reader.readLine()) != null) { matcher = GPT_TEXT_PATTERN.matcher(line); line = matcher.find() ? matcher.group(1) : ""; line = line.replace(" ", EMPTRY_SPACE); emitter.send(SseEmitter.event().data(line)); // 强制刷新输出,确保数据实时推送 HttpServletResponse resp = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); if (resp != null) resp.flushBuffer(); } emitter.send(FINAL_MESSAGE); emitter.complete(); } catch (IOException e) { emitter.completeWithError(e); } }); } catch (Exception e) { emitter.completeWithError(e); } }); return emitter; }
3. 使用XMLHttpRequest替代EventSource/fetch
XMLHttpRequest在多数WebView中支持流式读取,通过监听progress事件获取实时数据:
function handleGptSuggestStreaming(params, target) { let xhr = new XMLHttpRequest(); const url = getFullURL(GPT_SUGGEST_URL.format(params)); xhr.open('GET', url, true); xhr.setRequestHeader('Accept', 'text/event-stream'); xhr.setRequestHeader('Cache-Control', 'no-cache'); let receivedData = ''; xhr.onprogress = function() { const newData = xhr.responseText.substring(receivedData.length); receivedData = xhr.responseText; // 分割SSE事件并处理 const events = newData.split(/\n\n/); events.forEach(event => { if (!event) return; const dataMatch = event.match(/^data: (.*)$/m); if (!dataMatch) return; const line = dataMatch[1]; if (line.includes(FINAL_MESSAGE)) { xhr.abort(); return; } const formattedText = line.replace(/\\n/g, '<br>') .replace(/\n/g, '<br>') .replace(/ /g, ' '); updateTargetMessage(formattedText, target); }); }; xhr.onerror = () => xhr.abort(); xhr.send(); }
原代码参考
前端EventSource方案JS代码
function handleGptSuggestCallEventSource(params, target) { let eventSource = mwSuggestEventSource(params) eventSource.onmessage = function(event) { if (event.data.includes(FINAL_MESSAGE)) { eventSource.close(); // Stop the EventSource return; } else { let formattedText = event.data.replace(/\\n/g, '<br>'); formattedText = formattedText.replace(/\n/g, '<br>'); formattedText = formattedText.replace(/ /g, ' '); updateTargetMessage(formattedText, target); } }; eventSource.onerror = function(err) { eventSource.close(); }; } function mwSuggestEventSource(params) { return callGPTEventSource(GPT_SUGGEST_URL.format(params)); } function callGPTEventSource(url) { try { return new EventSource(getFullURL(url)); } catch (e) { showToastError(getStr(e)); } }
后端Java代码
public SseEmitter callApiAsync(HttpRequest request) { SseEmitter emitter = new SseEmitter(); executorService.execute(() -> { try { client.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()) .thenAccept(response -> { try (InputStream inputStream = response.body()) { BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); String line; Matcher matcher; while ((line = reader.readLine()) != null) { matcher = GPT_TEXT_PATTERN.matcher(line); if (matcher.find()) { line = matcher.group(1); } else { line = ""; } line = line.replace(" ", EMPTRY_SPACE); emitter.send(SseEmitter.event().data(line)); // Stream each line to client } emitter.send(FINAL_MESSAGE); emitter.complete(); // Complete the emitter after finishing reading } catch (IOException e) { emitter.completeWithError(e); } }); } catch (Exception e) { emitter.completeWithError(e); } }); return emitter; }
内容的提问来源于stack exchange,提问作者Yonoss
相关产品推荐
相关产品推荐

