如何使用 MongoDB、Express、React 和 Node.js(MERN 堆栈)创建简单美观的聊天室 先决条件 结构 设置客户端 设置服务器 设置我们的 Heroku 托管 设置 GitHub 并链接到 Heroku 完成客户端 最后步骤

2025-06-04

如何使用 MongoDB、Express、React 和 Node.js(MERN 堆栈)创建简单美观的聊天

先决条件

结构

设置客户端

设置服务器

设置我们的 Heroku 托管

设置 GitHub 并链接到 Heroku

完成客户端

最后步骤

最近,我参与了一个有趣的项目,名为SpeedBoard,它是一个用于 Agile 和 Scrum 回顾的实时看板。它是我们在 Scrum Sprint 评审会后使用的工具,方便我们分享关于上一个 Sprint 的反馈。

由于这是一次非常丰富的体验,我想做一个快速教程,介绍如何使用相同的技术栈建立一个简单的聊天室,该技术栈包括:MongoDBExpressReactNode.js,也称为MERN 技术栈。我还使用了Socket.IO作为实时引擎,以及Material-UI (一个基于Material Design的 React UI 框架)

如果您不想等到本教程结束,您可以查看最终结果的预览,如果您想分叉并开始改进它,还可以检查 Github 存储库;)

先决条件

在本教程中,我们将使用Heroku托管我们的实时项目,并使用Github托管我们的代码并将其部署到 Heroku,因此请确保您已经拥有他们的帐户,他们都提供免费注册。

结构

在开始之前,我们先快速浏览一下项目的结构。在根文件夹中,我们有两个子文件夹:一个client包含 React 应用,另一个server包含 Node.js 服务器:

speedchatapp/
├── client/
├── server/
Enter fullscreen mode Exit fullscreen mode

让我们打开终端并创建项目文件夹:

mkdir speedchatapp
cd speedchatapp/
Enter fullscreen mode Exit fullscreen mode

设置客户端

在客户端,我们将使用Create React App (CRA),它提供了一种非常简单的方法来开始构建任何 React SPA。

CRA 提供了一个非常简单的命令来安装该应用程序,但首先,如果您过去npx使用过,让我们确保使用的是最新版本:create-react-app

npm uninstall -g create-react-app
Enter fullscreen mode Exit fullscreen mode

现在,让我们client使用这个简单的命令在我们的文件夹中创建我们的应用程序:

npx create-react-app client
Enter fullscreen mode Exit fullscreen mode

这可能需要几分钟来安装所有依赖项,完成后,尝试:

cd client/
npm start
Enter fullscreen mode Exit fullscreen mode

您现在应该能够通过http://localhost:3000/访问您的应用程序

创建 React 应用

这真是又快又简单 :) 但距离最终结果还差得远!等项目服务器端准备好后,我们再回来看我们的 React 应用。

设置服务器

现在我们已经准备好了框架client,让我们看看后端。

首先,让我们server在项目根目录创建文件夹并初始化package.json文件:

mkdir server
cd server/
npm init
Enter fullscreen mode Exit fullscreen mode

实用程序将引导您完成文件的配置,但您可以输入Enter本教程的所有选项。

现在,我们将使用以下命令安装服务器所需的所有依赖项(Express,Mongoose 和 Socket.IO):

npm install express mongoose socket.io --save
Enter fullscreen mode Exit fullscreen mode

然后,将.gitignore文件从client文件夹复制到server文件夹,以防止某些文件和文件夹被推送到我们的GitHub存储库(例如/node_modules文件夹):

cp ../client/.gitignore ./
Enter fullscreen mode Exit fullscreen mode

我们将创建服务器运行所需的两个文件。第一个文件(Message.js)是我们将保存在数据库中的文档的架构。我们需要三个信息:在聊天中发送消息的用户的姓名、消息内容以及用于记录发送消息时间的时间戳。

server/Message.js

const mongoose = require('mongoose');

const messageSchema = new mongoose.Schema({
  content: String,
  name: String,
}, {
  timestamps: true,
});

