MERN-项目初学者指南-二-

76 阅读28分钟

MERN 项目初学者指南(二)

原文:MERN Projects for Beginners

协议:CC BY-NC-SA 4.0

四、使用 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 的同时创建基本设置。按照第一章的设置说明,我在 Firebase 控制台中创建了消息应用。

React 基本设置

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

cd messaging-app-frontend
npm start

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

图 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 的设置与第一章中描述的相同。按照这些说明,创建一个名为 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 中,您需要创建一个数据库用户并授予网络访问权限。该过程与第一章中的解释相同。遵循这些说明,然后获取用户凭证和连接 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 部署后端。按照你在第一章中所做的相同步骤,创建一个名为消息传递-应用-后端的应用。

部署成功后,进入 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 中部署前端了。遵循与第一章相同的程序。完成此过程后,站点应处于活动状态并正常工作,如图 4-42 所示。

img/512020_1_En_4_Chapter/512020_1_En_4_Fig42_HTML.jpg

图 4-42

最终应用

摘要

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

五、使用 MERN 构建一个基于照片的社交网络

在这一章中,你将使用 MERN 框架建立一个基于照片的社交网络。后端托管在 Heroku,前端站点使用 Firebase 托管。Firebase 也处理身份验证功能。

Material-UI 提供了项目中的图标。使用 Pusher 是因为 MongoDB 不像 Firebase 那样是实时数据库。您希望帖子反映出某人点击提交按钮的瞬间。

有了这个基于照片的功能性社交网络,你可以从你的电脑上传图片并写下描述。用户通过电子邮件登录。最终托管的 app 如图 5-1 所示。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig1_HTML.jpg

图 5-1

最终应用

首先,在你的终端上创建一个photo-social-mern文件夹。在里面,它使用创建-反应-应用来创建一个名为照片-社交-前端的新应用。以下是命令。

mkdir photo-social-mern
cd photo-social-mern
npx create-react-app photo-social-frontend

Firebase 托管初始设置

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

由于使用了认证功能,您需要进行第四章中提到的额外配置,并使用您需要复制的firebaseConfig(参见图 5-2 )。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig2_HTML.jpg

图 5-2

配置

在 Visual Studio Code (VSCode)中打开代码,在src文件夹中创建一个firebase.js文件,并将配置内容粘贴到那里。

const firebaseConfig = {
    apiKey: "AIxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxY",
    authDomain: "photo-xxxxxxxxxxxxxxxxxxxxxxx.com",
    projectId: "photo-xxxxxxxxxxx",
    storageBucket: "photo-xxxxxxxxxxxx",
    messagingSenderId: "52xxxxxxx",
    appId: "1:52xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
};

React 基本设置

返回 React 项目,将cd返回到photo-social-frontend目录。用npm start启动 React 应用。

cd photo-social-frontend
npm start

index.jsApp.jsApp.css中删除文件和基本设置就像在第二章中所做的一样。遵循这些指示。图 5-3 显示了应用在 localhost 上的外观。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig3_HTML.jpg

图 5-3

初始应用

创建标题组件

让我们创建应用标题,这是一个很好的标志。在App.js文件中,用app__header类名创建一个 div,并使用 public 文件夹中的 React 徽标,这是每个 React 项目都附带的。更新的内容用粗体标记。

import './App.css';
function App() {
  return (
    <div className="app">
      <div className="app__header">
        <img className="app__headerImage" src="logo192.png" alt="Header" />
      </div>
    </div>
  );
}

export default App;

接下来,开始在App.css文件中编写样式。在这里,您为应用、app__headerapp__headerImage类编写样式。

.app {
  background-color: #fafafa;
}

.app__header{
  background-color: white;
  padding: 20px;
  border-bottom: 1px solid lightgray;
  object-fit: contain;
}

.app__headerImage {
  object-fit: contain;
  margin-left: 10px;
  height: 40px;
}

图 5-4 显示了 localhost 上的 logo。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig4_HTML.jpg

