Skip to content

Latest commit

 

History

History
1775 lines (1345 loc) · 51 KB

File metadata and controls

1775 lines (1345 loc) · 51 KB

四、使用 MERN 构建消息应用

欢迎来到你的第三个 MERN 项目,在这里你使用 MERN 框架构建了一个很棒的消息应用。后端托管在 Heroku,前端站点托管在 Firebase。

Material-UI 提供了项目中的图标。使用 Pusher 是因为 MongoDB 不是像 Firebase 那样的实时数据库,聊天应用需要实时数据。这是一个带有谷歌认证的功能性聊天应用,不同的用户可以使用他们的谷歌账户登录聊天。图 4-1 显示了一个全功能托管和完成的应用。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig1_HTML.jpg

图 4-1

最终托管的应用

转到您的终端并创建一个messaging-app-mern文件夹。在里面,使用 create-react-app 创建一个名为 messaging-app-frontend 的新应用。

mkdir messaging-app-mern
cd messaging-app-mern
npx create-react-app messaging-app-frontend

Firebase 托管初始设置

由于前端站点是通过 Firebase 托管的,所以可以在 create-react-app 创建 React app 的同时创建基本设置。按照第 1 章的设置说明,我在 Firebase 控制台中创建了消息应用。

React 基本设置

让我们返回到 React 项目,将cd返回到messaging-app-frontend目录。用npm start启动 React 应用。

cd messaging-app-frontend
npm start

index.jsApp.jsApp.css中删除文件和基本设置就像在第 2 章中所做的一样。遵循这些指示。

4-2 显示了该应用在 localhost 上的外观。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig2_HTML.jpg

图 4-2

初始应用

创建侧栏组件

让我们创建一个侧边栏组件,显示登录用户的头像和其他图标,包括一个搜索栏。在创建侧边栏组件之前,在App.js文件中添加基本样式。在App.js,中创建一个包含所有代码的app__body类。更新的内容用粗体标记。

import './App.css';
function App() {
  return (
    <div className="app">
      <div className="app__body">
      </div>
    </div>
  );
}
export default App;

接下来,在App.css中设置容器的样式,得到一个带阴影的居中容器。

.app{
    display: grid;
    place-items: center;
    height: 100vh;
    background-color: #dadbd3;
}
.app__body{
    display: flex;
    background-color: #ededed;
    margin-top: -50px;
    height: 90vh;
    width: 90vw;
    box-shadow: -1px 4px 20px -6px rgba(0, 0, 0, 0.75);
}

转到本地主机。您应该会看到如图 4-3 所示的大阴影框。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig3_HTML.jpg

图 4-3

初始背景

接下来,在src文件夹中创建一个components文件夹。然后在components文件夹中创建两个文件——Sidebar.jsSidebar.css。将内容放在Sidebar.js文件中。以下是Sidebar.js文件的内容。

import React from 'react'
import './Sidebar.css'
const Sidebar = () => {
    return (
        <div className="sidebar">
            <div className="sidebar__header"></div>
            <div className="sidebar__search"></div>
            <div className="sidebar__chats"></div>
        </div>
    )
}
export default Sidebar