module.exports = mongoose.model('Message', messageSchema);
Enter fullscreen mode Exit fullscreen mode

第二个(index.js)是我们的主文件,我不会讲太多细节,因为这会使本教程有点太长,但请随时在评论中提出任何问题,我很乐意回答它们或在必要时直接在代码中改进评论。

server/index.js

const express = require('express');
const app = express();
const http = require('http').Server(app);
const path = require('path');
const io = require('socket.io')(http);

const uri = process.env.MONGODB_URI;
const port = process.env.PORT || 5000;

const Message = require('./Message');
const mongoose = require('mongoose');

mongoose.connect(uri, {
  useUnifiedTopology: true,
  useNewUrlParser: true,
});

app.use(express.static(path.join(__dirname, '..', 'client', 'build')));

io.on('connection', (socket) => {

  // Get the last 10 messages from the database.
  Message.find().sort({createdAt: -1}).limit(10).exec((err, messages) => {
    if (err) return console.error(err);

    // Send the last messages to the user.
    socket.emit('init', messages);
  });

  // Listen to connected users for a new message.
  socket.on('message', (msg) => {
    // Create a message with the content and the name of the user.
    const message = new Message({
      content: msg.content,
      name: msg.name,
    });

    // Save the message to the database.
    message.save((err) => {
      if (err) return console.error(err);
    });

    // Notify all other users about a new message.
    socket.broadcast.emit('push', msg);
  });
});

http.listen(port, () => {
  console.log('listening on *:' + port);
});
Enter fullscreen mode Exit fullscreen mode

您的项目结构现在应如下所示:

speedchatapp/
├── client/
│   └── (Several files and folders)
└── server/
    ├── node_modules/
    ├── .gitignore
    ├── index.js
    ├── Message.js
    ├── package-lock.json (auto-generated)
    └── package.json
Enter fullscreen mode Exit fullscreen mode

在回到我们的 React 应用程序完成我们的项目之前,让我们设置我们的 Heroku 托管并将其链接到我们的 Github 存储库,以确保部署正常工作。

设置我们的 Heroku 托管

让我们下载并安装Heroku CLI来从我们的终端设置一切。

下载并安装后,让我们返回终端并登录我们的 Heroku 帐户:

heroku login
Enter fullscreen mode Exit fullscreen mode

它将在您的浏览器中打开一个新选项卡,一旦您登录,您就可以关闭浏览器选项卡并返回到您的终端。

现在让我们创建托管我们项目的新应用程序:

heroku create
Enter fullscreen mode Exit fullscreen mode

它将自动生成一个带有 URL 的标识符,您可以通过该 URL 访问您的应用程序,它看起来应该像这样:

https://sleepy-meadow-81798.herokuapp.com/

Heroku 应用

如果您想要一个更容易记住的名称,您可以重命名您的应用程序,然后您可以在本教程的其余部分使用它:

在 Heroku UI 中重命名应用程序

好了,现在我们需要 MongoDB 数据库来存储用户的聊天消息。让我们将 mongolab 插件添加到我们的应用中:

heroku addons:create mongolab --app speedchatapp
Enter fullscreen mode Exit fullscreen mode

speedchatapp在上一个命令中使用了它是因为我重命名了我的应用程序,但如果您没有重命名它,则应该使用创建它时提供的那个,例如sleepy-meadow-81798

创建后,它将以绿色显示变量的名称,即MONGODB_URI。现在让我们获取新创建的数据库的配置 URI:

heroku config:get MONGODB_URI
Enter fullscreen mode Exit fullscreen mode

您应该看到类似这样的内容:

mongodb://heroku_123abc:abc123@ds141188.mlab.com:41188/heroku_123abc
Enter fullscreen mode Exit fullscreen mode

复制此 URI,并在项目根目录下创建一个名为 的文件,.env其内容如下[VARIABLE_IN_GREEN]=[URI]。它应该如下所示:

