| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133 |
- /**
- * SSE 封装,使微信小程序等环境可通过 enableChunked + 分块解析使用 SSE
- * - 小程序:使用 uni.request enableChunked,按块接收后解析 SSE 格式并回调 onMessage
- * - H5:可优先使用原生 EventSource,不支持时回退到 chunked 方式
- */
- // 将 UTF-8 字节数组解码为字符串(兼容小程序无 TextDecoder、避免中文乱码)
- function utf8Decode(bytes) {
- if (!bytes || bytes.length === 0) return '';
- if (typeof TextDecoder !== 'undefined') {
- return new TextDecoder('utf-8').decode(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes));
- }
- const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
- let s = '';
- let i = 0;
- const len = arr.length;
- while (i < len) {
- const b = arr[i++];
- if (b < 0x80) {
- s += String.fromCharCode(b);
- } else if (b < 0xe0 && i < len) {
- s += String.fromCharCode(((b & 0x1f) << 6) | (arr[i++] & 0x3f));
- } else if (b < 0xf0 && i + 1 < len) {
- s += String.fromCharCode(((b & 0x0f) << 12) | ((arr[i++] & 0x3f) << 6) | (arr[i++] & 0x3f));
- } else if (b < 0xf8 && i + 2 < len) {
- let c = ((b & 0x07) << 18) | ((arr[i++] & 0x3f) << 12) | ((arr[i++] & 0x3f) << 6) | (arr[i++] & 0x3f);
- c -= 0x10000;
- s += String.fromCharCode(0xd800 + (c >> 10), 0xdc00 + (c & 0x3ff));
- } else {
- s += String.fromCharCode(b);
- }
- }
- return s;
- }
- // 将分块数据转为字符串(UTF-8 解码,避免中文乱码)
- function chunkToText(data) {
- if (data == null) return '';
- if (typeof data === 'string') return data;
- if (data instanceof ArrayBuffer) data = new Uint8Array(data);
- if (data instanceof Uint8Array) return utf8Decode(data);
- return '';
- }
- // 解析 SSE 缓冲区:按 \n\n 分割消息,解析 data:/event:/id: 行,返回 { messages, remaining }
- function parseSSEBuffer(buffer) {
- const messages = [];
- let rest = buffer;
- for (;;) {
- const idx = rest.indexOf('\n\n');
- if (idx === -1) break;
- const block = rest.slice(0, idx);
- rest = rest.slice(idx + 2);
- let data = '';
- let event = '';
- let id = '';
- block.split('\n').forEach((line) => {
- const s = line.trim();
- if (s.startsWith('data:')) data = (data ? data + '\n' : '') + s.slice(5).trim();
- else if (s.startsWith('event:')) event = s.slice(6).trim();
- else if (s.startsWith('id:')) id = s.slice(3).trim();
- });
- messages.push({ data, event, id });
- }
- return { messages, remaining: rest };
- }
- /**
- * 创建 SSE 连接(兼容微信小程序等无原生 EventSource 的环境)
- * @param {string} url - 完整请求 URL
- * @param {object} options - { header, timeout, ... } 会透传给 uni.request
- * @returns {object} - { abort(), onMessage(cb), onOpen(cb), onError(cb) }
- */
- export function createSSEConnection(url, options = {}) {
- let buffer = '';
- const callbacks = { onMessage: null, onOpen: null, onError: null };
- const { header = {}, timeout = 0, ...rest } = options;
- const requestTask = uni.request({
- url,
- method: 'GET',
- header: { ...header },
- enableChunked: true,
- timeout,
- ...rest,
- fail(err) {
- if (callbacks.onError) callbacks.onError(err);
- if (options.fail) options.fail(err);
- }
- });
- if (requestTask && typeof requestTask.onChunkReceived === 'function') {
- requestTask.onChunkReceived((res) => {
- try {
- buffer += chunkToText(res.data);
- const { messages, remaining } = parseSSEBuffer(buffer);
- buffer = remaining;
- messages.forEach((msg) => {
- if (callbacks.onMessage) callbacks.onMessage(msg);
- });
- } catch (e) {
- if (callbacks.onError) callbacks.onError(e);
- }
- });
- }
- if (requestTask && typeof requestTask.onHeadersReceived === 'function') {
- requestTask.onHeadersReceived(() => {
- if (callbacks.onOpen) callbacks.onOpen();
- });
- }
- return {
- get requestTask() {
- return requestTask;
- },
- onMessage(cb) {
- callbacks.onMessage = cb;
- return this;
- },
- onOpen(cb) {
- callbacks.onOpen = cb;
- return this;
- },
- onError(cb) {
- callbacks.onError = cb;
- return this;
- },
- abort() {
- if (requestTask && typeof requestTask.abort === 'function') requestTask.abort();
- }
- };
- }
|