接下来安装 Material-UI ( https://material-ui.com )得到图标。根据 Material-UI 文档进行两次 npm 安装。通过messaging-app-frontend文件夹中的集成端子安装铁芯。

npm i @material-ui/core @material-ui/icons

接下来,让我们在Sidebar.js文件中使用这些图标。导入它们,然后在sidebar__header类中使用它们。更新的内容用粗体标记。

import React from 'react'
import './Sidebar.css'
import DonutLargeIcon from '@material-ui/icons/DonutLarge'
import ChatIcon from '@material-ui/icons/Chat'
import MoreVertIcon from '@material-ui/icons/MoreVert'
import { Avatar, IconButton } from '@material-ui/core'
const Sidebar = () => {
    return (
        <div className="sidebar">
            <div className="sidebar__header">
                <Avatar />
                <div className="sidebar__headerRight">
                    <IconButton>
                        <DonutLargeIcon />
                    </IconButton>
                    <IconButton>
                        <ChatIcon />
                    </IconButton>
                    <IconButton>
                        <MoreVertIcon />
                    </IconButton>
                </div>
            </div>
            <div className="sidebar__search"></div>
            <div className="sidebar__chats"></div>
        </div>
    )
}
export default Sidebar

让我们在Sidebar.css文件中添加侧边栏标题样式。flexbox 用于实现这一点。

.sidebar {
    display: flex;
    flex-direction: column;
    flex: 0.35;
}
.sidebar__header {
    display: flex;
    justify-content: space-between;
    padding: 20px;
    border-right: 1px solid lightgray;
}
.sidebar__headerRight {
    display: flex;
    align-items: center;
    justify-content: space-between;
    min-width: 10vw;
}
.sidebar__headerRight > .MuiSvgIcon-root{
    margin-right: 2vw;
    font-size: 24px !important;
}

接下来,让我们导入App.js中的侧边栏组件,让它显示在 localhost 上。更新的内容用粗体标记。

import './App.css';
import Sidebar from './components/Sidebar';
function App() {
  return (
    <div className="app">
      <div className="app__body">
            <Sidebar />
      </div>
    </div>
  );
}
export default App;

4-4 显示了本地主机上对齐的图标。

接下来,在Sidebar.js中创建搜索栏。从 Material-UI 导入SearchOutlined并与sidebar__searchContainer类一起使用。在旁边放一个输入框。

import { SearchOutlined } from '@material-ui/icons'
const Sidebar = () => {
    return (
        <div className="sidebar">
            <div className="sidebar__header">
                <Avatar src="https://pbs.twimg.com/profile_img/1020939891457241088/fcbu814K_400x400.jpg"/>
                <div className="sidebar__headerRight">
                     ...
                </div>
            </div>
            <div className="sidebar__search">
                <div className="sidebar__searchContainer">
                    <SearchOutlined />
                    <input placeholder="Search or start new chat" type="text" />
                </div>
           </div>
            <div className="sidebar__chats"></div>
        </div>
    )
}
export default Sidebar

img/512020_1_En_4_Chapter/512020_1_En_4_Fig4_HTML.jpg

图 4-4

图标对齐

我用我的推特账户上的一张图片作为头像。更新的内容用粗体标记。

搜索栏的样式在Searchbar.css文件中。很多 flexboxes 都是用来做造型的。将新内容添加到现有内容中。

.sidebar__search {
    display: flex;
    align-items: center;
    background-color: #f6f6f6;
    height: 39px;
    padding: 10px;
}
.sidebar__searchContainer{
    display: flex;
    align-items: center;
    background-color: white;
    width: 100%;
    height: 35px;
    border-radius: 20px;
}
.sidebar__searchContainer > .MuiSvgIcon-root{
    color: gray;
    padding: 10px;
}
.sidebar__searchContainer > input {
    border: none;
    outline-width: 0;
    margin-left: 10px;
}

4-5 显示了本地主机上的所有内容。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig5_HTML.jpg

图 4-5

搜索栏

创建侧边栏聊天组件

现在让我们构建侧边栏聊天组件。在components文件夹中,创建两个文件——SidebarChat.jsSidebarChat.css。在Sidebar.js文件中使用它们。更新的内容用粗体标记。

...
import SidebarChat from './SidebarChat'
const Sidebar = () => {
    return (
        <div className="sidebar">
            <div className="sidebar__header">
               ...
            </div>
            <div className="sidebar__search">
               ...
           </div>
            <div className="sidebar__chats">
                <SidebarChat />
                <SidebarChat />
                <SidebarChat />
        </div>
        </div>
    )
}
export default Sidebar

在编写侧边栏聊天组件之前,让我们设计一下sidebar__chats div 的样式,它包含了Sidebar.css文件中的SidebarChat组件。将新内容添加到现有内容中。

.sidebar__chats{
    flex: 1;
    background-color: white;
    overflow: scroll;
}

SidebarChat.js文件中,有一个简单的功能组件。如果你给一个 API 端点传递随机的字符串,它会提供随机的化身。使用种子状态变量;它每次都随着useEffect中的随机字符串而改变。

import React, { useEffect, useState } from 'react'
import { Avatar } from '@material-ui/core'
import './SidebarChat.css'
const SidebarChat = () => {
    const [seed, setSeed] = useState("")
    useEffect(() => {
        setSeed(Math.floor(Math.random() * 5000))
    }, [])
    return (
        <div className="sidebarChat">
            <Avatar src={`https://avatars.dicebear.com/api/human/b${seed}.svg`} />
            <div className="sidebarChat__info">
                <h2>Room name</h2>
                <p>Last message...</p>
            </div>
        </div>
    )
}
export default SidebarChat

接下来,让我们在SidebarChat.css文件中设计一些房间的样式。这里,您再次使用 flexbox 和一些衬垫。

.sidebarChat{
    display: flex;
    padding: 20px;
    cursor: pointer;
    border-bottom: 1px solid #f6f6f6;
}
.sidebarChat:hover{
    background-color: #ebebeb;
}
.sidebarChat__info > h2 {
    font-size: 16px;
    margin-bottom: 8px;
}
.sidebarChat__info {
    margin-left: 15px;
}

4-6 显示了 localhost 上的侧边栏聊天组件。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig6_HTML.jpg

图 4-6

边栏聊天

创建聊天组件

让我们开始研究聊天组件。在components文件夹中创建两个文件Chat.jsChat.css。把这个基本结构放到Chat.js文件里。随机字符串用于显示随机头像图标。

import React, { useEffect, useState } from 'react'
import { Avatar, IconButton } from '@material-ui/core'
import { AttachFile, MoreVert, SearchOutlined } from '@material-ui/icons'
import './Chat.css'
const Chat = () => {
    const [seed, setSeed] = useState("")
    useEffect(() => {
        setSeed(Math.floor(Math.random() * 5000))
    }, [])
    return (
        <div className="chat">
            <div className="chat__header">
                <Avatar src={`https://avatars.dicebear.com/api/human/b${seed}.svg`} />
                <div className="chat__headerInfo">
                    <h3>Room Name</h3>
                    <p>Last seen at...</p>
                </div>
                <div className="chat__headerRight">
                    <IconButton>
                        <SearchOutlined />
                    </IconButton>
                    <IconButton>
                        <AttachFile />
                    </IconButton>
                    <IconButton>
                        <MoreVert />
                    </IconButton>
                </div>
            </div>
            <div className="chat__body"></div>
            <div className="chat__footer"></div>
        </div>
    )
}
export default Chat

接下来,在Chat.css文件中设置聊天标题的样式,并在chat__body类中添加一个漂亮的背景图片。

.chat{
    display: flex;
    flex-direction: column;
    flex: 0.65;
}

.chat__header{
    padding: 20px;
    display: flex;
    align-items: center;
    border-bottom: 1px solid lightgray;
}
.chat__headerInfo {
    flex: 1;
    padding-left: 20px;
}
.chat__headerInfo > h3 {
    margin-bottom: 3px;
    font-weight: 500;
}
.chat__headerInfo > p {
    color: gray;
}
.chat__body{
    flex: 1;
    background-image: url("https://user-images.githubusercontent.com/15075759/28719144-86dc0f70-73b1-11e7-911d-60d70fcded21.png");
    background-repeat: repeat;
    background-position: center;
    padding: 30px;
    overflow: scroll;
}

App.js文件呈现聊天组件。更新的内容用粗体标记。

import './App.css';
import Sidebar from './components/Sidebar';
import Chat from './components/Chat';
function App() {
  return (
    <div className="app">
      <div className="app__body">
            <Sidebar />
            <Chat />
      </div>
    </div>
  );
}
export default App;

前往本地主机。图 4-7 显示聊天的标题已经完成,并且显示了一个漂亮的背景图像。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig7_HTML.jpg

图 4-7

聊天组件

接下来,返回到Chat.js文件,将硬编码的消息放在chat__message类的p标签中。两个 span 标记用于名称和时间戳。

注意聊天用户的chat__receiver类。更新的内容用粗体标记。

...
const Chat = () => {
    const [seed, setSeed] = useState("")
    useEffect(() => {
        setSeed(Math.floor(Math.random() * 5000))
    }, [])
    return (
        <div className="chat">
            <div className="chat__header">
              ...
            </div>
            <div className="chat__body">
                <p className="chat__message">
                    <span className="chat__name">Nabendu</span>
                    This is a message
                    <span className="chat__timestamp">
                        {new Date().toUTCString()}
                    </span>
                </p>
                <p className="chat__message chat__receiver">
                    <span className="chat__name">Parag</span>
                    This is a message back
                    <span className="chat__timestamp">
                        {new Date().toUTCString()}
                    </span>
                </p>
                <p className="chat__message">
                    <span className="chat__name">Nabendu</span>
                    This is a message again again
                    <span className="chat__timestamp">
                        {new Date().toUTCString()}
                    </span>
                </p>
            </div>
            <div className="chat__footer"></div>
        </div>
    )
}
export default Chat

Chat.css文件中添加样式。

.chat__message{
    position: relative;
    font-size: 16px;
    padding: 10px;
    width: fit-content;
    border-radius: 10px;
    background-color: #ffffff;
    margin-bottom: 30px;
}
.chat__receiver{
    margin-left: auto;
    background-color: #dcf8c6;
}
.chat__timestamp{
    margin-left: 10px;
    font-size: xx-small;
}
.chat__name{
    position: absolute;
    top: -15px;
    font-weight: 800;
    font-size: xx-small;
}

4-8 显示了本地主机上的三条消息。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig8_HTML.jpg

图 4-8

聊天消息

创建聊天页脚组件

让我们完成chat__footer div。表单中还有两个图标和一个输入框。Chat.js 的更新代码用粗体标记。

...
import { AttachFile, MoreVert, SearchOutlined, InsertEmoticon } from '@material-ui/icons'
import MicIcon from '@material-ui/icons/Mic'
import './Chat.css'
...
const Chat = () => {
...
    return (
        <div className="chat">
            <div className="chat__header">
              ...
            </div>
            <div className="chat__body">
              ...
            </div>
            <div className="chat__footer">
                <InsertEmoticon />
                <form>
                    <input
                        placeholder="Type a message"
                        type="text"
                    />
                    <button type="submit">Send a message</button>
                </form>
                <MicIcon />
            </div>
        </div>
    )
}
export default Chat

是时候设计这个chat__footer div 了。注意按钮的display: none。因为它被包装在一个表单中,所以您可以在其中使用 enter。在Chat.css文件中添加以下内容。

.chat__footer{
    display: flex;
    justify-content: space-between;
    align-items:center;
    height: 62px;
    border-top: 1px solid lightgray;
}
.chat__footer > form {
    flex: 1;
    display: flex;
}
.chat__footer > form > input {
    flex: 1;
    outline-width: 0;
    border-radius: 30px;
    padding: 10px;
    border: none;
}
.chat__footer > form > button {
    display: none;
}
.chat__footer > .MuiSvgIcon-root {
    padding: 10px;
    color: gray;
}

4-9 显示了本地主机上的页脚。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig9_HTML.jpg

图 4-9

页脚完成

初始后端设置

让我们转到后端,从 Node.js 代码开始。打开一个新的终端窗口,在根目录下创建一个新的messaging-app-backend文件夹。移动到messaging-app-backend目录后,输入git init命令,这是 Heroku 稍后需要的。

mkdir messaging-app-backend
cd messaging-app-backend
git init

接下来,通过在终端中输入npm init命令来创建package.json文件。你被问了一堆问题;对于大多数情况,只需按下回车键。你可以提供描述作者,但不是强制的。你一般在server.js做进入点,这是标准的(见图 4-10 )。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig10_HTML.jpg

图 4-10

初始后端设置

一旦package.json被创建,您需要创建包含node_modules.gitignore文件,因为您不想以后将 node_modules 推送到 Heroku。以下是.gitignore文件内容。

node_modules

接下来,打开package.json.需要在 Node.js 中启用类似 React 的导入,包括一个启动脚本来运行server.js文件。更新的内容用粗体标记。

{
  "name": "messaging-app-backend",
  "version": "1.0.0",
  "description": "Messaging app backend",
  "main": "server.js",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js"
  },
  "author": "Nabendu Biswas",
  "license": "ISC"
}