图 5-4

完美的标志

创建帖子组件

现在让我们创建 post 组件,它包含登录用户的头像,包括一张照片和一个简短的描述。在src文件夹中创建一个components文件夹。然后,在components文件夹中创建两个文件——Post.jsPost.css

Post.js文件是一个简单的功能组件,包含用户名、图片和帖子。

import React from 'react'
import './Post.css'
const Post = () => {
    return (
        <div className="post">
            <h3>TWD</h3>
            <img className="post__image" src="https://www.techlifediary.com/wp-content/uploads/2020/06/react-js.png" alt="React" />
            <h4 className="post__text"><strong>thewebdev</strong>&#x1F525;Build a Messaging app with MERN (MongoDB, Express, React JS, Node JS) &#x1F525;</h4>
        </div>
    )
}

export default Post

App.js文件中,包含三次Post组件。更新的内容用粗体标记。

import './App.css';
import Post from './components/Post';
function App() {
  return (
    <div className="app">
      <div className="app__header">
        <img className="app__headerImage" src="logo192.png" alt="Header" />
      </div>
       <Post />
        <Post />
        <Post />
    </div>
  );
}

export default App;

图标来自于 Material-UI ( https://material-ui.com )。首先,根据文档进行两次 npm 安装。通过photo-social-frontend文件夹中的集成端子安装铁芯。

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

Post.js中,从 Material-UI 添加一个头像图标。在一个post__header div 中,它和h3标签一起使用。更新的内容用粗体标记。

...
import { Avatar } from '@material-ui/core'

const Post = () => {
    return (
        <div className="post">
           <div className="post__header">
                <Avatar
                    className="post__avatar"
                    alt="TWD"
                    src="/statimg/avatar/1.jpg"
                />
                <h3>TWD</h3>
            </div>
            <img className="post__image" src="https://www.techlifediary.com/wp-content/uploads/2020/06/react-js.png" alt="React" />
            ...
        </div>
    )
}

export default Post

接下来,在Post.css文件中添加样式。

.post {
    background-color: white;
    max-width: 800px;
    border: 1px solid lightgray;
    margin-bottom: 45px;
}
.post__image {
    width: 100%;
    object-fit: contain;
    border-top: 1px solid lightgray;
    border-bottom: 1px solid lightgray;
}
.post__text {
    font-weight: normal;
    padding: 20px;
}

.post__header {
    display: flex;
    align-items: center;
    padding: 20px;
}
.post__avatar {
    margin-right: 10px;
}

图 5-5 显示了应用现在在 localhost 上的样子。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig5_HTML.jpg

图 5-5

风格帖子

使组件动态化

让我们把一切都动态化,把用户名、标题和图片 URL 作为道具传递。在Post.js中,进行以下更改。更新的内容用粗体标记。

...
import { Avatar } from '@material-ui/core'

const Post = ({ username, caption, imageUrl }) => {
    return (
        <div className="post">
           <div className="post__header">
                <Avatar
                    className="post__avatar"
                    alt= {username}
                    src="/statimg/avatar/1.jpg"
                />
                <h3> {username}</h3>
            </div>
            <img className="post__image" src={imageUrl} alt="React" />
            <h4 className="post__text"><strong>{username}</strong>{caption}</h4>
        </div>
    )
}

export default Post

接下来,我们来优化一下App.js中的代码。这里,您使用useState钩子来创建新的状态帖子。这里的柱子是数组中的对象。

在 return 语句中,映射 posts 数组并显示每个帖子。更新的内容用粗体标记。

...
import React, { useEffect, useState } from 'react';
function App() {
  const [posts, setPosts] = useState([
    {
      username: "TWD",
      caption: "&#x1F525;Build a Messaging app with MERN Stack&#x1F525;",
      imageUrl: "https://www.techlifediary.com/wp-content/uploads/2020/06/react-js.png"
    },
    {
      username: "nabendu82",
      caption: "Such a beautiful world",
      imageUrl: "https://quotefancy.com/media/wallpaper/3840x2160/126631-Charles-Dickens-Quote-And-a-beautiful-world-you-live-in-when-it-is.jpg"
     }
  ])
  return (
    <div className="app">
      <div className="app__header">
        <img className="app__headerImage" src="logo192.png" alt="Header" />
      </div>
        {posts.map(post => (
            <Post username={post.username} caption={post.caption} imageUrl={post.imageUrl} />
        ))}
    </div>
  );
}

export default App;

图 5-6 显示在 localhost 上。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig6_HTML.jpg

图 5-6

一切动态

Firebase 身份验证设置

让我们来看看 Firebase 身份验证,它允许您登录应用并发布内容。这个项目使用基于电子邮件的认证,这不同于前一章中的 Google 认证。

你需要回到火焰基地。点击认证选项卡,然后点击开始按钮,如图 5-7 所示。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig7_HTML.jpg

图 5-7

开始

在下一个界面中,点击邮箱/密码的编辑图标,如图 5-8 所示。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig8_HTML.jpg

图 5-8

电子邮件和密码

在弹出的窗口中,点击启用按钮,然后点击保存按钮,如图 5-9 所示。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig9_HTML.jpg

图 5-9

启用电子邮件和密码

创建注册模式

现在,让我们展示一个来自 Material-UI 的注册模型。这个代码来自 https://material-ui.com/components/modal/#modal

首先,在App.js文件中导入几个依赖项和两个样式。在那之后,你就有了模态样式的常量。打开状态最初设置为

返回内部,将模态和注册按钮的打开状态设置为

更新的内容用粗体标记。

...
import { makeStyles } from '@material-ui/core/styles';
import Modal from '@material-ui/core/Modal';
import { Button, Input } from '@material-ui/core';
function getModalStyle() {
  const top = 50;
  const left = 50;
  return {
    top: `${top}%`,
    left: `${left}%`,
    transform: `translate(-${top}%, -${left}%)`,
  };
}
const useStyles = makeStyles((theme) => ({
  paper: {
    position: 'absolute',
    width: 400,
    backgroundColor: theme.palette.background.paper,
    border: '2px solid #000',
    boxShadow: theme.shadows[5],
    padding: theme.spacing(2, 4, 3),
  },
}));

function App() {
  const classes = useStyles();
  const [modalStyle] = React.useState(getModalStyle);
  const [open, setOpen] = useState(false)
  ...
  return (

    <div className="app">
      <Modal open={open} onClose={() => setOpen(false)}>
        <div style={modalStyle} className={classes.paper}>
          <h2>Modal Code</h2>
        </div>
      </Modal>
      <div className="app__header">...</div>
      <Button onClick={() => setOpen(true)}>Sign Up</Button>
        {posts.map(post => (
            <Post ={post.username} caption={post.caption} imageUrl={post.imageUrl} />
        ))}
    </div>
  );
}

export default App;

在 localhost 上,点击注册按钮,获得带文本的模态(见图 5-10 )。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig10_HTML.jpg

图 5-10

模式弹出菜单

在创建表单之前,您需要在App.js文件中创建三个状态变量——usernameemail,password

用户名、电子邮件和密码的字段在App.js文件的模式中。还有一个按钮包含一个调用signUp函数的onClick处理程序。

更新的内容用粗体标记。

...

function App() {
 ...
  const [username, setUsername] = useState('')
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  ...
  const signUp = e => {
    e.preventDefault()
  }

  return (
    <div className="app">
      <Modal open={open} onClose={() => setOpen(false)}>
        <div style={modalStyle} className={classes.paper}>
          <form className="app__signup">
              <center>
                <img className="app__headerImage" src="logo192.png"       alt="Header" />
              </center>
                <Input placeholder="username"
                  type="text"
                  value={username}
                  onChange={e => setUsername(e.target.value)}
                />

                <Input placeholder="email"
                  type="text"
                  value={email}
                  onChange={e => setEmail(e.target.value)}
                />
                <Input placeholder="password"
                  type="password"
                  value={password}
                  onChange={e => setPassword(e.target.value)}
                />
                <Button type="submit" onClick={signUp}>Sign Up</Button>
            </form>
        </div>
      </Modal>
      <div className="app__header">...</div>
      ...
    </div>
  );
}

export default App;

App.css文件中,为app__signup类添加样式。

.app__signup {
  display: flex;
  flex-direction: column;
}

图 5-11 显示点击 localhost 上的注册按钮打开一个表单。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig11_HTML.jpg

图 5-11

注册表单

向 Firebase 注册

让我们从用于身份验证的 Firebase 设置开始。首先,在photo-social-frontend文件夹中安装 Firebase 的所有依赖项。

npm i firebase

接下来,更新firebase.js文件以使用配置来初始化应用。更新的内容用粗体标记。

import firebase from 'firebase';

const firebaseConfig = {
    ...
};

const firebaseApp = firebase.initializeApp(firebaseConfig)
const db = firebaseApp.firestore()
const auth = firebase.auth()
const storage = firebase.storage()

export { db, auth, storage }

让我们为应用添加身份验证。首先,从本地 Firebase 导入 auth,然后在App.js文件中添加一个新的user状态变量。

向使用 Firebase 中的createUserWithEmailAndPassword并传递电子邮件和密码的signUp函数添加代码。之后,更新用户,将displayName设置为用户名。使用useEffect钩子来监控任何用户更改,并使用setUser()来更新user变量。

在返回界面中,检查用户是否登录,然后显示注销按钮或注册按钮。

更新的内容用粗体标记。

import { auth } from './firebase'
...

function App() {
 ...
  const [user, setUser] = useState(null)
  ...
  useEffect(() => {
    const unsubscribe = auth.onAuthStateChanged(authUser => {
      if(authUser) {
        console.log(authUser)
        setUser(authUser)
      } else {
        setUser(null)
      }
    })
    return () => {
      unsubscribe()
    }
  }, [user, username])
  const signUp = (e) => {
    e.preventDefault()
    auth.createUserWithEmailAndPassword(email, password)
      .then(authUser => authUser.user.updateProfile({ displayName: username }))
      .catch(error => alert(error.message))

    setOpen(false)
  }
  return (
    <div className="app">
      <Modal open={open} onClose={() => setOpen(false)}>...</Modal>
      <div className="app__header">...</div>
      {user ? <Button onClick={() => auth.signOut()}>Logout</Button> : <Button onClick={() => setOpen(true)}>Sign Up</Button>}
      ...
    </div>
  );
}

export default App;

身份验证在本地主机上正常工作。你可以注册一个新用户,如图 5-12 所示。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig12_HTML.jpg

图 5-12

用户注册

使用 Firebase 登录

现在让我们通过在App.js文件中创建一个新的登录按钮和一个新的模态组件来处理登录功能。

首先,在App.js文件中创建openSignIn状态变量和函数。该函数包含来自 Firebase 的signInWithEmailAndPassword

注意,只使用了 email 和密码,但是有一个新的openSignIn状态变量和它的setOpenSignIn setter。更新的内容用粗体标记。

...
function App() {
 ...
  const [openSignIn, setOpenSignIn] = useState(false)
...
  const signIn = e => {
    e.preventDefault()
    auth.signInWithEmailAndPassword(email, password)
      .catch(error => alert(error.message))
    setOpenSignIn(false)
  }

  return (
    <div className="app">
      <Modal open={open} onClose={() => setOpen(false)}>...</Modal>
      <Modal open={openSignIn} onClose={() => setOpenSignIn(false)}>
        <div style={modalStyle} className={classes.paper}>
          <form className="app__signup">
            <center>
              <img className="app__headerImage" src="logo192.png" alt="Header" />
            </center>
              <Input placeholder="email" type="text" value={email}
                onChange={e => setEmail(e.target.value)}  />
              <Input placeholder="password" type="password" value={password}
                onChange={e => setPassword(e.target.value)}  />
              <Button type="submit" onClick={signIn}>Sign In</Button>
          </form>
        </div>
      </Modal>
      <div className="app__header">...</div>
      {user ? <Button onClick={() => auth.signOut()}>Logout</Button> :  (
          <div className="app__loginContainer">
            <Button onClick={() => setOpenSignIn(true)}>Sign In</Button>
            <Button onClick={() => setOpen(true)}>Sign Up</Button>
          </div>
        )}}
      ...
    </div>
  );
}

