基于websocket协议实现聊天机器人功能

490 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第9天,点击查看活动详情

一.渲染基础聊天记录列表

将聊天数据存在数组状态中,再动态渲染到界面上

1.声明一个名为messageList的状态,用来保存聊天记录

const [messageList, setMessageList] = useState<
  {
    type: 'robot' | 'user'
    text: string
  }[]
>([
  { type: 'robot', text: '亲爱的用户您好,小智同学为您服务。' },
  { type: 'user', text: '你好' }
])

2.从 Redux 中获取当前用户基本信息

// 获取当前用户信息
const { user } = useInitState(getUser, 'profile')

useInitState为自定义hooks

useEffect发请求并获取数据,参数1为请求名称,参数2位获取数据名

import { RootState } from "@/types/store";
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";

export default function useInitState<StateName extends keyof RootState> 
(action:()=>void,stateName:StateName){
  const dispatch=useDispatch()
  const state = useSelector( (state:any)=>state[stateName])
  
  useEffect(()=>{
    dispatch(action())
  },[dispatch,action])

  return state
}

3. 根据数组数据,渲染聊天记录列表

css就不搞过来了,主要是思路的梳理

    <div className={styles.root}>
      {/* 顶部导航栏 */}
      <NavBar className="fixed-header" onBack={() => history.go(-1)}>
        狗狗聊天室
      </NavBar>
      {/* 聊天记录列表 */}
      <div className="chat-list">
       {/* 机器人的消息 */} 
      {messageList.map((msg,index)=>{
          if(msg.type==='robot'){
            return (
                <div className="chat-item" key={index}>
                   <div className='name'>客服</div>
                   <img src={'https://ns-strategy.cdn.bcebos.com/ns-strategy/upload/fc_big_pic/part-00435-1823.jpg'} alt="" />                  
                  <div className="message">{msg.text}</div>
                </div> 
            )}else {
              return (
                <div className="chat-item user" key={index}>
                   <div className='me'>{user.name}</div>
               <img src={user.photo} alt="" />
                <div className="message">{msg.text}</div>
              </div>
              )
            }
            })}
      </div>
      {/* 底部消息输入框 */}
      <div className="input-footer">
        <List.Item
          className="login-code-extra"
          extra={<span className="code-extra">发送</span>}>
          <Input 
          className="no-border"
           placeholder="请描述您的问题" />
        </List.Item>
        <Icon type="iconbianji" />
      </div>
    </div>
      )
    }
  })}
</div>

二.websocket

HTTP协议(ajax)的缺陷

WebSocket 是一种数据通信协议,类似于我们常见的 http 协议。我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?

答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起。http基于请求响应实现。

举例来说,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。

这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用"轮询"open in new window:每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。

轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,前辈们就琢磨出了WebSocket 。

image.png

websocket简介

WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。

它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术open in new window的一种。

典型的websocket应用场景:即时通讯,客服,聊天室,广播,点餐

socket技术

  1. 客户端:发socket请求

    1. 可以用原生的
    2. 可以使用包 socket.io
  2. 服务器端: 提供socket服务

    1. socket.io

点我进体验聊天室

三.聊天室搭建

1.流程:

建立与服务器的连接===>双向通信===>断开连接

image.png

2.socket.io的基本使用:

本项目使用的是基于 WebSocket 协议的 socket.io 接口。我们可以使用专门的 socket.io 客户端库,就能轻松建立起连接并进行互相通信。

(1)服务器建立链接

import io from 'socket.io-client'

// 和服务器建立了链接
const client = io('地址', {
    query: {
        token: 用户token
    },
    transports: ['websocket']
})

(2)服务器进行通讯

// connect, disconnect 是固定的名字
client.on('connect', () => {})  // 当和服务器建立连接成功,这个事件就会触发

client.on('disconnect', () => {})    // 和服务器断开链接,就会触发disconnect

// message是事件名,这个可以由后端去修改
// 接收到服务器的消息
client.on('message', () => {})  // 接收到服务器的消息,这个事件就会触发

// 主动给服务器发送消息
client.emit('message', 值)


// 主动关闭和服务器的链接
client.close()

3.代码实现:

借助 useEffect,在进入页面时调用客户端库建立 socket.io 连接

(1)安装 socket.io 客户端库:socket.io-client (只安装客户端要使用到的包)

yarn add socket.io-client