最后,您需要在启动之前安装两个软件包。打开终端,在messaging-app-backend文件夹中安装 Express 和 Mongoose。

npm i express mongoose

MongoDB 设置

MongoDB 的设置与第 1 章中描述的相同。按照这些说明,创建一个名为 messaging-app-mern 的新项目。

在继续之前,将nodemon安装在messaging-app-backend文件夹中。它帮助 server.js 中的更改即时重启 Node 服务器。

npm i nodemon

初始路线设置

messaging-app-backend文件夹中创建一个server.js文件,在这里导入 Express 和 Mongoose 包。然后使用 Express 创建一个运行在端口 9000 上的port变量。

第一个 API 端点是一个由app.get()创建的简单 GET 请求,如果成功,它会显示文本 Hello TheWebDev

然后,用app.listen()监听端口。

import express from 'express'

import mongoose from 'mongoose'

//App Config
const app = express()
const port = process.env.PORT || 9000
//Middleware
//DB Config
//API Endpoints
app.get("/", (req, res) => res.status(200).send("Hello TheWebDev"))
//Listener
app.listen(port, () => console.log(`Listening on localhost: ${port}`))

在终端输入 nodemon server.js 查看监听 localhost: 9000 控制台日志。为了检查路线是否正常工作,转到http://localhost:9000/查看终点文本,如图 4-11 所示。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig11_HTML.jpg