export default App;

localhost 上的按钮有了新的标志。它会打开一个弹出窗口来输入凭证(参见图 5-13 )。使用您为登录按钮输入的相同凭据,您可以成功登录。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig13_HTML.jpg

图 5-13

登录弹出窗口

添加帖子和图片

Firebase 用户身份验证已完成。添加帖子的代码并上传图片。一旦你开始后端,你就回到这个部分。

components文件夹中新建文件ImageUpload.jsImageUpload.css,并导入到App.js文件中。接下来,在App.js文件中传递来自ImageUpload的道具用户名。

App.js,中,创建一个具有app__posts类名的新 div,并在其中包含文章。App.js文件的更新内容用粗体标记。

...
import ImageUpload from './components/ImageUpload';
...
function App() {
...
  return (
    <div className="app">
        ...
        {user ? <Button onClick={() => auth.signOut()}>Logout</Button> :(
            ...
        )}
        <div className="app__posts">
          {posts.map(post => (
              <Post username={post.username} caption={post.caption} imageUrl={post.imageUrl} />
          ))}
        </div>
        {user?.displayName ? <ImageUpload username={user.displayName} /> : <h3 className="app__notLogin">Need to login to upload</h3>}
    </div>
  );
}

export default App;

ImageUpload.js文件中,从基本内容开始。有一个标题输入框和另一个图像输入框。还有一个按钮和一个进度条。