MONGODB_URI=mongodb://heroku_123abc:abc123@ds141188.mlab.com:41188/heroku_123abc
Enter fullscreen mode Exit fullscreen mode

让我们再复制一次并在其末尾.gitignore添加文件,以避免将数据库的凭据推送到 GitHub:.env

cp server/.gitignore ./
echo '.env' >> .gitignore
Enter fullscreen mode Exit fullscreen mode

在部署应用期间,我们需要告诉 Heroku 如何启动服务器。我们可以创建一个Procfile 文件,并将其放在项目的根目录下。接下来,我们创建它并添加启动服务器的命令行:

echo 'web: node server/index.js' > Procfile
Enter fullscreen mode Exit fullscreen mode

现在让package.json我们在项目的根目录下初始化另一个。和以前一样,不用担心所有选项,现在只需Enter在所有提示符下输入:

npm init
Enter fullscreen mode Exit fullscreen mode

我们要做的最后一件事是安装名为Concurrently的 npm 包,它将允许我们在开发模式下在单个命令行中运行服务器和客户端:

npm install --save-dev concurrently
Enter fullscreen mode Exit fullscreen mode

最后,在我们新创建的package.json项目的根目录中,我们将在scripts部分中添加两行:

"scripts": {
    "dev": "concurrently --kill-others \"heroku local\" \"npm run start --prefix ./client\"",
    "postinstall": "npm install --prefix ./server && npm install --prefix ./client && npm run build --prefix ./client",
}
Enter fullscreen mode Exit fullscreen mode

正如您所猜测的,该postinstall命令将在 Heroku 完成运行npm install我们文件夹根目录下的命令后执行。它告诉 Heroku 也在我们的and文件夹npm install中运行该命令,并且还将构建我们的 React 应用以供生产环境使用clientserver

现在,是时候测试它了,转到项目的根目录并输入:

npm run dev
Enter fullscreen mode Exit fullscreen mode

这将以开发模式启动服务器和我们的 React 应用程序,并且它将在您的浏览器中打开一个窗口,其中包含我们 React 应用程序的上一个登录页面。

在你的终端中,你应该看到如下内容:

> concurrently --kill-others "heroku local" "npm run start --prefix ./client"

[1] 
[1] > react-scripts start
[1] 
[0] [OKAY] Loaded ENV .env File as KEY=VALUE Format
[0] 12:16:15 PM web.1 |  listening on *:5000
[1] Starting the development server...
[1] 
[1] Compiled successfully!
[1] 
[1] You can now view client in the browser.
[1] 
[1]   Local:            http://localhost:3000/
[1]   On Your Network:  http://192.168.0.10:3000/
[1] 
[1] Note that the development build is not optimized.
[1] To create a production build, use npm run build.
Enter fullscreen mode Exit fullscreen mode

注意:我们在开发和实时模式下使用相同的数据库,如果您想使用不同的数据库,您可以像我们之前看到的那样在 Heroku 中创建另一个数据库,并.env使用新数据库的凭据更新您的文件,以确保它不会干扰生产中的数据库。

设置 GitHub 并链接到 Heroku

现在,我们将在 GitHub 上创建一个新的存储库,并将其连接到 Heroku,这样每次我们在主分支上合并 Pull 请求时,它都会自动将其部署到 Heroku 上。

让我们在 GitHub 上创建我们的仓库。前往https://github.com/new:

在 Github 上创建新的存储库

写下我们下一步要使用的仓库 URL。回到我们的终端,在项目的根文件夹中:

// Initialize the root folder as a Git repository
git init 

// Add all the files for the initial commit
git add .

// Commit staged files
git commit -m "Initial commit"

// Set the GitHub remote repository
git remote add origin <repository url>

// Push the local changes to GitHub
git push origin master
Enter fullscreen mode Exit fullscreen mode

现在我们的代码在 GitHub 上,让我们将这个存储库链接到我们的 Heroku 应用程序。