图 4-11

初始路线

数据库用户和网络访问

在 MongoDB 中,您需要创建一个数据库用户并授予网络访问权限。该过程与第 1 章中的解释相同。遵循这些说明,然后获取用户凭证和连接 URL。

server.js文件中,创建一个connection_url变量,并将 URL 粘贴到 MongoDB 的字符串中。您需要提供之前保存的密码和数据库名称。

更新后的代码用粗体标记。

...
//App Config
const app = express()
const port = process.env.PORT || 9000
const connection_url = ' mongodb+srv://admin:<password>@cluster0.ew283.mongodb.net/messagingDB?retryWrites=true&w=majority'
//Middleware
//DB Config
mongoose.connect(connection_url, {
    useNewUrlParser: true,
    useCreateIndex: true,
    useUnifiedTopology: true
})
//API Endpoints
app.get("/", (req, res) => res.status(200).send("Hello TheWebDev"))

...

MongoDB 模式和路由

现在让我们创建 MongoDB 所需的模式文件。它告诉您字段在 MongoDB 中的存储方式。在messaging-app-backend文件夹中创建一个dbMessages.js文件。

这里,messagingmessages被认为是一个集合名,您在数据库中存储一个类似于messagingSchema的值。它由一个带有消息、名称、时间戳和接收密钥的对象组成。

import mongoose from 'mongoose'
const messagingSchema = mongoose.Schema({
    message: String,
    name: String,
    timestamp: String,
    received: Boolean
})
export default mongoose.model('messagingmessages', messagingSchema)

现在,您可以使用该模式来创建向数据库添加数据的端点。

server.js中,创建一个到/messages/new端点的 POST 请求。负载在req.body到 MongoDB。然后用create()发送dbMessage。如果成功,您会收到状态 201;否则,您会收到状态 500。

接下来,创建/messages/sync的 GET 端点,从数据库中获取数据。你在这里用的是find()。如果成功,您将收到状态 200(否则,状态 500)。

更新后的代码用粗体标记。

import express from 'express'
import mongoose from 'mongoose'
import Messages from './dbMessages.js'
...
//API Endpoints
app.get("/", (req, res) => res.status(200).send("Hello TheWebDev"))
app.post('/messages/new', (req, res) => {
    const dbMessage = req.body
    Messages.create(dbMessage, (err, data) => {
        if(err)
            res.status(500).send(err)
        else
            res.status(201).send(data)
    })
})
app.get('/messages/sync', (req, res) => {
    Messages.find((err, data) => {
        if(err) {
            res.status(500).send(err)
        } else {
            res.status(200).send(data)
        }
    })
})

//Listener
app.listen(port, () => console.log(`Listening on localhost: ${port}`))

要查看路线,请使用 Postman 应用。下载并安装它。

http://localhost:9000发送 GET 请求,检查是否是邮递员发送的,如图 4-12 所示。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig12_HTML.jpg

图 4-12

初始 GET 请求

在处理 POST 请求之前,您需要完成两件事情。第一,实行 First 否则,在部署应用时会出现跨来源错误。打开终端,在messaging-app-backend文件夹中安装 CORS。

npm i cors

server.js中,导入 CORS,然后配合app.use()使用。你还需要使用express.json()中间件。更新后的代码用粗体标记。

import express from 'express'
import mongoose from 'mongoose'
import Cors from 'cors'
import Messages from './dbMessages.js'
...
//Middleware
app.use(express.json())
app.use(Cors())

...

在 Postman 中,您需要将请求更改为 POST,然后添加http://localhost:9000/messages/new端点。

接下来,点击车身并选择 raw 。从下拉菜单中选择 JSON(应用/json) 。在文本编辑器中,输入如图 4-13 所示的数据。通过在关键字中添加双引号来生成数据 JSON。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig13_HTML.jpg

图 4-13

发布请求

接下来,点击发送按钮。如果一切正确,你得到状态:201 已创建,如图 4-13 所示。

我同样地插入了其他数据,但是用收到的作为真的。您需要测试 GET /messages/sync端点。将请求更改为 GET 并点击发送按钮。如果一切正常,您将获得状态:200 OK ,如图 4-14 所示。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig14_HTML.jpg

