前言
本人基本没有写过文章,所以文章写得烂。这算是第一次尝试为了写给别人看的文章了。 仓库地址
技术栈
typescript + socket.io + antd-mobile + react-group-transition + node + express
功能
- 实现登录,注册,持久化登录
- 头像上传和更换
- 实现主页宠物信息展示和信息提交
- 宠物点赞
- 实现了少量的页面过渡动画
- 实现用户信息通讯
功能实现
登录 持久化登录 注册
简要描述
- 我设计的mysql的user表字段为useranme password。username字段为主键。
- 注册页是上传信息,服务器检查username是否相同。相同则注册成功,否则失败。
- 登录页面,负责上传信息,服务器检查是否账号密码正确来确定是否通过jwt生成token返回给用户。
客户端
//注册逻辑
function finish(val: any) {
const { username, password, verify } = val;
if (password !== verify) {
Toast.show({
icon: 'fail',
content: "两次密码不一致"
})
} else {
setRequesting(true);
registerRequest(username, password).then(({ data }) => {
const { state } = data;
console.log(data);
if (state) {
Toast.show({
icon: "success",
content: "注册成功",
afterClose() {
navigate("/login")
}
})
} else {
Toast.show({
content: "用户名已存在"
})
}
setRequesting(false);
})
}
}
//登录逻辑
const onFunished = (val: any) => {
const { username, password } = val;
setRequesting(true);
loginRequest(username, password).then(({ data }) => {
const { state, token } = data;
if (state) {
Toast.show({
content: "登录成功",
afterClose() {
navigate("/home");
}
});
dispatch(changeUserInfo({
username,
token
}))
//自己封装了个本地储存类
localCache.set("userInfo", {
username,
token
});
} else {
Toast.show({
content: "账号或密码错误",
})
}
setRequesting(false);
})
}
服务器端
async function userRegister(username, password) {
const _sql = `INSERT INTO user SET username="${username}" , password="${password}";`;
try {
await query(_sql);
return true;
} catch (err) {
return false;
}
}
Router.post("/register", async (req, res) => {
console.log("register");
const { username, password } = req.body;
const ans = await userRegister(username, password);
res.send({
state: ans,
});
});
}
async function userLogin(username, password) {
const _sql = `SELECT * FROM user WHERE username="${username}" AND password="${password}";`;
const ans = await query(_sql);
return ans;
}
Router.post("/login", async (req, res) => {
const { username, password } = req.body;
const ans = await userLogin(username, password);
if (ans.length) {
const token = generate(username);
res.send({
state: true,
token,
});
} else {
res.send({
state: false,
});
}
});
//对于服务的前提条件是登录的请求的中间件
module.exports = (req, res, next) => {
const token = req.header("Authorization");
console.log("token", token);
const username = verify(token);
console.log(username);
if (!username) {
res.send({
state: -1,
message: "请登录",
});
} else {
req.global = {
username: username,
};
next();
}
};
头像上传与更换
- 头像更换中,我将头像存入静态数据文件夹中,图片名为用户名。用户上传时更换文件夹图片即可。
- 服务端解析图片使用 connect-multiparty
客户端
//文件上传
export function changeAvatorRequest(file: File) {
const Form = new FormData();
Form.append("avator", file);
return service.post("/user/changeAvator", Form, {
headers: {
"content-type": "application/x-www-form-urlencoded",
},
});
}
<input
style={{ display: 'none' }}
id="ipt"//负责与label联动
type='file'
accept='image/*'
onChange={e => {
e.preventDefault();
const file = e.target.files![0];
changeAvatorRequest(file).then(() => {
setUpdataAvator(Date.now()) //责告诉客户端要更新头像照片了
})
}}
/>
服务端
//app文件
const app = express();
app.use(bodyParser.urlencoded({ extended: true })); // 解析form表单提交的数据application/x-www-form-urlencoded
app.use(multipart()); //解析form-data提交数据
module.exports.copyFile = function (from, to) {
const file = fs.readFileSync(from);
fs.writeFileSync(path.resolve(__dirname, to), file);
};
Router.post("/changeAvator", auth, async (req, res) => {
const files = req.files;
const { username } = req.global;
utils.copyFile(
files.avator.path,
path.resolve(__dirname, `../public/avator/${username}.jpeg`)
);
console.log("changeAvator");
console.log(files);
res.send({
state: 1,
});
});
socket.io 实现连接和身份认证
- 这里使用socket.io第三方库。并且新开了个服务器端口。
- 客户端只存储提醒信息的消息
- 我最开始使用的是express-ws。使用起来麻烦,并没有像http请求这样每次必有一个来回。所以我在发送消息方面仍然使用的是http发送消息
服务端
const express = require("express");
const app = express();
const http = require("http");
const server = http.createServer(app);
const { Server } = require("socket.io");
const { verify } = require("../utils/jwt");
const { receiverList } = require("../mysql/sqlService");
const io = new Server(server, {
allowEIO3: true,
cors: true,
});
const username_ws_hash = new Map();//用于存储在线用户的名字和socket, 用户名 - socket
io.on("connection", async (socket) => {//这个是每次socket.io连接成功后触发的函数
socket.global = {};
socket.on("login", async (data, callback) => {
const { token } = data;
const username = verify(token);
console.log(username, socket.id);
if (!username) {
callback({
state: 0,
});
} else {
username_ws_hash.set(username, socket);
socket.global.username = username;
const list = await receiverList(username);
callback({
state: 1,
list,
});
}
});
socket.on("disconnection", (data) => {
username_ws_hash.delete(socket.global.username);//下线时删除用户的在线列表
});
socket.on("connect_error", (e) => {//报错监听
console.log("connect_error", e);
});
});
客户端
import store from "@/store";
import io, { Socket } from "socket.io-client";
async function socketStart() {
const socket = io("http://127.0.0.2:7070");
let isLogin = false;
socket.on("connect", () => {
console.log(socket.id, "监听客户端连接成功-connect");
});
const userInfo = store.getState().userInfo;
const token = userInfo.token;
return new Promise<{ socket: Socket; userMessage: any }>(
(resolve, reject) => {
socket.emit("login", { token }, (data: any) => {
const { state, list } = data;
if (state == 1) {
console.log("登录成功");
isLogin = true;
} else if (state == 0) {
console.log("登录失败");
isLogin = false;
}
resolve({
socket,
userMessage: list,
});
});
}
);
}
export default socketStart;