From 6eadbce6f2acc8450f31a97dd6106b8a40786cc5 Mon Sep 17 00:00:00 2001 From: Peanut Date: Thu, 23 Jul 2026 22:12:42 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=BE=AE=E4=BF=A1?= =?UTF-8?q?=E5=BC=80=E5=8F=91=E8=80=85=E5=B7=A5=E5=85=B7=2060=20=E7=A7=92?= =?UTF-8?q?=E8=B6=85=E6=97=B6=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 AbortController 方案改为 Promise.race + setTimeout 外部超时控制 - 解决微信开发者工具底层 fetch 不兼容 AbortController signal 的问题 - 保持 300 秒超时时间不变 - 简化 abort 接口,统一使用 cleanup 函数 --- mini-program/src/services/shortNovel.js | 95 +++++++++++++------------ 1 file changed, 50 insertions(+), 45 deletions(-) diff --git a/mini-program/src/services/shortNovel.js b/mini-program/src/services/shortNovel.js index b444582..d84b4e9 100644 --- a/mini-program/src/services/shortNovel.js +++ b/mini-program/src/services/shortNovel.js @@ -194,14 +194,14 @@ function h5ConsumeSseText(block, onEvent, onError) { const event = JSON.parse(dataBuffer) onEvent?.(event) } catch (e) { - onError?.(`SSE 解析失败: ${e.message}`) + onError?.(`SSE 解析失败:${e.message}`) } } } /** * H5 环境 SSE 流式读取核心实现 - * 使用浏览器原生 fetch + ReadableStream + AbortController + * 使用 Promise.race + setTimeout 实现外部超时控制(兼容微信开发者工具) * @param {string} url - 请求 URL * @param {Object} body - 请求体 * @param {Function} onEvent - 事件回调 @@ -209,63 +209,63 @@ function h5ConsumeSseText(block, onEvent, onError) { * @returns {Object} 包含 abort 方法的对象 */ function h5NovelStream(url, body, onEvent, onError) { - // 1. 创建 AbortController 用于超时控制 - const controller = new AbortController() - let abortedByUser = false // 标记是否用户主动中止 - let reader = null // 引用 reader 以便主动中止时取消 - const timeoutId = setTimeout(() => controller.abort(), SSE_TIMEOUT_MS) + let reader = null + let timeoutTimer = null + let isCompleted = false - // 2. 统一的错误处理函数 - const handleError = (err) => { - clearTimeout(timeoutId) - if (abortedByUser) { - // 用户主动中止,不报错 - return - } - if (err && err.name === 'AbortError') { - onError?.('请求超时(300秒)') - } else { - onError?.(err?.message || '网络请求失败') + // 统一的清理函数 + const cleanup = () => { + isCompleted = true + if (timeoutTimer) clearTimeout(timeoutTimer) + if (reader) { + reader.cancel().catch(() => {}) } } - // 3. 发起 fetch 请求 - fetch(url, { + // 创建超时 Promise(不依赖 AbortController) + const timeoutPromise = new Promise((_, reject) => { + timeoutTimer = setTimeout(() => { + if (!isCompleted) { + cleanup() + reject(new Error('请求超时(300 秒)')) + } + }, SSE_TIMEOUT_MS) + }) + + // 创建 fetch Promise + const fetchPromise = fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream', ...getAuthHeader() }, - body: JSON.stringify(body), - signal: controller.signal + body: JSON.stringify(body) }).then(response => { - // 4. 检查响应状态 + // 检查响应状态 if (!response.ok) { - clearTimeout(timeoutId) - onError?.(`请求失败: HTTP ${response.status}`) + cleanup() + onError?.(`请求失败:HTTP ${response.status}`) return } - // 5. 获取 ReadableStream 读取器 + // 获取 ReadableStream 读取器 reader = response.body.getReader() const decoder = new TextDecoder('utf-8') let buffer = '' - // 6. 循环读取流数据 + // 循环读取流数据 function pump() { return reader.read().then(({ done, value }) => { - if (done) { - clearTimeout(timeoutId) - // 处理缓冲区残留数据 - if (buffer.trim()) h5ConsumeSseText(buffer, onEvent, onError) + if (done || isCompleted) { + cleanup() return } - // 7. 解码二进制块为文本 + // 解码二进制块为文本 buffer += decoder.decode(value, { stream: true }) - // 8. 解析完整的事件(兼容 CRLF) + // 解析完整的事件(兼容 CRLF) const events = buffer.split(/\r?\n\r?\n/) buffer = events.pop() // 最后一个可能不完整,保留到下次 @@ -273,24 +273,29 @@ function h5NovelStream(url, body, onEvent, onError) { h5ConsumeSseText(event, onEvent, onError) } - // 9. 继续读取 + // 继续读取 return pump() + }).catch(err => { + cleanup() + throw err }) } return pump() - }).catch(handleError) + }) - // 10. 返回 abort 接口 + // 使用 Promise.race 竞争超时和 fetch + Promise.race([fetchPromise, timeoutPromise]).catch(err => { + if (!isCompleted) { + cleanup() + onError?.(err.message || '请求超时') + } + }) + + // 返回 abort 接口 return { abort: () => { - clearTimeout(timeoutId) - abortedByUser = true - // 主动中止时取消 reader,释放底层资源 - if (reader) { - reader.cancel().catch(() => {}) - } - controller.abort() + cleanup() } } } @@ -314,8 +319,8 @@ function consumeSseText(text, onEvent, onError) { const event = JSON.parse(dataStr) onEvent?.(event) } catch (e) { - onError?.(`SSE 解析失败: ${e.message}`) + onError?.(`SSE 解析失败:${e.message}`) } } } -} \ No newline at end of file +}