以下是ImageUpload.js文件的内容。

import React, { useState } from 'react'
import './ImageUpload.css'
const ImageUpload = ({ username }) => {
    const [image, setImage] = useState(null)
    const [progress, setProgress] = useState(0)
    const [caption, setCaption] = useState('')
const handleChange = e => {
        if(e.target.files[0]) {
            setImage(e.target.files[0])
        }
    }

const handleUpload = () => {}
    return (

        <div className="imageUpload">
            <progress className="imageUpload__progress" value={progress} max="100" />
            <input
                type="text"
                placeholder="Enter a caption..."
                className="imageUpload__input"
                value={caption}
                onChange={e => setCaption(e.target.value)}
            />
            <input className="imageUpload__file" type="file" onChange={handleChange} />
            <button className="imageUpload__button" onClick={handleUpload}>Upload</button>
        </div>
    )
}

export default ImageUpload

前端几乎完成,但你需要完成造型。首先,在ImageUpload.css文件中添加样式。以下是该文件的内容。

.imageUpload {
    display: flex;
    flex-direction: column;
    max-width: 800px;
    width: 100%;
    margin: 10px auto;
}

.imageUpload__progress{
    width: 100%;
    margin-bottom: 10px;
}

.imageUpload__input{
    padding: 10px;
    margin-bottom: 10px;
}

