小智同学-组件和路由配置
目标
实现小智同学页面的静态结构和样式
从素材中获取小智同学的结构与样式
结构
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>
效果
小智同学-动态渲染聊天记录列表
目标
将聊天数据存在数组状态中,再动态渲染到界面上
步骤
- 声明一个数组状态
const [messageList, setMessageList] = useState<
{
type: 'robot' | 'user'
text: string
}[]
>([
{ type: 'robot', text: '亲爱的用户您好,小智同学为您服务。' },
{ type: 'user', text: '你好' }
])
- 从 Redux 中获取当前用户基本信息
// 当前用户信息
const { user } = useInitState(getUser, 'profile')
- 根据数组数据,动态渲染聊天记录列表
{/* 聊天记录列表 */}
<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 就是这样发明的。
websocket简介
WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。
它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
典型的websocket应用场景:
- 即时通讯.....客服
- 聊天室 广播
- 点餐
socket技术
-
客户端:发socket请求
- 可以用原生的
- 可以使用包 socket.io
-
服务器端: 提供socket服务
- socket.io
测试
socketio-chat-h9jt.herokuapp.com/
小智同学-整体思路
基本流程
建立与服务器的连接
小智同学-代码实现
本项目聊天客服的后端接口,使用的是基于 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 连接
操作步骤
- 安装 socket.io 客户端库:
socket.io-client
npm i socket.io-client
- 在进入机器人客服页面时,创建 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()
}
}, [])