fix: 修复微信开发者工具 60 秒超时问题
- 将 AbortController 方案改为 Promise.race + setTimeout 外部超时控制 - 解决微信开发者工具底层 fetch 不兼容 AbortController signal 的问题 - 保持 300 秒超时时间不变 - 简化 abort 接口,统一使用 cleanup 函数
This commit is contained in:
@@ -194,14 +194,14 @@ function h5ConsumeSseText(block, onEvent, onError) {
|
|||||||
const event = JSON.parse(dataBuffer)
|
const event = JSON.parse(dataBuffer)
|
||||||
onEvent?.(event)
|
onEvent?.(event)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
onError?.(`SSE 解析失败: ${e.message}`)
|
onError?.(`SSE 解析失败:${e.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* H5 环境 SSE 流式读取核心实现
|
* H5 环境 SSE 流式读取核心实现
|
||||||
* 使用浏览器原生 fetch + ReadableStream + AbortController
|
* 使用 Promise.race + setTimeout 实现外部超时控制(兼容微信开发者工具)
|
||||||
* @param {string} url - 请求 URL
|
* @param {string} url - 请求 URL
|
||||||
* @param {Object} body - 请求体
|
* @param {Object} body - 请求体
|
||||||
* @param {Function} onEvent - 事件回调
|
* @param {Function} onEvent - 事件回调
|
||||||
@@ -209,63 +209,63 @@ function h5ConsumeSseText(block, onEvent, onError) {
|
|||||||
* @returns {Object} 包含 abort 方法的对象
|
* @returns {Object} 包含 abort 方法的对象
|
||||||
*/
|
*/
|
||||||
function h5NovelStream(url, body, onEvent, onError) {
|
function h5NovelStream(url, body, onEvent, onError) {
|
||||||
// 1. 创建 AbortController 用于超时控制
|
let reader = null
|
||||||
const controller = new AbortController()
|
let timeoutTimer = null
|
||||||
let abortedByUser = false // 标记是否用户主动中止
|
let isCompleted = false
|
||||||
let reader = null // 引用 reader 以便主动中止时取消
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), SSE_TIMEOUT_MS)
|
|
||||||
|
|
||||||
// 2. 统一的错误处理函数
|
// 统一的清理函数
|
||||||
const handleError = (err) => {
|
const cleanup = () => {
|
||||||
clearTimeout(timeoutId)
|
isCompleted = true
|
||||||
if (abortedByUser) {
|
if (timeoutTimer) clearTimeout(timeoutTimer)
|
||||||
// 用户主动中止,不报错
|
if (reader) {
|
||||||
return
|
reader.cancel().catch(() => {})
|
||||||
}
|
|
||||||
if (err && err.name === 'AbortError') {
|
|
||||||
onError?.('请求超时(300秒)')
|
|
||||||
} else {
|
|
||||||
onError?.(err?.message || '网络请求失败')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 发起 fetch 请求
|
// 创建超时 Promise(不依赖 AbortController)
|
||||||
fetch(url, {
|
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',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Accept': 'text/event-stream',
|
'Accept': 'text/event-stream',
|
||||||
...getAuthHeader()
|
...getAuthHeader()
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body)
|
||||||
signal: controller.signal
|
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
// 4. 检查响应状态
|
// 检查响应状态
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
clearTimeout(timeoutId)
|
cleanup()
|
||||||
onError?.(`请求失败: HTTP ${response.status}`)
|
onError?.(`请求失败:HTTP ${response.status}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 获取 ReadableStream 读取器
|
// 获取 ReadableStream 读取器
|
||||||
reader = response.body.getReader()
|
reader = response.body.getReader()
|
||||||
const decoder = new TextDecoder('utf-8')
|
const decoder = new TextDecoder('utf-8')
|
||||||
let buffer = ''
|
let buffer = ''
|
||||||
|
|
||||||
// 6. 循环读取流数据
|
// 循环读取流数据
|
||||||
function pump() {
|
function pump() {
|
||||||
return reader.read().then(({ done, value }) => {
|
return reader.read().then(({ done, value }) => {
|
||||||
if (done) {
|
if (done || isCompleted) {
|
||||||
clearTimeout(timeoutId)
|
cleanup()
|
||||||
// 处理缓冲区残留数据
|
|
||||||
if (buffer.trim()) h5ConsumeSseText(buffer, onEvent, onError)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. 解码二进制块为文本
|
// 解码二进制块为文本
|
||||||
buffer += decoder.decode(value, { stream: true })
|
buffer += decoder.decode(value, { stream: true })
|
||||||
|
|
||||||
// 8. 解析完整的事件(兼容 CRLF)
|
// 解析完整的事件(兼容 CRLF)
|
||||||
const events = buffer.split(/\r?\n\r?\n/)
|
const events = buffer.split(/\r?\n\r?\n/)
|
||||||
buffer = events.pop() // 最后一个可能不完整,保留到下次
|
buffer = events.pop() // 最后一个可能不完整,保留到下次
|
||||||
|
|
||||||
@@ -273,24 +273,29 @@ function h5NovelStream(url, body, onEvent, onError) {
|
|||||||
h5ConsumeSseText(event, onEvent, onError)
|
h5ConsumeSseText(event, onEvent, onError)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. 继续读取
|
// 继续读取
|
||||||
return pump()
|
return pump()
|
||||||
|
}).catch(err => {
|
||||||
|
cleanup()
|
||||||
|
throw err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return pump()
|
return pump()
|
||||||
}).catch(handleError)
|
})
|
||||||
|
|
||||||
// 10. 返回 abort 接口
|
// 使用 Promise.race 竞争超时和 fetch
|
||||||
|
Promise.race([fetchPromise, timeoutPromise]).catch(err => {
|
||||||
|
if (!isCompleted) {
|
||||||
|
cleanup()
|
||||||
|
onError?.(err.message || '请求超时')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 返回 abort 接口
|
||||||
return {
|
return {
|
||||||
abort: () => {
|
abort: () => {
|
||||||
clearTimeout(timeoutId)
|
cleanup()
|
||||||
abortedByUser = true
|
|
||||||
// 主动中止时取消 reader,释放底层资源
|
|
||||||
if (reader) {
|
|
||||||
reader.cancel().catch(() => {})
|
|
||||||
}
|
|
||||||
controller.abort()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -314,8 +319,8 @@ function consumeSseText(text, onEvent, onError) {
|
|||||||
const event = JSON.parse(dataStr)
|
const event = JSON.parse(dataStr)
|
||||||
onEvent?.(event)
|
onEvent?.(event)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
onError?.(`SSE 解析失败: ${e.message}`)
|
onError?.(`SSE 解析失败:${e.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user