.imageUpload__file {

    margin-bottom: 10px;
}

.imageUpload__button {
    border: none;
    color: lightgray;
    background-color: #6082a3;
    cursor: pointer;
    padding: 10px;
    font-weight: bolder;
    font-size: 0.9rem;
}

.imageUpload__button:hover {
    color: #6082a3;
    background-color: lightgray;
}

图 5-14 显示了本地主机上的图像上传。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig14_HTML.jpg

图 5-14

图像上传

App.css文件中添加样式。更新后的代码用粗体标记。它保留了app__signupapp__headerImage的现有代码。

.app {
  display:grid;
  place-items: center;
  background-color: #fafafa;
}

.app__header{
  display: flex;
  justify-content: space-between;
  position: sticky;
  top: 0;
  z-index: 1;
  width: 100%;
  background-color: white;
  padding: 20px;
  border-bottom: 1px solid lightgray;
  object-fit: contain;
}

.app__notLogin{

  margin-bottom: 20px;
}

.app__loginContainer{
  margin-right: 10px;
}

.app__posts {
  padding: 20px;
}

App.js中有一个小的修正,将用户代码移动到app__header div 中。更新后的代码用粗体标记。

...
function App() {
...
  return (
    <div className="app">
      ...
      <div className="app__header">
        <img className="app__headerImage" src="logo192.png" alt="Header" />
        {user ? <Button onClick={() => auth.signOut()}>Logout</Button> :(
          <div className="app__loginContainer">
            <Button onClick={() => setOpenSignIn(true)}>Sign In</Button>
            <Button onClick={() => setOpen(true)}>Sign Up</Button>
          </div>
        )}
      </div>
     ...
    </div>
  );
}