图 4-14

获取请求

有时,POST 请求会出现服务器错误。错误为UnhandledPromiseRejectionWarning:MongooseServerSelectionError:connection。如果你得到这个错误,去你的网络访问标签,点击添加 IP 地址按钮。之后点击添加当前 IP 地址按钮,然后点击确认,如图 4-15 所示。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig15_HTML.jpg

图 4-15

网络错误修复

配置推动器

既然 MongoDB 不是实时数据库,那就该给 app 加一个 pusher 来获取实时数据了。前往 https://pusher.com 报名。推杆 app 仪表盘如图 4-16 所示。点击管理按钮。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig16_HTML.jpg

图 4-16

推杆仪表板

在下一个界面,点击创建 app 按钮,如图 4-17 所示。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig17_HTML.jpg

图 4-17

在 Pusher 中创建应用

在弹出窗口中,将应用命名为 messaging-app-mern 。前端是 React,后端是 Node.js,如图 4-18 所示。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig18_HTML.jpg

图 4-18

前端和后端

在下一个屏幕中,您将获得推杆前端和后端的代码,如图 4-19 所示。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig19_HTML.jpg

图 4-19

后端代码

将推杆添加到后端

如前一节所述,您需要停止服务器并安装 Pusher。在messaging-app-backend文件夹中,用下面的命令安装它。

npm i pusher