Heroku UI中,选择您的应用并单击Deploy选项卡。在 中Deployment method,单击Github,输入您的存储库名称,然后单击Connect

连接到 GitHub

另外,确保master分支上的“启用自动部署”已激活:

启用自动部署

现在看起来应该是这样的:

已启用自动部署

现在让我们触发第一次手动部署来检查一切是否正常。点击Deploy Branch,等待直到看到Your app was successfully deployed

最后,单击Open App页面右上角的按钮后,您应该会在 Heroku 托管上看到 React 应用程序。

从现在开始,将任何更新推送到你的 GitHub 存储库后,你应该会在 Heroku UI 中看到自动触发的部署:

部署已触发

完成客户端

现在我们的项目架构已经准备好了,让我们完成我们的clientReact 应用程序。

我们首先需要在client文件夹中安装前端依赖项:客户端的 Socket.IO、Material-UI 核心和图标:

cd client/
npm install socket.io-client @material-ui/core @material-ui/icons --save
Enter fullscreen mode Exit fullscreen mode

现在,在文件末尾client/package.json添加以下字段:proxy

"proxy": "http://localhost:5000"
Enter fullscreen mode Exit fullscreen mode

它会告诉开发服务器将任何未知请求代理到你的开发服务器。更多信息请查看官方文档

接下来,我们将创建一个config.js文件来告诉我们的应用程序在本地机器或实时托管时切换端点:

客户端/src/config.js

import pkg from '../package.json';

export default {
  development: {
    endpoint: pkg.proxy
  },
  production: {
    endpoint: window.location.hostname
  }
}
Enter fullscreen mode Exit fullscreen mode

好的,现在让我们从根文件夹启动本地开发环境:

npm run dev
Enter fullscreen mode Exit fullscreen mode

最后步骤

对于最后一步,请手动创建或更新下面的每个文件,或者直接转到 GitHub 存储库来检出项目。

代替client/src/App.css

body {
  background: #f5f5f5;
  padding: 16px;
}

#chat {
  max-height: calc(100vh - 128px);
  overflow: scroll;
  padding: 16px;
}

.name {
  color: rgba(0, 0, 0, 0.54);
}

.content {
  margin-bottom: 8px;
}
Enter fullscreen mode Exit fullscreen mode

代替client/src/App.js

import React from 'react';
import config from './config';
import io from 'socket.io-client';

import Paper from '@material-ui/core/Paper';
import Typography from '@material-ui/core/Typography';

import BottomBar from './BottomBar';
import './App.css';

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      chat: [],
      content: '',
      name: '',
    };
  }

  componentDidMount() {
    this.socket = io(config[process.env.NODE_ENV].endpoint);

    // Load the last 10 messages in the window.
    this.socket.on('init', (msg) => {
      let msgReversed = msg.reverse();
      this.setState((state) => ({
        chat: [...state.chat, ...msgReversed],
      }), this.scrollToBottom);
    });

    // Update the chat if a new message is broadcasted.
    this.socket.on('push', (msg) => {
      this.setState((state) => ({
        chat: [...state.chat, msg],
      }), this.scrollToBottom);
    });
  }

  // Save the message the user is typing in the input field.
  handleContent(event) {
    this.setState({
      content: event.target.value,
    });
  }

  //
  handleName(event) {
    this.setState({
      name: event.target.value,
    });
  }

  handleSubmit(event) {
    // Prevent the form to reload the current page.
    event.preventDefault();

    // Send the new message to the server.
    this.socket.emit('message', {
      name: this.state.name,
      content: this.state.content,
    });

    this.setState((state) => {
      // Update the chat with the user's message and remove the current message.
      return {
        chat: [...state.chat, {
          name: state.name,
          content: state.content,
        }],
        content: '',
      };
    }, this.scrollToBottom);
  }

  // Always make sure the window is scrolled down to the last message.
  scrollToBottom() {
    const chat = document.getElementById('chat');
    chat.scrollTop = chat.scrollHeight;
  }

  render() {
    return (
      <div className="App">
        <Paper id="chat" elevation={3}>
          {this.state.chat.map((el, index) => {
            return (
              <div key={index}>
                <Typography variant="caption" className="name">
                  {el.name}
                </Typography>
                <Typography variant="body1" className="content">
                  {el.content}
                </Typography>
              </div>
            );
          })}
        </Paper>
        <BottomBar
          content={this.state.content}
          handleContent={this.handleContent.bind(this)}
          handleName={this.handleName.bind(this)}
          handleSubmit={this.handleSubmit.bind(this)}
          name={this.state.name}
        />
      </div>
    );
  }
};

