使用 React、Typescript、Node 和 Socket.io 制作的即时通讯应用程序 🦜 目录 项目介绍 👋 功能 🌟 技术栈 ⚛️ 线框和设计 🎨 数据建模和 API 路由 💾 项目组织 🗂️ Sprint 01:设置和前端 🖥 Sprint 02:后端 📊 Sprint 03:修复和部署 ☁️ 结论✅

2025-05-25

使用 React、Typescript、Node 和 Socket.io 🦜 制作的即时通讯应用程序

目录

项目介绍👋

特点🌟

技术栈⚛️

线框和设计🎨

数据建模和 API 路由💾

项目组织

Sprint 01:设置和前端🖥

Sprint 02:后端

Sprint 03:修复并部署☁️

结论

大家好,今天我回到 Dev.to 分享我的另一个项目!

我们都知道,定期练习对于提升开发技能至关重要。随着我越来越自信,我尝试构建更复杂、更强大的应用程序。

最后一个项目做起来很有趣。我花了将近一个月的时间来部署它(我主要在课余时间工作)。

享受阅读😇

目录

  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上。

以下是我通常用来增强编程体验的工具列表:
➡️ 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;


Enter fullscreen mode Exit fullscreen mode


// 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;
}



Enter fullscreen mode Exit fullscreen mode

没什么特别的,它是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;


Enter fullscreen mode Exit fullscreen mode

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();
  }, []);


Enter fullscreen mode Exit fullscreen mode

我发现了express-validator,它在服务器端提供输入验证方面非常有帮助。毫无疑问,我会再次使用这个库。

Sprint 03:修复并部署☁️

好了,应用看起来不错,功能也运行良好。是时候完成这个作品集项目,开始一个新的了。

我不是云解决方案和复杂 CI/CD 方法的专家,因此我对免费托管服务感到满意。

Heroku有一个免费的解决方案,非常适合后端。我的 Node 服务器上传 5 分钟后,它就独立运行了。太棒了🌈

我在客户端遇到了一些安全问题。通常情况下,当我通过GitHub将React应用发送到Netlify时,一切都正常,但这次却不行。

我很多朋友因为某些“安全原因”无法访问我提供的网址,我不得不购买域名来解决这个问题。这没什么大不了的,一年15欧元的价格似乎也不算贵。

最后,用户上传的图像通过其公共 API存储在我的Cloudinary帐户中。

结论

再次,我非常享受从事这个项目并学到了很多东西。

我很高兴与您分享这个过程,我迫不及待地想听到您的建议和反馈。

这个项目只是一个作品集项目,并没有“生产”的意图。不过,代码已经在 GitHub 上开源了,你可以随意使用它。

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
PREV
如何使用 Express、Node 和 Gmail 构建 SMTP 邮件服务器
NEXT
我第一次如何构建一个真实世界的项目🌈目录项目介绍👋技术栈⚛️线框和设计🎨数据建模💾项目组织🗂️Sprint 01:前端📲Sprint 02:后端📊Sprint 03:实现功能⭐Sprint 04:修复和部署☁️结论✅