前言
废话
这两周我经历了太多,
这个想法大概两周前就有了,本来当时想写一篇博客记录一下,但是女朋友跟我分手了...
这两周悲痛欲绝,从来没有这么哭过,这种滋味太难受了 ┭┮﹏┭┮
但是在我不懈的努力下,似乎有复合的倾向,趁着今晚高兴,将这个想法记录下来,希望可以帮到其他人!
正言
最近公司做一个跨平台的app,技术使用uniapp,用到chat,需要对接sse接口。
vue使用的是v3,想查一下是否有v3的sse解决方案,查了一圈发现两种办法,renderjs
和 原生插件,因为renderjs
不兼容v3,最后想用原生插件解决。
结果去uniapp插件市场花20块买了个sse原生插件不兼容ios...
同事关于v3的chat代码涉及几千行,这咋办呢。
后来我在看一个uniapp组件库源码的时候给了我灵感,可以将renderjs
用v2的语法封装到一个组件里,然后将事件提供给父组件,这样就可以兼容v3和v2了,详情请看下文,
renderjs
renderjs介绍
那么renderjs是什么呢?下面是官方的解释:
emmmm,简单点说就是可以在renderjs
跑js的代码。
renderjs 语法
js
<script>
export default {
data() {
return {
hadnleData: {
key: 1,
text: "你好,世界!"
}
}
},
methods: {
start() {
this.hadnleData = Object.assign({}, this.hadnleData, {
...this.hadnleData,
key: this.hadnleData.key + 1,
});
},
receive(text) {
console.log("处理好的结果:", text)
}
}
}
</script>
<script module="demo" lang="renderjs">
export default {
methods: {
hadnle(data) {
let utf8Str = new TextEncoder().encode("Hello, World!");
let base64Str = btoa(String.fromCharCode(...utf8Str));
this.$ownerInstance.callMethod('receive', base64Str);
},
}
}
</script>
<template>
<view
:hadnleData="hadnleData"
:change:hadnleData="demo.hadnle"
>
<button @click="start">start</button>
</view>
</template>
上面是一个简单的字符串转base64代码,可以看到我们在renderjs
使用了new TextEncoder()
,它是一个web api,如果在app环境使用会报错:ReferenceError: TextEncoder is not defined
,但是可以在renderjs
正常运行。
我们可以在app环境用renderjs运行所有js库,有了以上的认识,我们只需要在renderjs
使用@microsoft/fetch-event-source
即可。
@microsoft/fetch-event-sources
是一个由微软开发的 npm 库,它提供了一个改进的 API,用于发起 Event Source 请求,也称为服务器发送事件(Server-Sent Events)。这个库基于 Fetch API,提供了比默认浏览器 EventSource API 更多的功能。
实现代码
核心组件
下面的代码非常简单,大家自己看吧
js
<script>
export default {
props: {},
data() {
return {
stopCount: 0,
renderjsData: {
url: "",
key: 0,
body: ""
}
}
},
methods: {
stopChat() {
this.stopCount += 1
},
/**
* 开始chat对话
* @param body
* @param url
* @param headers
*/
startChat({ body, url, headers = {} }) {
this.renderjsData = Object.assign({}, this.renderjsData, {
key: this.renderjsData.key + 1,
body: JSON.stringify(body),
url: url,
headers
});
},
open() {
this.$emit("onOpen")
},
message(msg) {
this.$emit("onMessage", msg)
},
error(err) {
this.$emit("onError", err)
},
finish() {
this.$emit("onFinish")
}
},
}
</script>
<script module="chat" lang="renderjs">
import { fetchEventSource } from '@microsoft/fetch-event-source';
export default {
data() {
return {
ctrl: null,
}
},
methods: {
/**
* 停止生成
*/
stopChatCore() {
this.ctrl?.abort();
},
/**
* 开始对话
*/
startChatCore({ url, body, headers }) {
if (!url) return;
try {
this.ctrl = new AbortController();
fetchEventSource(
url,
{
method: 'POST',
openWidthHidden: true,
signal: this.ctrl.signal,
headers: {
"Content-Type": "application/json",
...headers,
},
body: body,
onopen: () => {
this.$ownerInstance.callMethod('open');
},
onmessage: ({ data }) => {
this.$ownerInstance.callMethod('message', data);
},
onerror: (err) => {
this.$ownerInstance.callMethod('error', err);
},
}).then(() => {
this.$ownerInstance.callMethod('finish');
}).catch(err => {
this.$ownerInstance.callMethod('error', err);
})
} catch (e) {
console.log(e);
}
}
}
}
</script>
<template>
<view
:renderjsData="renderjsData"
:change:renderjsData="chat.startChatCore"
:stopCount="stopCount"
:change:stopCount="chat.stopChatCore"
/>
</template>
使用demo
js
<template>
<ChatSSEClient
ref="chatSSEClientRef"
@onOpen="openCore"
@onError="errorCore"
@onMessage="messageCore"
@onFinish="finishCore"
/>
<button @click="start">开始</button>
<button @click="stop">停止</button>
<template v-if="loading">
<view>{{ openLoading ? "正在连接sse..." : '连接完成!' }}</view>
<view>{{ loading ? "加载中..." : '' }}</view>
</template>
<view>
{{ responseText }}
</view>
</template>
<script setup>
import ChatSSEClient from "../../components/gao-ChatSSEClient.vue";
import { ref } from 'vue'
const chatSSEClientRef = ref(null);
const responseText = ref("");
const loading = ref(false);
const openLoading = ref(false);
const openCore = () => {
openLoading.value = false;
console.log("open sse");
}
const errorCore = (err) => {
console.log("error sse:", err);
}
const messageCore = (msg) => {
console.log("message sse:", msg);
responseText.value += `${msg}\n`
}
const finishCore = () => {
console.log("finish sse")
loading.value = false;
}
const start = () => {
if (loading.value) return;
openLoading.value = true;
loading.value = true;
responseText.value = "";
chatSSEClientRef.value.startChat({
// 将它换成你的地址
url: "",
headers: {
authorization: "",
},
body: {
"messages":[
{ "role":"user", "content": "你好!" }
],
"stream":true,
"model":"deepseek-chat",
"temperature":1,
"presence_penalty":0,
"frequency_penalty":0,
"top_p":1
},
})
}
const stop = () => {
chatSSEClientRef.value.stopChat()
console.log("stop");
}
</script>
uniapp 插件
代码上传到了uniapp插件市场,有需要的同学可以去这上面下载,包含了完整demo项目。
sse 客户端组件,支持v2、v3、安卓、ios、浏览器 - DCloud 插件市场
最后,希望对大家有帮助,再见!