IM即时通讯软件源码是一个完整的消息传递套件,供企业构建跨 Web、Android、iOS 设备的可定制协作平台,以建立虚拟连接。Instant Messaging 解决方案提供多种通信媒介,如语音和视频通话、实时聊天、视频会议,以连接来自多个设备的远程团队。MirrorFly 提供功能丰富的 API 和 SDK,以在任何应用程序上集成通信平台。企业消息传递解决方案能够拥有大约 1M+ 的并发用户群。MirrorFly 兼容端到端加密、信号协议、AES-256 位和其他隐私合规性,如 HIPAA、GDPR、COPAA,以保护整个对话。
仓库源码:im.jstxym.top
本文目录:
1、源码项目介绍
2、特征
3、技术栈
4、线框与设计
5、数据建模和 API 路由
6、项目组织
7、Sprint 01:前端
8、Sprint 02:后端
9、Sprint 03:修复和部署
10、结论
一、项目介绍
我很高兴介绍GroupChat 🥳
这个挑战的线框图由devchallenges提供,它提供了许多很酷的项目想法来构建和实践。看看你是否缺少灵感!
好的,让我们谈谈GroupChat,它是一个即时通讯应用程序,允许用户创建频道并与对特定主题感兴趣的人聊天。
听起来很简单?好吧,我不会说它“复杂”,但尝试新事物总是具有挑战性。
这是我第一次使用socket.io ,也是我第一个使用TypeScript构建的中型项目。
二、特色
✅ 自定义身份验证(电子邮件 - 密码)
✅ 以访客身份登录(访问受限)
✅ 随机头像/个人资料图片上传
✅ 授权(json web 令牌)
✅ 端到端输入验证
✅ 创建和加入频道
✅ 即时消息
✅ 错误报告
✅ 移动友好
三、技术栈
再一次,我选择了我最好的朋友MERN堆栈,其中包括:
➡️ MongoDB
➡️ Express
➡️ React
➡️ Node
除了上述技术之外,我还使用TypeScript来提高我的代码的健壮性,并使用Redux来管理应用程序状态。
我还应该提到socket.io,它支持浏览器和服务器之间的实时、双向和基于事件的通信。
对于部署,一种简单有效的方法是将前端托管在Netlify上,后端托管在Heroku上。
以下是我通常用来增强编程体验的工具列表:
➡️ 操作系统:MacOS
➡️ 终端:iterm2
➡️ IDE:VSCode
➡️ 版本控制:Git
➡️ 包管理器:NPM
➡️ 项目组织:Notion
四、线框和设计
老实说,我对设计产品的 UI 并没有太多的乐趣。因此,我决定使用现有的线框并专注于代码。
正如我已经说过的,我的灵感来自devchallenges。快速概览:

五、数据建模和 API 路由
数据库设计和 API 路由是重要的步骤。在开始编码之前确保你有一个行动计划,否则这将是一场灾难🧨
这是一个使用Lucidchart制作的简单数据模型:

确实很简单,但是对于这个项目来说已经足够了。
正如您可能猜到的,我们正在使用 Node/Express 构建一个涉及 HTTP 请求的 REST API。
让我们想象一下我们的路线:

六、项目组织
我喜欢一切都干净整洁。这是我决定使用的文件夹结构:

简单、干净、一致
为了跟踪我的进度,我在Trello上为自己做了一个任务板

替代文字
在进行下一步之前,我将简要介绍一下Git工作流程。
因为我是这个项目的唯一工作人员,所以GitHub 流程工作得很好。
代码的每个添加都有一个专门的分支,并且每个新 PR 都会对代码进行审查(仅由我自己......)。

七、Sprint 01:设置和前端
开始编码总是那么令人兴奋,这是我最喜欢的过程。
我想说第一周是最简单的。我从设置前端和后端开始,这意味着安装依赖项、环境变量、CSS 重置、创建数据库......
设置完成后,我构建了应该出现在屏幕上的每一个组件,并确保它们对移动设备友好(flex、媒体查询……)。
说到组件和 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;
}
// 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有一个适用于后端的免费解决方案。我的节点服务器上传 5 分钟后,它独立运行。厉害。
我在客户端遇到了一些安全问题。通常,当我通过GitHub将我的React应用程序发送到Netlify时一切正常,但这次不行。
由于某些“安全原因”,我的许多朋友无法访问给定的 URL,我不得不购买一个域名来修复它。这里没什么大不了的,一年 15 欧元似乎并没有被高估。
最后,用户上传的图像通过他们的公共 API存储在我的Cloudinary帐户中。
十、结论
再一次,我非常享受在这个项目上的工作并学到了很多东西。
很高兴与您分享这个过程,我迫不及待地想听到您的提示和反馈。
这个项目无非是一个组合项目,背后并没有“生产”的意图。但是,代码是在 GitHub 上开源的,您可以随意使用它。