export default App;
Enter fullscreen mode Exit fullscreen mode

创造client/src/BottomBar.js

import React from 'react';

import { fade, makeStyles } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import InputBase from '@material-ui/core/InputBase';
import Toolbar from '@material-ui/core/Toolbar';

import ChatIcon from '@material-ui/icons/Chat';
import FaceIcon from '@material-ui/icons/Face';

const useStyles = makeStyles(theme => ({
  appBar: {
    bottom: 0,
    top: 'auto',
  },
  inputContainer: {
    backgroundColor: fade(theme.palette.common.white, 0.15),
    '&:hover': {
      backgroundColor: fade(theme.palette.common.white, 0.25),
    },
    borderRadius: theme.shape.borderRadius,
    marginLeft: theme.spacing(1),
    position: 'relative',
    width: '100%',
  },
  icon: {
    width: theme.spacing(7),
    height: '100%',
    position: 'absolute',
    pointerEvents: 'none',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
  },
  inputRoot: {
    color: 'inherit',
  },
  inputInput: {
    padding: theme.spacing(1, 1, 1, 7),
    width: '100%',
  },
}));

export default function BottomBar(props) {
  const classes = useStyles();

  return (
    <AppBar position="fixed" className={classes.appBar}>
      <Toolbar>
        <div className={classes.inputContainer} style={{maxWidth: '200px'}}>
          <div className={classes.icon}>
            <FaceIcon />
          </div>
          <InputBase
            onChange={props.handleName}
            value={props.name}
            placeholder="Name"
            classes={{
              root: classes.inputRoot,
              input: classes.inputInput,
            }}
            inputProps={{ 'aria-label': 'name' }}
          />
        </div>
        <div className={classes.inputContainer}>
          <form onSubmit={props.handleSubmit}>
            <div className={classes.icon}>
              <ChatIcon />
            </div>
            <InputBase
              onChange={props.handleContent}
              value={props.content}
              placeholder="Type your message..."
              classes={{
                root: classes.inputRoot,
                input: classes.inputInput,
              }}
              inputProps={{ 'aria-label': 'content' }}
            />
          </form>
        </div>
      </Toolbar>
    </AppBar>
  );
}
Enter fullscreen mode Exit fullscreen mode

每次更新代码时,您都应该看到http://localhost:3000上的项目自动重新加载最新的更改。

最后,让我们将最新更新推送到 GitHub,以触发我们实时项目的新部署:

git add .
git commit -m "Final update"
git push origin master
Enter fullscreen mode Exit fullscreen mode

瞧,一切就绪!我们的聊天功能现已完成,随时可用:https://speedchatapp.herokuapp.com/

如果您有任何疑问,请随时在评论区提问,我很乐意解答并改进本教程。也欢迎您fork 该项目来改进它;)

文章来源:https://dev.to/armelpingault/how-to-create-a-simple-and-beautiful-chat-with-mongodb-express-react-and-node-js-mern-stack-29l6
PREV
How NOT to follow your passion 😬 1. Are you passionate about the THING? 🐴 Or are you passionate about the IDEA? 🦄 2. Why PASSION is not HAPPINESS 😞 - and what to do about it 😇 3. Your PASSION is your STRENGTH 🚀 How to turn your PASSION into HAPPINESS 💐 EXTRA value 🍓
NEXT
每个开发者都应该知道的 14 个改变游戏规则的网站🚀🔥