export default App;

图 5-15 显示了本地主机上桌面视图中的应用。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig15_HTML.jpg

图 5-15

前端完成

初始后端设置

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

mkdir photo-social-backend
cd photo-social-backend
git init

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

img/512020_1_En_5_Chapter/512020_1_En_5_Fig16_HTML.jpg

图 5-16

初始后端

一旦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"
}

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

npm i express mongoose

MongoDB 设置

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

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

npm i nodemon

初始路线设置

photo-social-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/查看终点文本,如图 5-17 所示。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig17_HTML.jpg

图 5-17

初始路线

数据库用户和网络访问

在 MongoDB 中,您需要创建一个数据库用户并授予网络访问权限。该过程与第一章中的解释相同。遵循这些说明,然后获取用户凭证和连接 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.giruc.mongodb.net/photoDB?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 模式和路由

让我们为帖子创建一个模型。在photo-social-backend文件夹中创建一个postModel.js文件。

首先,用需要传递的参数创建一个模式,然后导出它。

import mongoose from 'mongoose'

const postsModel = mongoose.Schema({
    caption: String,
    user: String,
    image: String
})

export default mongoose.model('posts', postsModel)

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

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

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

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

import express from 'express'
import mongoose from 'mongoose'
import Posts from './postModel.js'
...

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

app.post('/upload', (req, res) => {
    const dbPost = req.body
    Posts.create(dbPost, (err, data) => {
        if(err)
            res.status(500).send(err)
        else
            res.status(201).send(data)
    })
})

