关于项目中如何实现webscoket

288 阅读4分钟

小智同学-组件和路由配置

目标

实现小智同学页面的静态结构和样式

从素材中获取小智同学的结构与样式

结构

import Icon from '@/components/Icon'
import { NavBar, Input } from 'antd-mobile'
import { useHistory } from 'react-router-dom'
import styles from './index.module.scss'const Chat = () => {
  const history = useHistory()
​
  return (
    <div className={styles.root}>
      {/* 顶部导航栏 */}
      <NavBar className="fixed-header" onBack={() => history.go(-1)}>
        小智同学
      </NavBar>
​
      {/* 聊天记录列表 */}
      <div className="chat-list">
        {/* 机器人的消息 */}
        <div className="chat-item">
          <Icon type="iconbtn_xiaozhitongxue" />
          <div className="message">你好!</div>
        </div>
​
        {/* 用户的消息 */}
        <div className="chat-item user">
          <img src={'http://toutiao.itheima.net/images/user_head.jpg'} alt="" />
          <div className="message">你好?</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>
  )
}
​
export default Chat

样式

@import '~@scss/hairline.scss';
​
.root {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  padding: 46px 0 50px 0;
​
  :global {
    .fixed-header {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
    }
​
    .chat-list {
      height: 100%;
      overflow-y: scroll;
      padding: 15px 16px;
    }
​
    .chat-item {
      display: flex;
      margin-bottom: 12px;
​
      img {
        width: 32px;
        height: 32px;
        border-radius: 50%;
        object-fit: cover;
      }
​
      .icon {
        width: 32px;
        height: 32px;
        margin-right: 8px;
      }
​
      .message {
        max-width: 254px;
        padding: 12px 8px;
        line-height: 1.4;
        border-radius: 5px;
        font-size: 14px;
        background-color: #f7f8fa;
      }
    }
​
    .user {
      flex-direction: row-reverse;
​
      .icon,
      img {
        margin-right: 0;
        margin-left: 8px;
      }
    }
​
    .input-footer {
      position: fixed;
      left: 0;
      right: 0;
      bottom: 0;
      height: 50px;
      padding: 7px 16px;
​
      .input {
        height: 36px;
        min-height: 0;
        padding-left: 27px;
        border-radius: 18px;
        background-color: #f7f8fa;
        font-size: 14px;
        &::placeholder {
          font-size: 14px;
        }
      }
      .no-border {
        @include hairline-remove(bottom);
      }
​
      .icon {
 
        position: absolute;
        top: 50%;
        margin-top: -1.73333vw;
        margin-left: -2.33333vw;
        font-size: 6.46667vw;
 
      }
    }
  }
}

路由配置

App.tsx

<PrivateRoute path="/chat">
       <Chat></Chat>
</PrivateRoute>

效果

image.png

小智同学-动态渲染聊天记录列表

目标

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

步骤

  1. 声明一个数组状态
const [messageList, setMessageList] = useState<
  {
    type: 'robot' | 'user'
    text: string
  }[]
>([
  { type: 'robot', text: '亲爱的用户您好,小智同学为您服务。' },
  { type: 'user', text: '你好' }
])
  1. 从 Redux 中获取当前用户基本信息
// 当前用户信息
const { user } = useInitState(getUser, 'profile')
  1. 根据数组数据,动态渲染聊天记录列表
{/* 聊天记录列表 */}
<div className="chat-list">
  {messageList.map((msg, index) => {
    // 机器人的消息
    if (msg.type === 'robot') {
      return (
        <div className="chat-item" key={index}>
          <Icon type="iconbtn_xiaozhitongxue" />
          <div className="message">{msg.text}</div>
        </div>
      )
    }
    // 用户的消息
    else {
      return (
        <div className="chat-item user" key={index}>
          <img src={user.photo} alt="" />
          <div className="message">{msg.text}</div>
        </div>
      )
    }
  })}
</div>

效果

websocket

问题

如何让服务器主动给客户端发请求?

HTTP协议(ajax)的缺陷

WebSocket 是一种数据通信协议,类似于我们常见的 http 协议。

初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?

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

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

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

轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。

image.png

websocket简介

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

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

典型的websocket应用场景:

  • 即时通讯.....客服
  • 聊天室 广播
  • 点餐

socket技术

  1. 客户端:发socket请求

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

    1. socket.io

测试

socketio-chat-h9jt.herokuapp.com/

小智同学-整体思路

基本流程

建立与服务器的连接

image.png

小智同学-代码实现

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

目标

使用 socket.io 客户端与服务器建立 WebSocket 长连接

socket.io的API

安装包:npm i socket.io-client 只安装客户端要使用到的包

api

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

服务器进行通讯

// connect, disconnect 是固定的名字
client.on('connect', () => {})  // 当和服务器建立连接成功,这个事件就会触发
client.on('disconnect', () => {})    // 和服务器断开链接,就会触发disconnect
​
​
// message是事件名,这个可以由后端去修改
client.on('message', () => {})  // 接收到服务器的消息,这个事件就会触发
// 主动给服务器发送消息
client.emit('message', 值)
​
​
// 主动关闭和服务器的链接
client.close()

实现思路

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

操作步骤

  1. 安装 socket.io 客户端库:socket.io-client
npm i socket.io-client
  1. 在进入机器人客服页面时,创建 socket.io 客户端
import io from 'socket.io-client'
import { getToken } from '@/utils/storage'
// 用于缓存 socket.io 客户端实例
const clientRef = useRef<Socket | null>(null)
​
useEffect(() => {
  // 创建客户端实例
  const client = io('http://xxxx.net', {
    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()
  }
}, [])