使用 React、Typescript、Node 和 Socket.io 🦜 制作的即时通讯应用程序
目录
项目介绍👋
特点🌟
技术栈⚛️
线框和设计🎨
数据建模和 API 路由💾
项目组织
Sprint 01:设置和前端🖥
Sprint 02:后端
Sprint 03:修复并部署☁️
结论
大家好,今天我回到 Dev.to 分享我的另一个项目!
我们都知道,定期练习对于提升开发技能至关重要。随着我越来越自信,我尝试构建更复杂、更强大的应用程序。
最后一个项目做起来很有趣。我花了将近一个月的时间来部署它(我主要在课余时间工作)。
享受阅读😇
目录
项目介绍👋
我很高兴向大家介绍GroupChat 🥳
本次挑战的线框图由devchallenges提供,其中包含许多精彩的项目构想,可供构建和实践。如果你缺少灵感,不妨看看!
好的,让我们来谈谈GroupChat,它是一个即时通讯应用程序,允许用户创建频道并与对特定主题感兴趣的人聊天。
听起来很简单?好吧,我不会说它很“复杂”,但尝试新事物总是充满挑战的。
这是我第一次使用socket.io,也是我第一个用TypeScript构建的中型项目。
特点🌟
✅ 自定义身份验证(电子邮件 - 密码)
✅ 以访客身份登录(受限访问)
✅ 随机头像 / 个人资料图片上传
✅ 授权(json web 令牌)
✅ 端到端输入验证
✅ 创建和加入频道
✅ 即时消息
✅ 错误报告
✅ 移动友好
技术栈⚛️
我再次选择了我最好的朋友MERN堆栈,它包括:
➡️ MongoDB
➡️ Express
➡️ React
➡️ Node
除了上述技术之外,我还使用TypeScript来提高代码的稳健性,并使用Redux来管理应用程序状态。
我还应该提到socket.io,它支持浏览器和服务器之间的实时、双向和基于事件的通信。
对于部署,一种简单有效的方法是将前端托管在Netlify上,将后端托管在Heroku上。
以下是我通常用来增强编程体验的工具列表:
➡️ OS:MacOS
➡️ 终端:iterm2
➡️ IDE:VSCode
➡️ 版本控制:Git
➡️ 包管理器:NPM
➡️ 项目组织:Notion
线框和设计🎨
说实话,我不太喜欢设计产品的UI。所以,我决定先用现有的线框图,专注于代码。
正如我之前所说,我从devchallenges中获得了灵感。快速概览:
数据建模和 API 路由💾
数据库设计和 API 路由是重要的步骤。在开始编码之前,请确保你有一个行动计划,否则将会是一场灾难🧨
这是使用Lucidchart制作的简单数据模型:
确实简单,但是对于这个项目来说已经足够了。
正如您所猜测的,我们正在使用涉及 HTTP 请求的 Node/Express 构建 REST API。
让我们想象一下我们的路线:
注意:使用Apiary制作的 API 文档
项目组织
我喜欢一切井井有条、干净整洁的氛围。以下是我决定使用的文件夹结构:
简单、干净、一致💫
为了跟踪我的进度,我在Trello上创建了一个任务板
在进入下一步之前,我将简要介绍一下Git工作流程。
由于我是唯一一个负责这个项目的人,因此GitHub flow运行良好。
每次添加代码都有一个专用分支,并且每次新的 PR 都会对代码进行审查(仅由我自己审查...)。
注意:创建了大约 180 个提交和 40 个分支
Sprint 01:设置和前端🖥
开始编码总是如此令人兴奋,这是我最喜欢的部分。
我想说第一周是最轻松的。我从设置前端和后端开始,这意味着安装依赖项、环境变量、CSS 重置、创建数据库......
设置完成后,我构建了应该出现在屏幕上的每一个组件,并确保它们适合移动设备(弹性、媒体查询......)。
说到组件和 UI,这里有一个简单的例子:
// TopBar/index.tsx
import React from 'react';
import { IconButton } from '@material-ui/core';
import MenuIcon from '@material-ui/icons/Menu';
// Local Imports
import styles from './styles.module.scss';
type Props = {
title?: String;
menuClick: () => void;
};
const TopBar: React.FC<Props> = props => {
return (
<div className={styles.container}>
<div className={styles.wrapper}>
<IconButton className={styles.iconButton} onClick={props.menuClick}>
<MenuIcon className={styles.menu} fontSize="large" />
</IconButton>
<h2 className={styles.title}>{props.title}</h2>
</div>
</div>
);
};
export default TopBar;
// TopBar/styles.module.scss
.container {
width: 100%;
height: 60px;
box-shadow: 0px 4px 4px rgba($color: #000, $alpha: 0.2);
display: flex;
align-items: center;
justify-content: center;
}
.wrapper {
width: 95%;
display: flex;
align-items: center;
}
.title {
font-size: 18px;
}
.iconButton {
display: none !important;
@media (max-width: 767px) {
display: inline-block !important;
}
}
.menu {
color: #e0e0e0;
}
没什么特别的,它是TypeScript(我还有很多东西要学)和SCSS模块的基本实现。
我非常喜欢SCSS ,并为感兴趣的人写了一篇介绍:
您还可以注意到,一些组件(图标、输入等)是从我最喜欢的 UI 库中导入的:Material UI。
说到TypeScript,刚开始的几天确实很痛苦很累,但是最后发现在开发过程中发现 bug 变得非常容易。
如果你发现使用TypeScript有困难,你可能需要看看这篇文章:
我对Redux不太熟悉,所以我不得不花一些时间阅读文档才能理解。
我使用的另一个很酷的工具是Formik,它以智能而简单的方式管理表单验证。
// Login/index.tsx
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';
import { TextField, FormControlLabel, Checkbox, Snackbar, CircularProgress } from '@material-ui/core';
import MuiAlert from '@material-ui/lab/Alert';
import { useDispatch } from 'react-redux';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import { useHistory } from 'react-router-dom';
// Local Imports
import logo from '../../../assets/gc-logo-symbol-nobg.png';
import CustomButton from '../../Shared/CustomButton/index';
import styles from './styles.module.scss';
type Props = {};
type SnackData = {
open: boolean;
message: string | null;
};
const Login: React.FC<Props> = props => {
const dispatch = useDispatch();
const history = useHistory();
const [isLoading, setIsLoading] = useState(false);
const [checked, setChecked] = useState(false);
const [snack, setSnack] = useState<SnackData>({ open: false, message: null });
// Async Requests
const loginSubmit = async (checked: boolean, email: string, password: string) => {
setIsLoading(true);
let response;
try {
response = await axios.post(`${process.env.REACT_APP_SERVER_URL}/users/login`, {
checked,
email: email.toLowerCase(),
password: password.toLowerCase()
});
} catch (error) {
console.log('[ERROR][AUTH][LOGIN]: ', error);
setIsLoading(false);
return;
}
if (!response.data.access) {
setSnack({ open: true, message: response.data.message });
setIsLoading(false);
return;
}
if (checked) {
localStorage.setItem('userData', JSON.stringify({ id: response.data.user.id, token: response.data.user.token }));
}
dispatch({ type: 'LOGIN', payload: { ...response.data.user } });
history.push('');
setIsLoading(false);
};
const formik = useFormik({
initialValues: {
email: '',
password: ''
},
validationSchema: Yup.object({
email: Yup.string().email('Invalid email address').required('Required'),
password: Yup.string()
.min(6, 'Must be 6 characters at least')
.required('Required')
.max(20, 'Can not exceed 20 characters')
}),
onSubmit: values => loginSubmit(checked, values.email, values.password)
});
return (
<div className={styles.container}>
<Link to="/">
<img className={styles.logo} alt="logo" src={logo} />
</Link>
<form className={styles.form}>
<TextField
className={styles.input}
id="email"
label="Email"
variant="outlined"
type="text"
helperText={formik.touched.email && formik.errors.email}
error={formik.touched.email && !!formik.errors.email}
{...formik.getFieldProps('email')}
/>
<TextField
className={styles.input}
id="password"
label="Password"
variant="outlined"
type="password"
{...formik.getFieldProps('password')}
helperText={formik.touched.password && formik.errors.password}
error={formik.touched.password && !!formik.errors.password}
/>
<FormControlLabel
className={styles.check}
control={
<Checkbox checked={checked} onChange={() => setChecked(prev => !prev)} name="checked" color="primary" />
}
label="Remember me"
/>
<CustomButton type="submit" onClick={formik.handleSubmit} isPurple title="Login" small={false} />
</form>
<Link to="/signup">
<p className={styles.guest}>Don't have an account? Sign Up</p>
</Link>
{isLoading && <CircularProgress />}
<Snackbar open={snack.open} onClose={() => setSnack({ open: false, message: null })} autoHideDuration={5000}>
<MuiAlert variant="filled" onClose={() => setSnack({ open: false, message: null })} severity="error">
{snack.message}
</MuiAlert>
</Snackbar>
</div>
);
};
export default Login;
Sprint 02:后端
该服务器非常简单,它是 Node/Express 服务器的经典表示。
我创建了猫鼬模型及其关联。
然后,我注册了路由并连接了相应的控制器。在我的控制器中,你可以找到经典的 CRUD 操作和一些自定义函数。
感谢JWT,让我们能够处理安全性问题,这对我来说很重要。
现在介绍这个应用程序最酷的功能,双向通信,或者我应该说是socket.io?
以下是一个例子:
// app.js - Server side
// Establish a connection
io.on('connection', socket => {
// New user
socket.on('new user', uid => {
userList.push(new User(uid, socket.id));
});
// Join group
socket.on('join group', (uid, gid) => {
for (let i = 0; i < userList.length; i++) {
if (socket.id === userList[i].sid) userList[i].gid = gid;
}
});
// New group
socket.on('create group', (uid, title) => {
io.emit('fetch group');
});
// New message
socket.on('message', (uid, gid) => {
for (const user of userList) {
if (gid === user.gid) io.to(user.sid).emit('fetch messages', gid);
}
});
// Close connection
socket.on('disconnect', () => {
for (let i = 0; i < userList.length; i++) {
if (socket.id === userList[i].sid) userList.splice(i, 1);
}
});
});
// AppView/index.tsx - Client side
useEffect(() => {
const socket = socketIOClient(process.env.REACT_APP_SOCKET_URL!, { transports: ['websocket'] });
socket.emit('new user', userData.id);
socket.on('fetch messages', (id: string) => fetchMessages(id));
socket.on('fetch group', fetchGroups);
setSocket(socket);
fetchGroups();
}, []);
我发现了express-validator,它在服务器端提供输入验证方面非常有帮助。毫无疑问,我会再次使用这个库。
Sprint 03:修复并部署☁️
好了,应用看起来不错,功能也运行良好。是时候完成这个作品集项目,开始一个新的了。
我不是云解决方案和复杂 CI/CD 方法的专家,因此我对免费托管服务感到满意。
Heroku有一个免费的解决方案,非常适合后端。我的 Node 服务器上传 5 分钟后,它就独立运行了。太棒了🌈
我在客户端遇到了一些安全问题。通常情况下,当我通过GitHub将React应用发送到Netlify时,一切都正常,但这次却不行。
我很多朋友因为某些“安全原因”无法访问我提供的网址,我不得不购买域名来解决这个问题。这没什么大不了的,一年15欧元的价格似乎也不算贵。
最后,用户上传的图像通过其公共 API存储在我的Cloudinary帐户中。
结论
再次,我非常享受从事这个项目并学到了很多东西。
我很高兴与您分享这个过程,我迫不及待地想听到您的建议和反馈。
这个项目只是一个作品集项目,并没有“生产”的意图。不过,代码已经在 GitHub 上开源了,你可以随意使用它。
KillianFrappartDev / GroupChat
使用 React、Redux、TypeScript、Node、MongoDB 和 Socket.io 制作的即时通讯 Web 应用项目
我知道在代码质量、安全性、优化等方面还有很多需要改进的地方……无论如何,我设法完成了它,结果看起来很酷,我希望你也喜欢它。
实时版本:GroupChat
永远不要停止挑战自己🚀
文章来源:https://dev.to/killianfrappartdev/instant-messaging-app-made-with-react-typescript-node-socket-io-27pc