app.get('/sync', (req, res) => {
    Posts.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}`))

在处理 POST 请求之前,您需要完成两件事情。第一,实行 First 否则,当您稍后部署应用时,会出现跨来源错误。打开终端,在photo-social-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 Posts from './postModel.js'

...

//Middleware
app.use(express.json())
app.use(Cors())

...

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

之后,点击正文然后选择 raw 。从下拉菜单切换到 JSON(应用/json) 。在文本编辑器中,输入如图 5-18 所示的数据。要改变的一件事是通过给键加上双引号来使数据 JSON。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig18_HTML.jpg

图 5-18

邮递员邮件

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

我以类似的方式插入了其他数据。您需要测试 GET /sync端点。将请求更改为 GET,然后单击发送按钮。如果一切正确,你得到状态:200 OK ,如图 5-19 所示。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig19_HTML.jpg

图 5-19

邮递员得到

有时在发布请求时,服务器会出错。错误为UnhandledPromiseRejectionWarning:MongooseServerSelectionError:connection

如果您遇到此错误,请转到您的网络访问选项卡,并点击添加 IP 地址按钮。之后点击添加当前 IP 地址按钮,点击确认,如图 5-20 所示。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig20_HTML.jpg

图 5-20

添加当前 IP

将后端与前端集成在一起

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

npm i axios

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

import axios from 'axios'

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

export default instance

ImageUpload.js文件中,从 Firebase 和 Axios 导入存储。更新handleUpload(),点击上传按钮后触发。

首先,在uploadTask变量中取上传的图片路径,放入数据库。检查state_changed因为快照改变了。根据上传的 has 数量,更新setProgress中的进度条。

之后,你需要做错误管理。从 Firebase 获取图像 URL。

接下来,获取标题、用户名和 URL,并在 MongoDB 中执行axios.post/upload的操作。

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

...
import { storage } from "../firebase";
import axios from '../axios'
const ImageUpload = ({ username }) => {
    ...
    const [url, setUrl] = useState("");
    const handleChange = e => {...}
    const handleUpload = () => {
        const uploadTask = storage.ref(`img/${image.name}`).put(image);
        uploadTask.on(
            "state_changed",
            (snapshot) => {
                const progress = Math.round(
                    (snapshot.bytesTransferred / snapshot.totalBytes) * 100
                );
                setProgress(progress);
            },
            (error) => {
                console.log(error);
            },
            () => {
                storage
                    .ref("images")
                    .child(image.name)
                    .getDownloadURL()
                    .then((url) => {
                        setUrl(url);
                        axios.post('/upload', {
                            caption: caption,
                            user: username,
                            image: url
                        })
                        setProgress(0);
                        setCaption("");
                        setImage(null);
                    });
            }
        );
    };
    return (...)
}

export default ImageUpload

在测试之前,您需要在 Firebase 控制台中设置存储。首先点击存储选项卡,然后点击开始按钮,弹出如图 5-21 所示的窗口。然后,点击下一个按钮。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig21_HTML.jpg

图 5-21

燃料库

在下一个画面中,点击完成按钮,如图 5-22 所示。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig22_HTML.jpg

图 5-22

云存储

进入本地主机,上传任何图片,输入标题,点击上传按钮。你可以看到帖子被保存到 MongoDB(见图 5-23 )。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig23_HTML.jpg

图 5-23

蒙戈布省省市镇

App.js中,你需要从 MongoDB 中获取帖子。首先,导入本地axios。然后创建一个新的useEffect钩子,并向/sync端点发出 GET 请求。

接下来,用从 MongoDB 收到的数据更新App.js

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

...
import axios from './axios'
...
function App() {
  ...
  const fetchPosts = async () => {
    await axios.get("/sync").then(response => setPosts(response.data))
  }
  useEffect(() => {
    fetchPosts()
  },[])
  ...
  return (
    <div className="app">
        ...
        <div className="app__posts">
          {posts.map(post => (
            <Post
              key={post._id}
              username={post.user}
              caption={post.caption}
              imageUrl={post.image}
            />
          ))}
        </div>

        ...
    </div>
  );
}

export default App;

图 5-24 显示了来自本地主机上的 MongoDB 数据库的 post。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig24_HTML.jpg

图 5-24

来自 MongoDB 的帖子

配置推动器

既然 MongoDB 不是实时数据库,那就该给 app 加一个 pusher 来获取实时数据了。因为你已经完成了第四章的设置,按照同样的说明,创建一个名为的应用。

将推杆添加到后端

同样,您需要停止服务器并安装 Pusher。在photo-social-backend文件夹中,用下面的命令安装它。

npm i pusher

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

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

...
import Pusher from 'pusher'
...
//App Config
...
const pusher = new Pusher({
    appId: "11xxxx",
    key: "9exxxxxxxxxxxxx",
    secret: "b7xxxxxxxxxxxxxxx",
    cluster: "ap2",
    useTLS: true
});

//API Endpoints
mongoose.connect(connection_url, {  ...})

mongoose.connection.once('open', () => {
    console.log('DB Connected')
    const changeStream = mongoose.connection.collection('posts').watch()
    changeStream.on('change', change => {
        console.log(change)
        if(change.operationType === "insert") {
            console.log('Trigerring Pusher')
            pusher.trigger('posts','inserted', {
                change: change
           })
        } 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}`))

