这节来写下聊天的前端页面:
左侧是聊天室列表,右边是聊天界面。
点击左侧切换聊天室,就可以在不同聊天室聊天。
先写下样式:
return <div id="chat-container">
<div className="chat-room-list">
<div className="chat-room-item">技术交流群</div>
<div className="chat-room-item selected">技术交流群</div>
<div className="chat-room-item">技术交流群</div>
<div className="chat-room-item">技术交流群</div>
</div>
<div className="message-list">
<div className="message-item">
<div className="message-sender">
<img src="http://localhost:9000/chat-room/dong.png" />
<span className="sender-nickname">神说要有光</span>
</div>
<div className="message-content">
你好
</div>
</div>
<div className="message-item">
<div className="message-sender">
<img src="http://localhost:9000/chat-room/dong.png" />
<span className="sender-nickname">神说要有光</span>
</div>
<div className="message-content">
你好
</div>
</div>
<div className="message-item from-me">
<div className="message-sender">
<img src="http://localhost:9000/chat-room/dong.png" />
<span className="sender-nickname">神说要有光</span>
</div>
<div className="message-content">
你好
</div>
</div>
</div>
</div>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
样式 index.scss
#chat-container {
margin: 20px;
display: flex;
flex-direction: row;
width: 800px;
height: 600px;
.chat-room-list {
width: 150px;
border: 1px solid #000;
overflow-y: auto;
}
.chat-room-item {
line-height: 50px;
padding-left: 20px;
border: 1px solid #000;
cursor: pointer;
&:hover, &.selected{
background: lightgreen;
}
}
.message-list {
border: 1px solid #000;
flex: 1;
overflow-y: auto;
.message-item {
padding: 20px;
display: flex;
flex-wrap: wrap;
.message-sender {
width:100%;
img{
width: 20px;
height: 20px;
padding-right: 10px;
}
}
.message-content{
border: 1px solid #000;
width:max-content;
padding: 10px 20px;
border-radius: 4px;
background: skyblue;
}
&.from-me{
text-align: right;
justify-content: right;
.message-content{
text-align: right;
justify-content: right;
background: #fff;
}
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
因为样式比较复杂,我们用到了 sass
安装下:
npm install --save-dev sass
看下效果:
布局比较简单,整体宽度 800px,左侧宽度固定,右侧 flex:1
然后右边的布局也是 flex 布局,有 .from-me 的设置 justify-content:right;
然后我们请求下聊天室列表:
const [roomList, setRoomList] = useState<Array<Chatroom>>();
async function queryChatroomList() {
try{
const res = await chatroomList();
if(res.status === 201 || res.status === 200) {
setRoomList(res.data.map((item: Chatroom) => {
return {
...item,
key: item.id
}
}));
}
} catch(e: any){
message.error(e.response?.data?.message || '系统繁忙,请稍后再试');
}
}
useEffect(() => {
queryChatroomList();
}, []);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
roomList?.map(item => {
return <div className="chat-room-item" data-id={item.id} key={item.id} >{item.name}</div>
})
}
2
3
4
5
然后 interfaces 加一下请求聊天记录的接口:
export async function chatHistoryList(id: number) {
return axiosInstance.get(`/chat-history/list?chatroomId=${id}`);
}
2
3
组件里当点击聊天室的时候,就查询对应的聊天记录显示:
useEffect(() => {
queryChatroomList();
}, []);
interface ChatHistory {
id: number
content: string
type: number
chatroomId: number
senderId: number
createTime: Date,
sender: UserInfo
}
const [chatHistory, setChatHistory] = useState<Array<ChatHistory>>();
async function queryChatHistoryList(chatroomId: number) {
try{
const res = await chatHistoryList(chatroomId);
if(res.status === 201 || res.status === 200) {
setChatHistory(res.data.map((item: Chatroom) => {
return {
...item,
key: item.id
}
}));
}
} catch(e: any){
message.error(e.response?.data?.message || '系统繁忙,请稍后再试');
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
return <div id="chat-container">
<div className="chat-room-list">
{
roomList?.map(item => {
return <div className="chat-room-item" data-id={item.id} key={item.id} onClick={() => {
queryChatHistoryList(item.id);
}}>{item.name}</div>
})
}
</div>
<div className="message-list">
{chatHistory?.map(item => {
return <div className="message-item" data-id={item.id} key={item.id} >
<div className="message-sender">
<img src={item.sender.headPic} />
<span className="sender-nickname">{item.sender.nickName}</span>
</div>
<div className="message-content">
{item.content}
</div>
</div>
})}
</div>
</div>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
然后我们加上聊天的功能:
用绝对定位把这个 div 定位在右下角:
position: relative;
.message-input {
width: 648px;
border: 1px solid #000;
height: 100px;
position: absolute;
bottom: 0;
right: 0;
.message-type {
display: flex;
.message-type-item {
width: 100px;
&:hover {
font-weight: bold;
cursor: pointer;
}
}
}
.message-input-area {
width: 650px;
display: flex;
.message-send-btn {
width: 50px;
height: 50px;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
但这样会和 .message-list 重合:
我们改下 .message-list 的高度
height: calc(100% - 100px);
然后把它的 border-bottom 去掉:
border-bottom: 0;
这样,布局就完成了。
我们来加一下发消息的功能:
用 inputText 的 state 保存输入的内容,点击发送的时候调用 sendMessage 方法。
sendMessage 方法从 localStorage 拿 userId,然后单独一个 state 保存 chatroomId。
点击切换聊天室的时候 setChatroomId
当收到新的消息的时候重新查询聊天记录
roomId 改变的时候重新链接一下:
import { Button, Input, message } from "antd";
import { useEffect, useRef, useState } from "react";
import { io, Socket } from "socket.io-client";
import './index.scss';
import { chatHistoryList, chatroomList } from "../../interfaces";
import { UserInfo } from "../UpdateInfo";
import TextArea from "antd/es/input/TextArea";
interface JoinRoomPayload {
chatroomId: number
userId: number
}
interface SendMessagePayload {
sendUserId: number;
chatroomId: number;
message: Message
}
interface Message {
type: 'text' | 'image'
content: string
}
type Reply = {
type: 'sendMessage'
userId: number
message: Message
} | {
type: 'joinRoom'
userId: number
}
interface Chatroom {
id: number;
name: string;
createTime: Date;
}
interface ChatHistory {
id: number
content: string
type: number
chatroomId: number
senderId: number
createTime: Date,
sender: UserInfo
}
interface User {
id: number;
email: string;
headPic: string;
nickName: string;
username: string;
createTime: Date;
}
export function getUserInfo(): User {
return JSON.parse(localStorage.getItem('userInfo')!);
}
export function Chat() {
const socketRef = useRef<Socket>();
const [roomId, setChatroomId] = useState<number>();
useEffect(() => {
if(!roomId) {
return;
}
const socket = socketRef.current = io('http://localhost:3005');
socket.on('connect', function() {
const payload: JoinRoomPayload = {
chatroomId: roomId,
userId: getUserInfo().id
}
socket.emit('joinRoom', payload);
socket.on('message', (reply: Reply) => {
queryChatHistoryList(roomId);
});
});
return () => {
socket.disconnect();
}
}, [roomId]);
function sendMessage(value: string) {
if(!value) {
return;
}
if(!roomId) {
return;
}
const payload: SendMessagePayload = {
sendUserId: getUserInfo().id,
chatroomId: roomId,
message: {
type: 'text',
content: value
}
}
socketRef.current?.emit('sendMessage', payload);
}
const [roomList, setRoomList] = useState<Array<Chatroom>>();
async function queryChatroomList() {
try{
const res = await chatroomList();
if(res.status === 201 || res.status === 200) {
setRoomList(res.data.map((item: Chatroom) => {
return {
...item,
key: item.id
}
}));
}
} catch(e: any){
message.error(e.response?.data?.message || '系统繁忙,请稍后再试');
}
}
useEffect(() => {
queryChatroomList();
}, []);
const [chatHistory, setChatHistory] = useState<Array<ChatHistory>>();
async function queryChatHistoryList(chatroomId: number) {
try{
const res = await chatHistoryList(chatroomId);
if(res.status === 201 || res.status === 200) {
setChatHistory(res.data.map((item: Chatroom) => {
return {
...item,
key: item.id
}
}));
}
} catch(e: any){
message.error(e.response?.data?.message || '系统繁忙,请稍后再试');
}
}
const [inputText, setInputText] = useState('');
return <div id="chat-container">
<div className="chat-room-list">
{
roomList?.map(item => {
return <div className="chat-room-item" key={item.id} data-id={item.id} onClick={() => {
queryChatHistoryList(item.id);
setChatroomId(item.id);
}}>{item.name}</div>
})
}
</div>
<div className="message-list">
{chatHistory?.map(item => {
return <div className="message-item" data-id={item.id}>
<div className="message-sender">
<img src={item.sender.headPic} />
<span className="sender-nickname">{item.sender.nickName}</span>
</div>
<div className="message-content">
{item.content}
</div>
</div>
})}
</div>
<div className="message-input">
<div className="message-type">
<div className="message-type-item" key={1}>表情</div>
<div className="message-type-item" key={2}>图片</div>
<div className="message-type-item" key={3}>文件</div>
</div>
<div className="message-input-area">
<TextArea className="message-input-box" value={inputText} onChange={(e) => {
setInputText(e.target.value)
}}/>
<Button className="message-send-btn" type="primary" onClick={() => {
sendMessage(inputText)
setInputText('');
}}>发送</Button>
</div>
</div>
</div>
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
测试下:
聊天没问题,就是样式不大对。
我们加一下判断:
className={`message-item ${item.senderId === userInfo.id ? 'from-me' : ''}`}
现在样式就对了,只不过自己聊没意思。
在数据库里查一下上面那个聊天室的另一个用户:
换个浏览器登录小强的账号:
然后在光的账号这边看下:
收到了小强的消息。
我们聊一会天:
能聊天了。
就是每次需要手动滚动到底部才能看到新消息。
我们加一下自动滚动:
<div id="bottom-bar" key='bottom-bar'></div>
setTimeout(() => {
document.getElementById('bottom-bar')?.scrollIntoView({block: 'end'});
}, 300);
2
3
这样,基本聊天功能就完成了。
但这样其实性能并不好,没必要每发一条消息就查一下聊天记录。
我们在服务端把消息存到聊天记录表之后,把这条消息返回:
此外,sender 的信息也要查出来:
把 UserService 导出,然后在 chat 模块引入:
返回 history 的时候把 sender 也查出来:
@Inject(UserService)
private userService: UserService
@SubscribeMessage('sendMessage')
async sendMessage(@MessageBody() payload: SendMessagePayload) {
const roomName = payload.chatroomId.toString();
const history = await this.chatHistoryService.add(payload.chatroomId, {
content: payload.message.content,
type: payload.message.type === 'image' ? 1 : 0,
chatroomId: payload.chatroomId,
senderId: payload.sendUserId
});
const sender = await this.userService.findUserDetailById(history.senderId);
this.server.to(roomName).emit('message', {
type: 'sendMessage',
userId: payload.sendUserId,
message: {
...history,
sender
}
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
然后前端就可以直接在后面添加了:
socket.on('message', (reply: Reply) => {
if(reply.type === 'sendMessage') {
setChatHistory((chatHistory) => {
return chatHistory ? [...chatHistory, reply.message] : [reply.message]
});
setTimeout(() => {
document.getElementById('bottom-bar')?.scrollIntoView({block: 'end'});
}, 300);
}
});
2
3
4
5
6
7
8
9
10
这样,全程只需要查询一次聊天记录,性能好很多。
# 总结
这节我们实现了聊天页面。
首先,我们写了布局,在左侧展示聊天室列表。
点击聊天室的时候,在右侧展示查询出的聊天记录。
点击发送消息的时候,通过 socket 链接来 emit 消息。
监听服务端的 message 消息,有新消息的时候添加到聊天记录里,并通过 scrollIntoView 滚动到底部。
这样,多个用户在不同房间聊天的功能就完成了。