😃😃😃 本人能力有限,欢迎大佬指正改进~

1. 前言

最近和后端同学I’M 渣渣一起完成了一个 Demo 级的多人聊天室应用,实现了聊天室的基本功能。

采用前后端分离方案,我负责前端代码的编写,I’M 渣渣实现了后端的接口。

前后端都部署到了自己的阿里云服务器上。

预览地址:多人聊天室

源码仓库:[前端][后端]

2. 应用简介

1. 主要功能

  1. 用户注册
  2. 用户登录
  3. 发送、接收消息
  4. 退出功能

2. 主要用到的技术

前端:

  1. React 脚手架Create-React-App
  2. 状态管理redux + react-redux
  3. 前端路由 React-Router、路由鉴权
  4. 少量使用 AntD 组件库 (Icon 图标、Notification 通知提醒框、Message 全局提示)

这里仅记录了前端的相关技术,后端基于TCP/HTTP协议,采用 C/C++实现,详情请查看基于阿里云用 C/C++做了一个 http 协议的 web 聊天室的服务器——《干饭聊天室》

3. 主要功能实现

1. 路由鉴权

将用户的登录状态放入redux,默认为false未登录。

App组件挂载时,判断用户登录状态,若已经登录,则直接跳转到聊天室页面;若未登录,则跳转到注册/登录页面,让用户登录。

1
2
3
4
5
6
7
8
9
10
11
12
13
<Switch>
{this.props.loginState ? (
<Fragment>
<Route path="/room" component={Room} />
<Redirect to="/room" />
</Fragment>
) : (
<Fragment>
<Route path="/welcome" component={Welcome} />
<Redirect to="/welcome" />
</Fragment>
)}
</Switch>

2. 用户注册

需要验证用户填写的用户名、密码是否符合规范,若不符合规范给出提示并直接return,若符合规范才发送注册请求给服务器。

1
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
// 点击按钮,登录
register = () => {
// 数字字母组合,字母开头
const unameReg = /^[a-zA-Z][a-zA-Z0-9]{2,9}$/;
const uname = this.uname.value;
const pwd = this.pwd.value;
const pwdAgian = this.pwdAgian.value;
// 判断用户名是否符合规则
if (!unameReg.test(uname)) {
this.openUnameError();
return;
}
// 判断密码长度
if (!(pwd.length >= 6 && pwd.length <= 16)) {
this.openPwdError();
return;
}
// 判断两次输入的密码是否一致
if (pwd !== pwdAgian) {
this.openPwdUnEqual();
return;
}
// 调用接口,发送注册请求
const url = `${constUrl}/register`;
axios({
method: 'get',
url,
params: {
name: uname,
pwd,
},
})
.then(res => {
// 注册成功
if (res.data.register === 0) {
this.openRegisterSuccess();
this.uname.value = '';
this.pwd.value = '';
this.pwdAgian.value = '';
// 跳转到登录页
this.props.history.replace(`/welcome/login`);
} else {
// 注册失败,打开相应的提示框
switch (res.data.error) {
case 0: {
this.openUnameReuse();
return;
}
default: {
this.openOtherError();
return;
}
}
}
})
.catch(err => console.error(err));
};

3. 用户登录

登录功能没什么好说的,收集参数并发送请求就可以了,若登录成功,则给出提示,并更改redux中的登录状态为true,页面自动会跳转到聊天室页面。若登陆失败,则根据服务器返回的数据,给出相应提示。

1
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
login = async () => {
const url = `${constUrl}/login`;
const name = this.inputUname.value;
const pwd = this.inputPwd.value;
axios({
method: 'get',
url,
params: {
name,
pwd,
},
})
.then(res => {
if (res.data.login === 0) {
this.props.login(name);
this.openLoginSuccess();
return;
}
switch (res.data.error) {
case 0: {
this.openUnameNotFound();
this.props.logout();
return;
}
case 1: {
this.openPwdError();
this.props.logout();
return;
}
case 2: {
this.openOtherError();
this.props.logout();
return;
}
default:
return;
}
})
.catch(err => console.error(err));

4. 接收消息

接受消息功能,同样也是向服务器发送请求,获得所需消息,并展示在页面上。

这里需要将展示聊天记录容器的滚动条自动调整到底部,详情见JavaScript 实现容器滚动条默认出现在底部位置

由于发送一次接收消息的请求,只能得到一次消息,所以我在componentDidMount()生命周期函数中,开启了一个定时器,每隔 1 秒就发送一次请求,获得所有消息,展示到页面上。由于 React 的diff算法,即使没有新的消息,频繁地发送请求,也不会有浪费太多性能。记得在componentWillUnmount()中,清除定时器。

1
2
3
4
5
6
7
8
9
10
componentDidMount() {
this.getMsg();
this.msgUpdate = setInterval(() => {
this.getMsg();
}, 1000);
}
componentWillUnmount() {
// 清除定时器
clearInterval(this.msgUpdate);
}
1
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
// 获取所需消息
getMsg = () => {
const url = `${constUrl}/information`;
const name = this.props.name;
axios({
method: 'get',
url,
params: { name },
// responseType: 'blob',
})
.then(res => {
console.log(res);
// 获取人数
const userCount = res.data.userCount;
this.setState({ userCount });
// 获取新数据
const newMsg = res.data.msg;
// 获取原数据
const oldMsg = this.state.msg;
if (res.data.information === 0) {
this.setState({ msg: [...oldMsg, ...newMsg] }, () => {
// 滚动条自动到底部
this.messageBox.scrollTop = this.messageBox.scrollHeight;
});
}
})
.catch(err => console.error(err));
};

5. 发送消息

发送请求之前,先判断输入框是否为空。不为空,再进行下一步,发送请求。

成功发送消息后,调用一次接收消息函数,获得所有消息,即可将刚刚发送的消息立即展示到页面。

1
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
// 发送消息
sendMsg = () => {
const msg = this.editMsg.value;
const name = this.props.name;
if (msg === '') {
message.warning('请输入消息!');
return;
}
const url = `${constUrl}/information`;
axios({
method: 'post',
url,
params: {
msg,
name,
time: new Date().getTime(),
id: nanoid(),
},
})
.then(res => {
// console.log(res);
if (res.data.msg === 0) {
this.editMsg.value = '';
message.success('发送成功!');
// 调用获取所有消息函数
this.getMsg();
}
})
.catch(err => console.error(err));

6. 退出功能

退出时,发送请求给服务器,告诉服务器有用户退出了、哪个用户退出了,主要用于展示当前在线人数。

redux中的登录状态改为false,页面自动跳转到注册/登录页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
logout = () => {
const url = `${constUrl}/logout`;
const name = this.props.name;
axios({
method: 'get',
url,
params: {
name,
},
});
this.openLogout();
this.props.logout();
};

4. 前端部署

前端应用经过React脚手架打包后,部署到了阿里云服务器上。

采用nginx服务器配置,配置如下:

1
2
3
4
5
6
7
8
server{
listen 80;
root /home/www/chatRoom;
server_name 47.110.144.145;
error_page 404 /index.html;

location /{}
}

5. Todo

由于本人能力有限,本 Demo 还有一些BUG没有解决:

  1. 当前在数人数显示模块没有正常工作,若用户不点击退出按钮,直接关闭浏览器页面,没有监听到用户的退出动作。
  2. 接收到的消息中,空格?=#等字符会显示其编码,无法正常显示。

将来可能添加的功能:

  1. 采用token验证用户身份

  2. 自定义背景图片切换

  3. 支持黑暗模式

  4. 适配移动端

  5. 支持上传头像

  6. 支持发送图片