uni-app App端使用RenderJS处理SSE流
使用RenderJS方案的优势:原生 API 支持:可以直接利用浏览器稳定、高效的API,无需任何第三方库。代码清晰:将网络层的复杂性(如流式解析)完全隔离在RenderJS中,逻辑层代码只关心业务状态,职责分明。官方方案:这是uni-app官方提供的用于在 App 端扩展 Web能力的机制,兼容性有保障。需要注意的点:通信开销:逻辑层和视图层(RenderJS)之间的数据传递存在一定的性能开销。
使用 RenderJS
是在 uni-app
的 App 端利用 H5 API(如 EventSource
)处理 SSE 流的一个非常巧妙且官方支持的方案。
这个方案的核心思想是:将网络通信和流式数据处理的任务“外包”给 RenderJS
运行的视图层 WebView,因为它拥有一个完整的浏览器环境。然后,RenderJS
将处理好的数据片段通过特定机制传递回逻辑层(Service 层)进行业务处理和界面渲染。
下面我将根据你提供的思路,给出一个完整的、可运行的代码实现和详细讲解。
核心原理讲解
<script module="xxx" lang="renderjs">
: 这段脚本不运行在 App 的主 JS 引擎(V8 或 JSCore)中,而是运行在渲染页面的 WebView 里。因此,它可以访问window
、document
、EventSource
等所有标准的 Web API。- 逻辑层与视图层通信:
- 逻辑层 -> RenderJS: 通过给挂载了
RenderJS
模块的 DOM 元素传递props
或直接调用RenderJS
内部的方法。 - RenderJS -> 逻辑层: 这是关键。
RenderJS
脚本可以通过this.ownerInstance.callMethod('methodName', args)
来调用 Vue 组件实例(逻辑层)中定义的方法。
- 逻辑层 -> RenderJS: 通过给挂载了
- 流程:
- 用户在界面点击“发送”按钮,触发逻辑层的一个方法。
- 逻辑层调用
RenderJS
模块暴露出的startStream
方法,并传入必要的参数(如请求 URL)。 RenderJS
模块收到指令后,创建EventSource
实例,开始监听来自服务器的 SSE 数据流。- 每当
EventSource
收到一个数据片段(onmessage
事件),RenderJS
就会调用this.ownerInstance.callMethod('emits', ...)
将这个数据片段“发射”回逻辑层。 - 逻辑层的
emits
方法被触发,接收到数据片段,然后更新data
中的msgList
,从而驱动界面实时更新,实现打字机效果。
具体代码实现
假设我们有一个聊天页面 pages/ai-chat-renderjs/ai-chat-renderjs.vue
。
Step 1: template
结构
我们需要一个 DOM 元素来挂载 RenderJS
模块。同时,通过 :prop
向 RenderJS
传递动态数据(比如请求的 URL 或触发信号),并通过 ref
来方便地调用它的方法。
<template>
<view class="chat-container">
<!-- 消息列表 -->
<view v-for="(msg, index) in msgList" :key="index" class="message">
<text class="role">{{ msg.role }}: </text>
<text class="content">{{ msg.content }}</text>
</view>
<!-- 操作按钮 -->
<button @click="start" :disabled="isLoading">发送消息</button>
<button @click="stop" v-if="isLoading">停止接收</button>
<!--
关键部分:
1. 创建一个 view 元素用于挂载 RenderJS 模块。
2. 使用 `module="sse"` 来命名这个模块。
3. 使用 `ref="sseRef"` 以便在 <script> 中通过 this.$refs.sseRef 访问它。
4. `:prop="reqData"` 把逻辑层的数据传递给 RenderJS。
5. `@update="onRenderJSUpdate"` 是一个示例,用于接收 RenderJS 的主动更新(此例中我们用 callMethod,不用这个)。
-->
<view :prop="reqData" module="sse" ref="sseRef"></view>
</view>
</template>
Step 2: RenderJS
脚本
这是处理 SSE 的核心,它运行在 WebView 中。
<script module="sse" lang="renderjs">
export default {
data() {
return {
eventSource: null // 保存 EventSource 实例
}
},
methods: {
/**
* @description: 由逻辑层调用,用于启动 SSE 连接
* @param {object} newReqData - 包含 URL 等请求信息
*/
startStream(newReqData) {
// 防止重复创建
if (this.eventSource) {
this.eventSource.close();
}
if (!newReqData || !newReqData.url) {
console.error('[RenderJS] URL is missing.');
return;
}
// 1. 使用 H5 的 EventSource API
this.eventSource = new EventSource(newReqData.url);
// 2. 监听消息
this.eventSource.onmessage = (event) => {
if (event.data === '[DONE]') {
this.eventSource.close();
// 通知逻辑层数据流已结束
this.emitToLogic({
type: 'done'
});
return;
}
try {
const data = JSON.parse(event.data);
// 通知逻辑层收到了新的数据片段
this.emitToLogic({
type: 'message',
content: data.text
});
} catch (e) {
console.error('[RenderJS] JSON parse error:', e);
}
};
// 3. 监听错误
this.eventSource.onerror = (err) => {
console.error('[RenderJS] EventSource failed:', err);
this.eventSource.close();
// 通知逻辑层发生了错误
this.emitToLogic({
type: 'error',
error: 'Stream connection error'
});
};
},
/**
* @description: 由逻辑层调用,用于手动停止 SSE 连接
*/
stopStream() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
console.log('[RenderJS] Stream manually stopped.');
}
},
/**
* @description: 封装向逻辑层发射数据的方法
* @param {object} data - 要发送的数据
*/
emitToLogic(data) {
// this.ownerInstance 是 RenderJS 访问 Vue 组件实例(逻辑层)的桥梁
// callMethod 用于调用 Vue 组件实例上的方法
if (this.ownerInstance) {
this.ownerInstance.callMethod('handleRenderJSEvent', data);
}
}
}
}
</script>
Step 3: 逻辑层 script
这是 Vue 组件的主脚本,负责管理状态、业务逻辑和与 RenderJS
交互。
<script>
export default {
data() {
return {
msgList: [], // 聊天消息列表
isLoading: false,
// 这个对象将作为 prop 传递给 RenderJS
// 虽然在这个例子中我们直接通过 callMethod 传递 URL,但 prop 机制也很有用
reqData: {}
};
},
beforeDestroy() {
// 页面销毁前,确保停止 SSE 连接,防止内存泄漏
this.stop();
},
methods: {
// 按钮点击事件:开始接收
start() {
if (this.isLoading) return;
this.isLoading = true;
// 为新的 AI 回复创建一个占位消息
const newMsgIndex = this.msgList.push({
role: 'AI',
content: ''
}) - 1; // 获取新消息的索引
const requestData = {
url: 'http://localhost:3000/ai-stream', // 你的 SSE 接口
// 把新消息的索引也传过去,方便 RenderJS 回传时我们知道要更新哪条消息
targetIndex: newMsgIndex
};
// 调用 RenderJS 模块中的 startStream 方法
this.$refs.sseRef.startStream(requestData);
},
// 按钮点击事件:停止接收
stop() {
if (!this.isLoading) return;
// 调用 RenderJS 模块中的 stopStream 方法
this.$refs.sseRef.stopStream();
this.isLoading = false;
},
/**
* @description: 这个方法专门用于接收来自 RenderJS 的事件
* @param {object} event - 从 RenderJS 发射过来的数据对象
*/
handleRenderJSEvent(event) {
const lastMsg = this.msgList[this.msgList.length - 1];
if (!lastMsg || lastMsg.role !== 'AI') return;
switch (event.type) {
case 'message':
// 收到了新的消息片段,追加到最后一条 AI 消息的内容上
lastMsg.content += event.content;
this.scrollToBottom(); // 滚动到底部
break;
case 'done':
// 数据流结束
this.isLoading = false;
console.log('Stream finished.');
break;
case 'error':
// 发生错误
lastMsg.content += `\n[错误: ${event.error}]`;
this.isLoading = false;
break;
}
},
// 辅助方法:滚动到页面底部
scrollToBottom() {
this.$nextTick(() => {
uni.pageScrollTo({
scrollTop: 99999, // 一个足够大的值
duration: 100
});
});
}
}
}
</script>
总结与对比
使用 RenderJS
方案的优势:
- 原生 API 支持:可以直接利用浏览器稳定、高效的
EventSource
API,无需任何第三方库。 - 代码清晰:将网络层的复杂性(如流式解析)完全隔离在
RenderJS
中,逻辑层代码只关心业务状态,职责分明。 - 官方方案:这是
uni-app
官方提供的用于在 App 端扩展 Web能力的机制,兼容性有保障。
需要注意的点:
- 通信开销:逻辑层和视图层(RenderJS)之间的数据传递存在一定的性能开销。对于 AI 对话这种中等频率的更新,完全可以接受。但对于极高频(如每秒几百次)的通信场景,需要进行性能测试。
- 环境隔离:
RenderJS
中无法访问uni
对象(如uni.showToast
),也无法访问 Vuex。它的世界里只有标准的 Web API。所有需要uni
API 的操作都必须通过callMethod
通知逻辑层来完成。 - 调试:
RenderJS
中的console.log
会输出在 WebView 的控制台,而不是 App 的主控制台。调试时需要在 HBuilderX 中开启 WebView 调试。
总的来说,对于在 App 端实现 SSE 流式请求,RenderJS
方案是一个非常优雅和强大的选择,比 enableChunked
手动解析要简单可靠,也避免了引入额外的 fetch
polyfill 库。
更多推荐
所有评论(0)