server.js文件中,导入它,然后使用推动器初始化代码。从 Pusher 网站获取初始化代码( https://pusher.com )。要添加代码,用db.once打开一个数据库连接。然后用watch()观看来自 MongoDB 的消息集合。

changeStream里面,如果operationType被插入,你把数据插入到推动器里。更新后的代码用粗体标记。

...
import Pusher from 'pusher'
...
//App Config
const app = express()
const port = process.env.PORT || 9000
const connection_url = ' mongodb+srv://admin:<password>@cluster0.ew283.mongodb.net/messagingDB?retryWrites=true&w=majority'
const pusher = new Pusher({
    appId: "11xxxx",
    key: "9exxxxxxxxxxxxx",
    secret: "b7xxxxxxxxxxxxxxx",
    cluster: "ap2",
    useTLS: true
});
//API Endpoints
const db = mongoose.connection
db.once("open", () => {
    console.log("DB Connected")
    const msgCollection = db.collection("messagingmessages")
    const changeStream = msgCollection.watch()
    changeStream.on('change', change => {
        console.log(change)
        if(change.operationType === "insert") {
            const messageDetails = change.fullDocument
            pusher.trigger("messages", "inserted", {
                name: messageDetails.name,
                message: messageDetails.message,
                timestamp: messageDetails.timestamp,
                received: messageDetails.received
            })
        } else {
            console.log('Error trigerring Pusher')
        }
    })
})

app.get("/", (req, res) => res.status(200).send("Hello TheWebDev"))
...

//Listener
app.listen(port, () => console.log(`Listening on localhost: ${port}`))

为了测试这一点,您需要从 Postman 发送一个 POST 请求。同时,你需要在调试控制台中推料。

4-20 显示了调试控制台日志中显示的消息。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig20_HTML.jpg

图 4-20

推送器中的消息

在服务器中,控制台日志显示相同,如图 4-21 所示。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig21_HTML.jpg

图 4-21

服务器日志

将推杆添加到前端

是时候回到前端使用 Pusher 了。首先,你需要在messaging-app-frontend文件夹中安装pusher-js包。

npm i pusher-js

使用以下代码,并在App.js文件的前端插入新数据。更新的内容用粗体标记。

...
import React, { useEffect, useState } from 'react'
import Pusher from 'pusher-js'

function App() {
  const [messages, setMessages] = useState([])

  useEffect(() => {
    const pusher = new Pusher('9exxxxxxxxxxxx', {
      cluster: 'ap2'
    });
    const channel = pusher.subscribe('messages');
    channel.bind('inserted', (data) => {
      setMessages([...messages, data])
    });
    return () => {
      channel.unbind_all()
      channel.unsubscribe()
    }
  }, [messages])

  console.log(messages)

  return (
    <div className="app">
      ...
    </div>
  );
}
export default App;

去找邮递员并发送另一个邮寄请求。图 4-22 显示了本地主机上控制台日志的数据。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig22_HTML.jpg

图 4-22

控制台日志

将后端与前端集成在一起

你想在应用初始加载时获取所有消息,然后推送消息。您必须达到 GET 端点,为此您需要 Axios。打开messaging-app-frontend文件夹并安装。

npm i axios

接下来,在components文件夹中创建一个新的axios.js文件,并创建一个axios的实例。基础 URL 是http://localhost:9000

import axios from 'axios'

const instance = axios.create({
    baseURL: "http://localhost:9000"
})
export default instance

接下来,返回到App.js,首先包含本地axios。然后使用useEffect钩子中的axios/messages/sync端点获取所有数据。收到消息后,通过setMessages()进行设置。最后,将消息作为道具传递给聊天组件。

更新的内容用粗体标记。

...
import axios from './components/axios'

function App() {
  const [messages, setMessages] = useState([])

  useEffect(() => {
    axios.get("/messages/sync").then(res => {
      setMessages(res.data)
    })
  }, [])

  useEffect(() => {
    ...
  }, [messages])

  return (
    <div className="app">
      <div className="app__body">
        <Sidebar />
        <Chat messages={messages} />
      </div>
    </div>
  );
}
export default App;

Chat.js文件中,使用这条消息的道具并通过它映射到屏幕上显示。

如果消息包含received键,则添加chat__receiver类。更新的内容用粗体标记。

...
const Chat = ({ messages }) => {
    const [seed, setSeed] = useState("")
    useEffect(() => {
        setSeed(Math.floor(Math.random() * 5000))
    }, [])
    return (
        <div className="chat">
            <div className="chat__header">
              ...
            </div>
            <div className="chat__body">
                {messages.map(message => (
                    <p className={`chat__message ${message.received && 'chat__receiver'}`}>
                        <span className="chat__name">{message.name}</span>
                            {message.message}
                        <span className="chat__timestamp">
                            {message.timestamp}
                        </span>
                    </p>
                ))}
            </div>
            <div className="chat__footer">
                 ...
             </div>
        </div>
    )
}
export default Chat

你可以在 localhost 上看到所有的消息。如果你通过 Postman 发布了一条新消息,你会在聊天中得到它,如图 4-23 所示。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig23_HTML.jpg

图 4-23

新消息

添加直接从消息框发布的逻辑。首先,导入局部axios,然后创建一个输入状态变量。

然后在输入上做onChange React 的事情,并在按钮的onClick事件处理程序上附加一个sendMessage函数。

sendMessage函数中,使用所需的数据对/messages/new端点进行 POST 调用。Chat.js中更新的内容用粗体标出。

import axios from './axios'
...
const Chat = ({ messages }) => {
    const [seed, setSeed] = useState("")
    const [input, setInput] = useState("")
    const sendMessage = async (e) => {
        e.preventDefault()
        await axios.post('/messages/new', {
            message: input,
            name: "thewebdev",
            timestamp: new Date().toUTCString(),
            received: true
        })
        setInput("")
    }
    useEffect(() => {
        setSeed(Math.floor(Math.random() * 5000))
    }, [])
    return (
        <div className="chat">
            <div className="chat__header">
              ...
            </div>
            <div className="chat__body">
               ...
            </div>
            <div className="chat__footer">
                <InsertEmoticon />
                <form>
                    <input
                        value={input}
                        onChange={e => setInput(e.target.value)}
                        placeholder="Type a message"
                        type="text"
                    />
                    <button onClick={sendMessage} type="submit">Send a message</button>
                </form>
                <MicIcon />
             </div>
        </div>
    )
}
export default Chat

您可以在输入框中键入文本,当您按下 Enter 键时,该消息会立即显示在聊天中,如图 4-24 所示。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig24_HTML.jpg

图 4-24

来自输入的消息

附加设置

接下来,让我们将 Google 身份验证添加到项目中,以便用户可以使用他们的 Google 帐户登录。

对于 Google 身份验证,您需要在 Firebase 控制台中进行额外的设置。点击屏幕右上角的设置图标。之后点击项目设置按钮,如图 4-25 所示。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig25_HTML.jpg

图 4-25

附加设置

在下一页中,点击页面底部的 web 图标,如图 4-26 所示。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig26_HTML.jpg

图 4-26

网络图标

在下一页,输入应用的名称(在我的例子中是 messaging-app-mern )。选中 Firebase hosting 复选框。点击注册 app 按钮(见图 4-27 )。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig27_HTML.jpg

图 4-27

Firebase 托管

在下一页,点击下一个按钮(见图 4-28 )。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig28_HTML.jpg

图 4-28

下一个屏幕

在下一页,从终端运行firebase-tools全局安装 Firebase。注意,这是机器上的一次性设置,因为它与-g选项一起使用(见图 4-29 )。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig29_HTML.jpg

图 4-29

全局安装

忽略下一组命令,点击继续到控制台按钮(见图 4-30 )。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig30_HTML.jpg

图 4-30

继续

接下来,向下滚动页面并选择配置单选按钮。然后复制firebaseConfig数据,如图 4-31 所示。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig31_HTML.jpg

图 4-31

配置详细信息

在 Visual Studio 代码中打开代码,并在src文件夹中创建一个firebase.js文件。粘贴 VSCode 中的内容。

初始化 Firebase 应用并使用数据库。使用 Firebase 中的auth, provider。以下是firebase.js内容。

import firebase from 'firebase/app';
import 'firebase/auth';        // for authentication
import 'firebase/storage';     // for storage
import 'firebase/database';    // for realtime database
import 'firebase/firestore';   // for cloud firestore
const firebaseConfig = {
    apiKey: "Axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    authDomain: "messaging-xxxxxxxxxxxxxxxx.com",
    projectId: "messaging-xxxxx",
    storageBucket: "messaging-app-xxxxxxxxxxxxxxxxx",
    messagingSenderId: "83xxxxxxxxxxxx",
    appId: "1:836xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
};
const firebaseApp = firebase.initializeApp(firebaseConfig)
const db = firebaseApp.firestore()
const auth = firebase.auth()
const provider = new firebase.auth.GoogleAuthProvider()

export { auth, provider }
export default db

在终端中,您需要在messaging-app-frontend文件夹中安装所有 Firebase 依赖项。

npm i firebase

创建登录组件

components文件夹中创建两个文件Login.jsLogin.css。在Login.js文件中,有一个简单的功能组件,显示一个徽标和一个用 Google 按钮登录的**。以下是Login.js的内容。**

import React from 'react'
import { Button } from '@material-ui/core'
import './Login.css'

const Login = () => {
    const signIn = () => {

    }

    return (
        <div className="login">
            <div className="login__container">
                <img src="logo512.png" alt="whatsapp" />
                <div className="login__text">
                    <h1>Sign in to Messaging App</h1>
                </div>
                <Button onClick={signIn}>Sign In with Google</Button>
            </div>
        </div>
    )
}

export default Login

让我们在Login.css文件中创建样式。以下是Login.css内容。

.login{
    background-color: #f8f8f8;
    height: 100vh;
    width: 100vw;
    display: grid;
    place-items: center;
}
.login__container{
    padding: 100px;
    text-align: center;
    background-color: white;
    border-radius: 10px;
    box-shadow: -1px 4px 20px -6px rgba(0, 0, 0, 0.75);
}
.login__container > img {
    object-fit: contain;
    height: 100px;
    margin-bottom: 40px;
}
.login__container > button {
    margin-top: 50px;
    text-transform: inherit !important;
    background-color: #0a8d48 !important;
    color: white;
}

接下来,让我们展示一个没有用户的登录组件。创建一个临时状态变量,并将其显示在App.js文件中。更新的内容用粗体标记。

...
import Login from './components/Login';
function App() {
  const [messages, setMessages] = useState([])
  const [user, setUser] = useState(null)
  ...
  return (
    <div className="app">
      { !user ? <Login /> : (
        <div className="app__body">
          <Sidebar />
          <Chat messages={messages} />
        </div>
      )}
    </div>
  );
}
export default App;

4-32 显示了本地主机上的登录屏幕。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig32_HTML.jpg

图 4-32

登录屏幕

添加 Google 身份验证

使用登录方式前,返回 Firebase,点击认证选项卡,然后点击开始按钮,如图 4-33 所示。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig33_HTML.jpg

图 4-33

开始

在下一个界面中,点击谷歌认证的编辑配置图标,如图 4-34 所示。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig34_HTML.jpg

图 4-34

谷歌登录

在弹出窗口中,点击启用按钮。接下来,输入你的 Gmail id,点击保存按钮(见图 4-35 )。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig35_HTML.jpg

图 4-35

启用 Google 登录

...
import { auth, provider } from '../firebase'
const Login = () => {
    const signIn = () => {
        auth.signInWithPopup(provider)
            .then(result => console.log(result))
            .catch(error => alert(error.message))
    }

    return (
        <div className="login">
                ...
        </div>
    )
}
export default Login

接下来,在Login.js文件中,需要从本地 Firebase 文件导入auth, provider。之后,使用signInWithPopup()方法得到结果。更新的内容用粗体标记。

点击 localhost 上的用 Google 按钮登录。将打开一个 Gmail 身份验证弹出窗口。点击用户名后,在控制台中可以看到登录用户的所有信息,如图 4-36 所示。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig36_HTML.jpg

图 4-36

Google 认证成功

使用 Redux 和上下文 API

让我们将用户数据分派到数据层,这里 Redux/Context API 开始发挥作用。

您希望用户信息存储在全局状态中。首先,创建一个新的StateProvider.js文件。使用 useContext API 创建一个StateProvider函数。以下是内容。你可以在 www.youtube.com/watch?v=oSqqs16RejM 的我的 React hooks YouTube 视频中了解更多关于useContext钩子的信息。

import React, { createContext, useContext, useReducer } from "react"
export const StateContext = createContext()
export const StateProvider = ({ reducer, initialState, children }) => (
    <StateContext.Provider value={useReducer(reducer, initialState)}>
        {children}
    </StateContext.Provider>
)
export const useStateValue = () => useContext(StateContext)

接下来,在components文件夹中创建一个reducer.js文件。这是一个类似于 Redux 组件中的 reducer 的概念。您可以在 www.youtube.com/watch?v=m0G0R0TchDY 了解更多信息。以下是内容。

export const initialState = { user: null }

export const actionTypes = {
    SET_USER: "SET_USER"
}
const reducer = (state, action) => {
    console.log(action)
    switch(action.type) {
        case actionTypes.SET_USER:
            return {
                ...state,
                user: action.user
            }
        default:
            return state
    }
}
export default reducer

index.js文件中,导入所需文件后,用StateProvider组件包装 app 组件。更新的内容用粗体标记。

...
import { StateProvider } from './components/StateProvider';
import reducer, { initialState } from './components/reducer';
ReactDOM.render(
  <React.StrictMode>
    <StateProvider initialState={initialState} reducer={reducer}>
      <App />
    </StateProvider>
  </React.StrictMode>,
  document.getElementById('root')
);

当你从 Google 取回用户数据时,你在Login.js文件中将它调度到 reducer,它存储在数据层。

这里,useStateValue是一个钩子。事实上,它是一个自定义钩子的例子。更新的内容用粗体标记。

...
import { actionTypes } from './reducer'
import { useStateValue } from './StateProvider'

const Login = () => {
    const [{}, dispatch] = useStateValue()

    const signIn = () => {
        auth.signInWithPopup(provider)
            .then(result => {
                dispatch({
                    type: actionTypes.SET_USER,
                    user: result.user
                })
             })
            .catch(error => alert(error.message))
    }

    return (
        <div className="login">
            ...
        </div>
    )
}
export default Login

App.js文件中,使用useStateValue钩子,从中提取全局用户。然后,你基于它登录。更新的内容用粗体标记。

...
import { useStateValue } from './components/StateProvider';
function App() {
  const [messages, setMessages] = useState([])
  const [{ user }, dispatch] = useStateValue()
  ...
  return (
    <div className="app">
      ...
    </div>
  );
}
export default App;

如果你在 localhost 上登录,你会被带到应用,如图 4-37 所示。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig37_HTML.jpg

图 4-37

已登录

在其他组件中使用 Redux 数据

你可以访问用户的数据,所以你可以在任何地方使用它。让我们使用用户的 Google 图片作为Sidebar.js文件中的头像。让我们去掉多余的房间,因为这个项目只有一个房间,每个人都可以聊天。

更新的内容用粗体标记。

...
import { useStateValue } from './StateProvider';
const Sidebar = () => {
    const [{ user }, dispatch] = useStateValue()
    return (
        <div className="sidebar">
            <div className="sidebar__header">
                <Avatar src={user?.photoURL} />
                <div className="sidebar__headerRight">
                   ...
                </div>
            </div>
            <div className="sidebar__search">
                   ...
            </div>
            <div className="sidebar__chats">
                <SidebarChat />
            </div>
        </div>
    )
}
export default Sidebar

4-38 在 localhost 的页面左上角显示了登录用户的 Google 图片。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig38_HTML.jpg

图 4-38

登录映像

Chat.js,中,使用useStateValue钩子获取用户的显示名称。然后检查 message.name 是否等于user.displayName以显示chat__receiver类。修复上次出现的硬编码**...Chat.js文件中chat__header消息;更新以显示最后一个人发信息的时间。同时将房间名称更改为开发帮助**。

更新的内容用粗体标记。

...
import { useStateValue } from './StateProvider';

const Chat = ({ messages }) => {
   ...
   const [{ user }, dispatch] = useStateValue()

    const sendMessage = async (e) => {        e.preventDefault()
        await axios.post('/messages/new', {
            message: input,
            name: user.displayName,
            timestamp: new Date().toUTCString(),
            received: true
        })
        setInput("")
    }
    ...
    return (
        <div className="chat">
            <div className="chat__header">
                <Avatar src={`https://avatars.dicebear.com/api/human/b${seed}.svg`} />
                <div className="chat__headerInfo">
                    <h3>Dev Help</h3>
                    <p>Last seen at {" "}
                        {messages[messages.length -1]?.timestamp}
                    </p>
                </div>
            </div>
            <div className="chat__body">
                {messages.map(message => (
                    <p className={`chat__message ${message.name === user.displayName && 'chat__receiver'}`}>
                    ...
                    </p>
                ))}
            </div>
            <div className="chat__footer">
               ...
             </div>
        </div>
    )
}
export default Chat

键入一些内容,然后单击 Enter。您可以看到消息已收到。图 4-39 显示场景已经更新。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig39_HTML.jpg

图 4-39

时间更新

最后要改变的是侧边栏中的硬编码消息。你需要在这里显示最后一条消息。首先,将消息从App.js文件发送到侧栏组件。

更新的内容用粗体标记。

...
function App() {
  ...
  return (
    <div className="app">
      { !user ? <Login /> : (
        <div className="app__body">
          <Sidebar messages={messages} />
          <Chat messages={messages} />
        </div>
      )}
    </div>
  );
}
export default App;

之后,从Sidebar.js文件到SidebarChat组件。更新的内容用粗体标记。

...
const Sidebar = ({ messages }) => {
    const [{ user }, dispatch] = useStateValue()
    return (
        <div className="sidebar">
            <div className="sidebar__header">
                      ...
            </div>
            <div className="sidebar__search">
                      ...
            </div>
            <div className="sidebar__chats">
                <SidebarChat messages={messages} />
            </div>
        </div>
    )
}
export default Sidebar

最后,在SidebarChat.js文件中,显示最后一条消息而不是硬编码的消息,并将房间名改为 Dev Help

更新的内容用粗体标记。

...
const SidebarChat = ({ messages }) => {
    ...
    return (
        <div className="sidebarChat">
            <Avatar src={`https://avatars.dicebear.com/api/human/b${seed}.svg`} />
            <div className="sidebarChat__info">
                <h2>Dev Help</h2>
                <p>{messages[messages.length -1]?.message}</p>
            </div>
        </div>
    )
}
export default SidebarChat

应用已完成。图 4-40 显示了侧边栏中的最新消息。我还在不同的谷歌账户中测试了我的登录。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig40_HTML.jpg

图 4-40

应用完成

将后端部署到 Heroku

转到 www.heroku.com 部署后端。按照你在第 1 章中所做的相同步骤,创建一个名为消息传递-应用-后端的应用。

部署成功后,进入 https://messaging-app-backend.herokuapp.com 。图 4-41 显示了正确的文本。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig41_HTML.jpg

图 4-41

初始路线检查

axios.js中,将端点改为 https://messaging-app-backend.herokuapp.com 。如果一切正常,你的应用应该可以运行了。

import axios from 'axios'
const instance = axios.create({
    baseURL: " https://messaging-app-backend.herokuapp.com "
})
export default instance

将前端部署到 Firebase

是时候在 Firebase 中部署前端了。遵循与第 1 章相同的程序。完成此过程后,站点应处于活动状态并正常工作,如图 4-42 所示。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig42_HTML.jpg

图 4-42

最终应用

摘要

在这一章中,你创建了一个简单而实用的聊天应用。Firebase 在网上主办的。您学习了添加 Google 身份验证,通过它您可以使用 Google 帐户登录。您还学习了使用 Node.js 创建的 API 路由将聊天存储在 MongoDB 数据库中。