一 React中使用WebSocket进行实时通讯,TS封装一个简易WebSocket(用SSE更好)
1.封装useWebSocket
import Taro from '@tarojs/taro';
import { useEffect, useRef, useState } from 'react';
//消息类型,下面的代码我用any类型,不建议用any,大家可以自己声明Message类型,为了方便我暂时用any 不用Message
type Message = {
type: string;
data: any;
};
type WebSocketOptions = {
url: string;
onmessage?: (message: any) => void;
reconnectInterval?: number;
reconnectAttempts?: number;
};
const defaultOptions: Required<WebSocketOptions> = {
url: '',//连接的长链接
onmessage: () => { },//消息
reconnectInterval: 1000,//重连时长设置
reconnectAttempts: Number.MAX_VALUE,//最大连接范围数
};
const useWebSocket = (
options: WebSocketOptions,
): [WebSocket | undefined, (message: any) => void, any, boolean] => {
const { url, reconnectInterval, reconnectAttempts, onmessage } = {
...defaultOptions,
...options,
};
const [isConnected, setIsConnected] = useState(false);//是否连接
const [reconnectCount, setReconnectCount] = useState(0);//用于判断重连
const [lastMessage, setLastMessage] = useState<any>();//最新的消息
const socketRef = useRef<any>();
const reconnectTimerRef = useRef<any>();
const connect = () => {
//连接函数封装
setIsConnected(false);
Taro.connectSocket({
url: url,
header: {
'content-type': 'application/json'
},
success: function () {
console.log('connect success')
}
}).then(task => {
socketRef.current = task
task.onOpen(function () {
console.log('WebSocket is connected');
setIsConnected(true);
setReconnectCount(0);
})
task.onMessage(function (event) {
console.log(event)
const message: any = JSON.parse(event.data);
console.log(`WebSocket received message: ${message}`);
setLastMessage(message)
onmessage(message)
})
task.onError(function (event) {
//异常问题
console.error('WebSocket error:', event);
})
task.onClose(function (event) {
//连接关闭
console.error(`WebSocket closed with code ${event.code}`);
setIsConnected(false);
if (reconnectCount < reconnectAttempts) {
//用于判断断开连接后重新连接
reconnectTimerRef.current = setTimeout(() => {
setReconnectCount((prevCount) => prevCount + 1);
// connect();
}, reconnectInterval);
}
})
})
};
useEffect(() => {
connect();
return () => {
socketRef.current?.close();
clearTimeout(reconnectTimerRef.current);
};
}, []);
const sendMessage = (message: any) => {
console.log(socketRef.current)
//用于发送消息
if (isConnected && socketRef.current) {
console.log(`WebSocket sending message: ${JSON.stringify(message)}`);
socketRef.current.send({
data: JSON.stringify(message)
})
} else {
console.error('Cannot send message - WebSocket is not connected');
}
};
//暴露出我们需要的
return [socketRef.current, sendMessage, lastMessage, isConnected];
};
export default useWebSocket;
2.在项目中引入
const [webSocket, sendMessage, lastMessage, isConnected] = useWebSocket({
url: 'wss://xxxxxxx', //这里放长链接 ,
onmessage: (message) => {
setUserMessage('')
setMessage(e => {
const last = e.pop();
if (last.role === 'robot') {
last.text = last.text + message.content
return e.concat([last])
} else {
const robotItem = { text: message.content, role: 'robot', type: 'text' };
return e.concat([last, robotItem])
}
})
if (message.status === 2) {
message.solutionVO && setMessage(e => e.concat(
{ type: 'component', role: 'robot', render: <NewMessage name={userMessage} solutionList={message.solutionVO}></NewMessage> },
))
setThinking(false)
} else {
setThinking(true)
}
}
});
3. 使用方法和说明
- 发送消息是用
sendMessage
//和后端约定好消息发送格式,一般需要登陆的话可以先发送一个用户登陆的消息,用作标识
sendMessage({});
- 最新消息可以直接在
lastMessage
中拿到 isConnected
用于判断是否连接webSocket
是暴露出来的整体,你可以我们封装下的所有方法
注:正常来说需要一个心跳来保持,这个需要和后端进行沟通,前端需要隔多少秒发送一个ping
,来检测连接是否正常,否则立马重连
二 实现 ChatGPT 聊天打字兼自动滚动效果
1.如何实现视图跟随内容滚动
首先如何实现聊天界面?其实每一个消息都是一个Message,所有的Message保存在messageData中,将messageData遍历即可实现,如下所示:
<ScrollView
id="viewCommunicationBody"
scrollWithAnimation
scrollY
scrollTop={scrollTop}
scrollIntoView={toView}
>
{
<View id="viewCommunInner">
messageData.map((el, i) => <Message id={`item-${i}` text={el.text} />)
</View>
}
</ScrollView>
这里用了ScrollView的scrollIntoView跟scrollTop来实现视图跟随内容滚动
在每次messageData增加了一条最新的数据让ScrollView滚动到最新的Message
useEffect(() => {
messageData && setToView(`item-${messageData.length - 1}`)
}, [messageData])
但是只是这样会有一个问题,那就是当Message中的文本太长将高度撑高,会出现最后一个Message部分文字被遮挡,这个时候就需要用scrollTop了
const handleScollTop = () => {
const query = Taro.createSelectorQuery()
query.select('#viewCommunicationBody').boundingClientRect()
query.select('#viewCommunInner').boundingClientRect()
query.exec((res) => {
if (res) {
const scrollViewHeight = res[0].height
const scrollContentHeight = res[1].height
if (scrollContentHeight > scrollViewHeight) {
const scrollTop = scrollContentHeight - scrollViewHeight + 220
setScrollTop(scrollTop)
}
}
})
}
想让 scroll-view 一直滚动到底部,只需要让 scroll-top 等于 scroll-view 内容高度减去 scroll-view 容器本身高度就可以了
2.打字效果实现
在上文封装的useWebSocket中可以获取到webSockect每次返回的数据,将每次返回的数据拼接再传入Message中就可以实现打字机效果了
const [messageData, setMessage] = useState<any[]>([])
const [webSocket, sendMessage, lastMessage, isConnected] = useWebSocket({
url: 'wss://xxxxxxxxx', //这里放长链接 ,
onmessage: (message) => {
// 这里可以获取到webSockect每次返回的数据
setMessage(e => {
const last = e.pop();
// 将获取到的数据拼接
if (last.role === 'robot') {
last.text = last.text + message.content
return e.concat([last])
} else {
// 传入Message组件
const robotItem = { text: message.content, role: 'robot', type: 'text' };
return e.concat([last, robotItem])
}
})
}
})
三 使用第三方插件微信同声传译将语音转文字,文字转语音
1.微信公众平台添加插件:微信同声传译
2.配置
在app.config中配置(version跟provider对应插件最新版本号跟AppID)
plugins: {
"WechatSI": {
"version": "0.0.7",
"provider": "wx069ba97219f66d99"
}
}
3.封装useRecorder
import Taro, { requirePlugin, useDidHide } from "@tarojs/taro";
import { useEffect, useRef, useState } from "react";
interface RecorderProps {
text: string
}
export function useRecorder(props: RecorderProps) {
const { text } = props
const [currentText, setCurrentText] = useState<string>('')
const innerAudioContext = useRef<any>()
const plugin = requirePlugin("WechatSI")
const recorderManager = plugin.getRecordRecognitionManager()
recorderManager.onRecognize = function (res) {
console.log("current result", res.result)
}
recorderManager.onStop = function (res) {
console.log("record file path", res.tempFilePath)
console.log("result", res.result)
setCurrentText(res.result)
}
recorderManager.onStart = function (res) {
console.log("成功开始录音识别", res)
}
recorderManager.onError = function (res) {
console.error("error msg", res.msg)
}
// 文字转语音
const playTextToVoice = () => {
//创建内部 audio 上下文 InnerAudioContext 对象。
innerAudioContext.current = Taro.createInnerAudioContext();
plugin.textToSpeech({
// 调用插件的方法
lang: 'zh_CN',
// lang: ‘en_US’,
content: text,
success: function (res) {
playAudio(res.filename);
}
});
}
// 播报语音
const playAudio = (e) => {
console.log(e, innerAudioContext)
innerAudioContext.current.src = e; //设置音频地址
innerAudioContext.current.play(); //播放音频
}
useDidHide(() => {
console.log('end')
innerAudioContext && innerAudioContext.current.stop();
innerAudioContext && innerAudioContext.current.destroy();
})
return { recorderManager, currentText, playTextToVoice, }
}
4.使用
import { Button, View } from '@tarojs/components'
import Taro from '@tarojs/taro';
import styles from './index.module.less'
import { useRecorder, } from '@/hooks/auth/useRecorder';
import { useEffect, useState } from 'react';
export default function index() {
const { recorderManager, currentText, playTextToVoice } = useRecorder(
{text:'这里传想要合成得语音'}
)
const handleClick = () => {
recorderManager.start({ duration: 30000, lang: "zh_CN" })
};
const handleComplete = () => {
recorderManager.stop(); // 停止录音
};
return (
<View className={styles.page}>
<Button className={styles.button} onTouchStart={handleClick} onTouchEnd={handleComplete}>开始录音</Button>
<Button className={styles.button} onClick={playTextToVoice}>播放合成语音</Button>
<View>语音识别内容:{currentText}</View>
</View>
)
}