为了测试这一点,您需要从前端上传一个新的图像。同时,你需要在调试控制台中推料。

图 5-25 显示了调试控制台日志中显示的消息。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig25_HTML.jpg

图 5-25

推进计程仪

将推杆添加到前端

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

npm i pusher-js

www.pusher.com 获取代码放入 app 前端。导入推动器,然后使用App.js文件中的代码,这里有一个新的用于推动器的useEffect()钩。更新的内容用粗体标记。

...
import Pusher from 'pusher-js'

const pusher = new Pusher('56xxxxxxxxxxxxxxxx', {
  cluster: 'ap2'
});

function App() {
  ...
  const fetchPosts = async () => {
    await axios.get("/sync").then(response => setPosts(response.data))
  }

  useEffect(() => {
    const channel = pusher.subscribe('posts');
    channel.bind('inserted', (data) => {
      fetchPosts()
    });
  }, [])

  useEffect(() => {
    fetchPosts()
  },[])
  ...
  return (
    <div className="app">
      ...
    </div>
  );
}

export default App;

去找邮递员并发送另一个邮寄请求。您可以在本地主机上看到控制台日志中的数据。应用已完成。无论何时你发布了什么,它都会实时显示出来。

隐藏秘密

您可以在将应用部署到 Heroku 或推送到 GitHub 之前隐藏秘密,这是一种最佳做法。使用以下命令将dotenv安装到photo-social-backend文件夹中。

npm i dotenv

然后在photo-social-backend文件夹中创建一个.env文件,并将所有秘密添加到其中。

DB_CONN='mongodb+srv://admin:<password>@cluster0.giruc.mongodb.net/photoDB?retryWrites=true&w=majority'
PUSHER_ID="11xxxx"
PUSHER_KEY="56xxxxxxxxxxxxxxxxxx"
PUSHER_SECRET="90xxxxxxxxxxxxxxxxxxx"

server.js中,导入dotenv,然后使用其中的值来代替所有的秘密。

...
import Posts from './postModel.js'
import dotenv from 'dotenv';

//App Config
dotenv.config()
const app = express()
const port = process.env.PORT || 9000
const connection_url = process.env.DB_CONN

const pusher = new Pusher({
    appId: process.env.PUSHER_ID,
    key: process.env.PUSHER_KEY,
    secret: process.env.PUSHER_SECRET,
    cluster: "ap2",
    useTLS: true
});

//Middleware
...

在后端的.gitignore文件中添加.env文件。更新的内容用粗体标记。

node_modules
.env

将后端部署到 Heroku

转到 www.heroku.com 部署后端。按照第一章的步骤创建一个名为照片-社交-后台的应用。

由于这次您有环境变量,您必须将它们添加到设置➤配置变量中。请注意,不要在按键周围加上任何引号,如图 5-26 所示。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig26_HTML.jpg

图 5-26

Heroku 的环境变量

部署成功后,进入 https://photo-social-backend.herokuapp.com 。图 5-27 显示了正确的文本。

img/512020_1_En_5_Chapter/512020_1_En_5_Fig27_HTML.jpg

图 5-27

后端已部署

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

import axios from 'axios'
const instance = axios.create({

    baseURL: " https://photo-social-backend.herokuapp.com "
})

export default instance

将前端部署到 Firebase

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

img/512020_1_En_5_Chapter/512020_1_En_5_Fig28_HTML.jpg

图 5-28

最终应用

摘要

在这一章中,你创建了一个简单而实用的基于照片的社交网络。Firebase 在网上主办的。您学习了添加电子邮件身份验证,通过它您可以使用电子邮件登录。您还了解了如何在 Firebase 中存储图像,以及如何使用 Node.js 创建的 API 路由在 MongoDB 数据库中存储图像和文章的链接。