(2)在进入机器人客服页面时,创建 socket.io 客户端

import useInitState from '@/hooks/useInitialState '
import io, { Socket } from 'socket.io-client'
import { getToken } from '@/utils/storage'
//注:getToken用于获取token,发请求时需要传值

const history = useHistory()
const {user} =useInitState(getUser,'profile')
const [messageList ,setMessageList]=useState<{
  type:'robot' | 'user' //1对1聊天
  text:string
 }[]>([])

// 用于缓存 socket.io 客户端实例
const clientRef = useRef<Socket | null>(null)

useEffect(() => {
  // 创建客户端实例
  const client = io('****', {
    transports: ['websocket'],
    // 在查询字符串参数中传递 token
    query: {
      token: getToken().token
    }
  })

  // 监听连接成功的事件
  client.on('connect', () => {
    // 向聊天记录中添加一条消息
    setMessageList(messageList => [
      ...messageList,
      { type: 'robot', text: '骚狗,干嘛呢' }
    ])
  })

  // 监听收到消息的事件
  client.on('message', data => {
    console.log('>>>>收到 socket.io 消息:', data)
  })

  // 将客户端实例缓存到 ref 引用中
  clientRef.current = client

  // 在组件销毁时关闭 socket.io 的连接
  return () => {
    client.close()
  }
}, [])

4.给机器人发消息

思路:将输入框内容通过 socket.io 发送到服务端,使用 socket.io 实例的 emit() 方法发送信息

(1)声明一个状态,并绑定消息输入框

// 输入框中的内容
const [message, setMessage] = useState('')
<Input
  className="no-border"
  placeholder="请描述您的问题"
  value={message}
  onChange={e => setMessage(e)}
  />

(2)为消息输入框添加键盘事件,在输入回车时发送消息

<Input
	// ...
      onKeyUp={onSendMessage}
  />
 // 按发送消息
const onSendMessage = (e:any) => {
  console.log(e,'e');
  // console.log(message,'message')
  if(e.keyCode===13){
     if (message.trim() === '') {
    return
    }
  // 1. 发消息
  clientRef.current?.emit('message', { msg: message, timestamp: Date.now() })
  // 2. 添加到聊天记录
  setMessageList([...messageList, { type: 'user', text: message }])
  // 3. 清空
  setMessage('')
  }
}

5.接收机器人回复的消息

通过socket.io监听回复的消息,并添加到聊天列表中。给socket.io 实例监听 message 事件,在事件回调中接收信息,添加状态

// 监听收到消息的事件
client.on('message', (data) => {
  // 向聊天记录中添加机器人回复的消息
  setMessageList((messageList) => [
    ...messageList,
    { type: 'robot', text: data.msg },
  ])
})

6.滚动条优化-滚动条滚动到最底部

当消息较多出现滚动条时,有后续新消息的话总将滚动条滚动到最底部.手动设置元素滚动条的位置。当聊天数据的变化时,改变聊天容器元素的 scrollTop 值让页面滚到最底部.

(1)获得聊天列表的容器元素。声明一个 ref 并设置到聊天列表的容器元素上

// 用于操作聊天列表元素的引用
const chatListRef = useRef<HTMLDivElement>(null)

(2)监听聊天数据的变化

<div className="chat-list" ref={chatListRef}>

(3)通过 useEffect 监听聊天数据变化,对聊天容器元素的 scrollTopopen in new window 进行设置

// 监听聊天数据的变化,改变聊天容器元素的 scrollTop 值让页面滚到最底部
useEffect(() => {
  const current = chatListRef.current! //!必须有
  current.scrollTop = current.scrollHeight
}, [messageList]) //监听列表

(4)scrollTop

Element.scrollTop 属性可以获取或设置一个元素的内容垂直滚动的像素数。一个元素的 scrollTop 值是这个元素的内容顶部(卷起来的)到它的视口可见内容(的顶部)的距离的度量。当一个元素的内容没有产生垂直方向的滚动条,那么它的 scrollTop 值为0

(5)scrollHeight

Element.scrollHeight这个只读属性是一个元素内容高度的度量,包括由于溢出导致的视图中不可见内容。scrollHeight的值等于该元素在不使用滚动条的情况下为了适应视口中所用内容所需的最小高度。没有垂直滚动条的情况下,scrollHeight值与元素视图填充所有内容所需要的最小值相同。

效果展示

image.png