React + Express + Socket.io之间的实时通信【2】:注册登录
接连着昨日的年轻莽撞,今天继续研究如何去制作一个类Slack、Discord的网页聊天室App。
其实这篇文在1月23日开始起草的,然后写代码写着写着就忘了写文。
再加上近期加入了一个新的项目,自己的项目不得不搁置一下。
前端
页面设计的工作我交给了reactstrap包,其实用react-bootstrap包、或者干脆直接引入Bootstrap的CSS文件都是可以的。
1 | npm install reactstrap |
仿制Discord的登录&注册页面还是相当容易的:
这里就不说写页面的具体细节,只挑几个我花了时间去搞的地方说。
1. 卡片居中
居中,是前端界最老生常谈的话题之一。浏览器上搜索“居中”一词,会发现十年前大家在聊怎么居中,几年后在聊怎么居中,现在还有GitHub网页上出现没有好好居中的标签。
Discord的登录/注册页面的设计方案很简单,正中间一个卡片,上面嘎嘎放表单即可。
我的实现:
1 | <Container className='d-flex vh-100'> |
这里推荐一下微软近期出的强力工具:PowerToys,用快捷键 Windows
+ Shift
+ C
就可以在屏幕上吸色了,吸的RGB值正好用来给我们的标签添加颜色样式。
2. 生日日期选择
这个其实就是:
1 | <Container> |
不过重点不在这里,而在JSX中用.map()
方法生成下拉选项。
先看一眼年份的:
1 | const currentYear = new Date().getFullYear(); |
其实可以再简化一些,不过能看就行!
月份的逻辑相同:
1 | const months = [ |
日期就更简单了:
1 | <Input> |
看着没做每个月内有多少天的逻辑对吧?其实Discord也是这样设计的。这种逻辑交给后端就好啦~
不过我研究了会儿都没实现出来Discord的效果:用户还未选择时,年月份三个下拉框都默认显示“年”/“月”/“日”。
3. 数据处理
今日的重头戏。用户在注册时这些数据总要传到服务端去的吧?今天就是来解决这个的。
先装包:
1 | npm install react-redux @reduxjs/toolkit |
Redux的知识点可以去看我之前的一篇关于React的文章。Redux的概念可以用三个东西来概括:Action、Store和Reducer。每当用户与某个组件交互时就会触发Action(比方说点击按钮),接着Action会携带着数据去往Store进行存储,途中遇到Reducer、状态被按照我们要求的进行了更改,最终回到Store这个大仓库手里。
为什么我们要使用Redux呢?如果我们需要在客户端向服务端发送数据,Redux可以更好地帮我们管理这些数据,并且Store还是全局的,在任一组件中我们都可以访问Store中的数据。
我们可以用@reduxjs/toolkit包配置Redux Store。新建一个文件store.js
:
1 | import { configureStore } from "@reduxjs/toolkit"; |
不过在完成这段代码之前,我们需要初始化状态。我们的状态需要什么样的数据存储其中?作为一个可以登陆注册的网页App,我们需要存储用户的登录状态、用户的信息、错误信息等等。这些存储的动作都需要一个Reducer来完成。
Redux官网中在文档里使用了createSlice
方法来创建State Slice。
新建一个文件authSlice.js
:
1 | import { createSlice } from "@reduxjs/toolkit"; |
这下我们可以完成store.js
了:
1 | import { configureStore } from "@reduxjs/toolkit"; |
Store和Reducer都有了,自然少不了Action。
Socket.io的连接逻辑我会在下一篇文章中讲解,这里我们只需要知道,当用户点击注册按钮时,我们需要将用户的信息发送到服务端。这个过程就是一个Action。
目前我们只需要验证用户信息是否合法,所以只写了一个Action:authActions.js
。
验证用户信息需要使用到Socket.io来和服务端进行通信:
1 | import socketIO from "socket.io-client"; |
此处假设服务端的端口是4000。
4. 注册和登录
这两个页面的逻辑是一样的,都是用户输入信息后点击按钮,触发Action,将用户信息发送到服务端。
在Socket.io的连接逻辑中,用户点击按钮后、信息会在客户端中被发送到服务端,服务端会对用户信息进行验证,如果验证通过,服务端会返回一个Token给客户端,客户端将Token存储到Store中;如果验证不通过,服务端则会返回一个错误信息给客户端。
这里只讲一下注册页面的逻辑。
1 | import { useState } from "react"; |
useState
是React的一个Hook,用于在函数组件中使用状态。
useNavigate
是React Router的一个Hook,用于在函数组件中进行页面跳转。
registerUser
是等会儿我们会定义的Action,先不写。
1 | const Register = () => { |
useState
的用法是这样的:接受一个参数作为状态的初始值,比方说我们这里是一个字典,包含着注册页面中所有输入框的值。它会返回一个数组,第一个元素代表着状态的当前值,第二个元素代表着一个函数,用于更新状态。
每当用户输入信息时,我们都应该更新状态。React提供了一个 onChange
事件供我们使用:
1 | const handleChange = e => { |
每次用户输入信息时,
handleChange
函数都会被触发,而handleChange
函数会调用setUserData
函数,更新状态。
表单被用户提交后,我们也需要触发一个Action来发送用户信息到服务端:
1 | const handleSubmit = e => { |
useDispatch
是React Redux的一个Hook,用于在函数组件中传递Action。
那么 registerUser
方法是怎么写的呢?
1 | const registerUser = (userData, navigate) => { |
socket.emit
用于发送数据到服务端;socket.on
用于接收服务端返回的数据。
registerUser
方法会先发送用户填写的信息到服务端,接着监听服务端返回的数据。如果服务端返回的数据中 status
是 00000
,则说明注册成功,我们就跳转到登录页面;如果不是,我们就在控制台打印出服务端返回的错误信息。
而在服务端中,我们需要接受名为 register
的事件、验证用户填写的信息,然后发送 newRegisteredUser
事件:
1 | socket.on("register", async userData => { |
User
是一个Mongoose模型,用于操作MongoDB数据库。
1
2
3
4
5
6
7 const mongoose = require("mongoose");
mongoose.connect("mongodb://localhost:27017/hotaru")
.then(() => console.log("Connected to MongoDB"))
.catch(err => console.error("Could not connect to MongoDB", err));
module.exports = mongoose;
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 const bcrypt = require( "bcrypt");
const mongoose = require("./mongodb");
const UserSchema = new mongoose.Schema({
emailAddress: {
type: String,
unique: true
},
username: {
type: String,
unique: true
},
password: {
type: String,
set(val) { return bcrypt.hashSync(val, 10) },
select: false
},
DOBYear: {
type: Number
},
DOBMonth: {
type: Number
},
DOBDay: {
type: Number
},
createTime: {
type: Date,
default: Date.now
}
})
const User = mongoose.model("User", UserSchema);
module.exports = { User };
bcrypt
是一个用于加密密码的包,不只是加密密码,我们验证用户登录时也会用到它。最根本的原因是我们不会在数据库中存储用户的明文密码,要验证用户登陆的话,只能用加密后的用户输入的密码和数据库中的密码进行比对。
我这里也根据网上的文章自己定义了一套错误码,未来可能会展开说说。
这样,我们就完成了注册页面的逻辑。
登录页面的逻辑和注册页面的逻辑是一样的,只是在 registerUser
方法中,我们需要发送 login
事件,而在服务端中,我们需要接受名为 login
的事件:
1 | const loginUser = (userData, navigate) => { |
localStorage
是浏览器提供的一个API,用于在浏览器中存储数据。这里存储了JWT Token,以后会提到这是什么。
1 | socket.on("login", async userData => { |
一套组合拳下来,一旦用户的信息被验证成功,就会跳转到频道页面。