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

MERN 项目初学者指南(全)

原文:MERN Projects for Beginners

协议:CC BY-NC-SA 4.0

一、MERN 部署设置

欢迎来到面向初学者的 MERN 项目,在这里你将学习使用 MERN (MongoDB,Express,React,Node.js)框架构建令人敬畏的 web 应用。这种堆栈在创业领域有很高的需求,因为你可以用它来制作一个全功能的 web 应用。一个懂 HTML、CSS、React 的前端工程师,可以很快学会 Node.js 和 MongoDB,构建一个完全量产就绪的 web app。

在本书中,您将学习如何在 Heroku 中使用 Node.js 代码托管后端。前端站点使用 React 代码和 Firebase 托管。它还通过一个名为 MongoDB Atlas 的云数据库托管。在接下来的五章中,大多数主机设置都是相同的,所以在大多数章节中不会重复。

MERN 堆栈一览

在安装 Firebase 之前,让我们讨论一下 MERN 堆栈中涉及的基础技术。

  • MongoDB 是一个基于 NoSQL 数据库的开源文档。它不同于将数据存储在表中的传统关系数据库。它将数据存储在类似 JSON 的文档中。它具有高度的可扩展性和性能导向性,因此适合现代网络应用。

  • React 是最流行的开源 JavaScript 库,用于构建网站或 web 应用的前端或用户界面。它由脸书开发和维护。

  • Node.js 允许开发者使用 JavaScript 编写服务器端代码。它与前端的 React 或 Angular 以及数据库的 MongoDB 集成得非常好。

  • Express 是 Node.js 的一个框架,通过它可以创建 API 端点,这些端点是任何后端服务器端代码的基础。

Firebase 托管初始设置

你需要一个谷歌账户才能使用 Firebase。进入 https://firebase.google.com ,点击进入右上角的控制台。你必须登录你的谷歌账户,如图 1-1 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig1_HTML.jpg

图 1-1

Firebase 控制台标题

点击页面中的添加项目链接,如图 1-2 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig2_HTML.jpg

图 1-2

添加项目

在此页面中,将项目命名为 dating-app-mern ,然后点击继续按钮,如图 1-3 所示。请注意,这只是一个安装说明。你将在下一章开始构建应用。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig3_HTML.jpg

图 1-3

应用名称

在下一页面中,点击创建项目按钮,如图 1-4 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig4_HTML.jpg

图 1-4

创建项目

创建项目需要一些时间,如图 1-5 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig5_HTML.jpg

图 1-5

项目已创建

MongoDB 设置

MongoDB 是您在云上使用的数据库。它也被称为 MongoDB Atlas。这比在本地机器上设置更容易操作。进入 www.mongodb.com 并登录或创建新账户。

创建新项目

登录后,您会看到类似于图 1-6 所示的屏幕。点击新建项目按钮。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig6_HTML.jpg

图 1-6

MongoDB 新项目

将你的项目命名为 dating-app-mern ,然后点击下一步按钮,如图 1-7 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig7_HTML.jpg

图 1-7

项目名

在下一个屏幕上,点击创建项目按钮,如图 1-8 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig8_HTML.jpg

图 1-8

MongoDB 创建项目

在下一个屏幕上,点击建立集群按钮,如图 1-9 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig9_HTML.jpg

图 1-9

构建集群

在下一个屏幕上,选择自由层,如图 1-10 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig10_HTML.jpg

图 1-10

自由层

在下一个屏幕上,您需要选择要在其中创建数据库的 AWS 区域。(我选择孟买是因为我住在印度,这给了我低延迟。)之后,点击创建集群按钮,如图 1-11 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig11_HTML.jpg

图 1-11

选择区域

下一个屏幕显示集群已经创建,这需要时间。您可以返回并创建您的第一个 API 端点,如图 1-12 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig12_HTML.jpg

图 1-12

集群已创建

数据库用户和网络访问

在 MongoDB 中创建用户,点击数据库访问页签,然后点击添加新数据库用户按钮,如图 1-13 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig13_HTML.jpg

图 1-13

创建数据库用户

在下一个屏幕上,您需要输入用户名和密码,如图 1-14 所示。你必须记住这两点。接下来,向下滚动并点击添加用户按钮。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig14_HTML.jpg

图 1-14

添加用户

接下来,进入网络访问选项卡,点击添加 IP 地址按钮,如图 1-15 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig15_HTML.jpg

图 1-15

网络存取

在弹出的窗口中,点击允许从任何地方访问按钮,然后点击确认按钮,如图 1-16 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig16_HTML.jpg

图 1-16

允许访问

接下来,返回到集群选项卡,点击连接按钮,弹出如图 1-17 所示的窗口。单击连接您的应用选项卡。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig17_HTML.jpg

图 1-17

连接应用

点击复制按钮复制连接 URL,如图 1-18 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig18_HTML.jpg

图 1-18

连接字符串

将后端部署到 Heroku

完成后端代码后,进入 www.heroku.com 部署后端。登录你的 Heroku 账号,点击新建下拉菜单,然后点击新建 app 按钮,如图 1-19 所示。您也可以从命令行使用 Heroku CLI 来实现这一点,但这里不做介绍。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig19_HTML.jpg

图 1-19

英雄库登录

接下来命名 app,点击创建 app 按钮,如图 1-20 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig20_HTML.jpg

图 1-20

Heroku app name

下一个屏幕显示了部署您的应用的所有命令,但是您需要 Heroku CLI。点击链接,按照说明将其安装到您的操作系统上,如图 1-21 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig21_HTML.jpg

图 1-21

希律王的指示

运行backend文件夹中的heroku login命令。系统会询问您是否有权限打开浏览器。此命令要求您按任意键在浏览器中打开。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig22_HTML.jpg

图 1-22。

在这里,您可以使用您的凭证登录,如图 1-23 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig23_HTML.jpg

图 1-23

登录凭据

成功登录后,您会看到如图 1-24 所示的页面,您需要关闭该页面。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig24_HTML.jpg

图 1-24

关闭弹出窗口

您需要将代码从本地机器推送到 Heroku 存储库。现在您已经登录到您的帐户,您可以运行以下命令来连接 Heroku Git。

heroku git:remote -a dating-mern-backend

接下来,让我们运行熟悉的git命令来提交代码。Git 是一个跟踪文件变化的软件。这是软件开发中必须的。以下命令将代码添加到临时区域,然后提交代码。push命令将其推送到远程 Heroku 服务器。

git add .
git commit -m "backend code complete"
git push heroku master

安装完成后,点击打开 app 按钮,进入部署站点,如图 1-25 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig25_HTML.jpg

图 1-25

打开后端应用

将前端部署到 Firebase

在前端项目完成之后(在下一章中),您可以在 Firebase 中部署它。转到frontend文件夹,在终端中运行firebase login命令。如果是第一次运行,将会打开一个弹出窗口。接下来,运行firebase init命令。键入 Y 继续。

firebase login
firebase init

使用向下箭头键进入托管,如图 1-26 所示。按空格键选择它,然后按 Enter 键。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig26_HTML.jpg

图 1-26

安装ˌ使成形

选择使用已有项目,如图 1-27 所示,按回车键。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig27_HTML.jpg

图 1-27

现有项目

接下来选择正确的项目,在我这里是 dating-app-mern-453b1 ,如图 1-28 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig28_HTML.jpg

图 1-28

正确的项目

接下来选择公共目录,也就是build。下面这个问题问的是一个单页 app 回答。下一个问题是关于 GitHub 部署的;回答,如图 1-29 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig29_HTML.jpg

图 1-29

建设

接下来,运行frontend文件夹中的npm run build以获得最佳的生产版本。最后一个命令,firebase deploy,将项目部署到 Firebase。如果成功,该网站现在是活的,这将在接下来的章节中显示。

安装 Node.js 和 npm

如果您的系统上还没有安装 Node.js 和 npm(Node 包管理器),我们来看一下它们的安装。本书中的大部分代码都需要 Node.js 和 npm。React 前端代码也需要 Node.js,通过 npm,可以安装很多小型开源程序,为 React 和 Node.js 都增加了功能。

当您安装 Node.js 时,npm 也会自动安装在您的系统上。尽管 macOS 用户可以在互联网上找到类似的指南,但以下说明适用于基于 Windows 的系统。

在你的网页浏览器中,输入 https://nodejs.org/en/download/ ,点击 Windows Installer,如图 1-30 所示。同样,它还会安装 npm。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig30_HTML.jpg

图 1-30

Node.js installer(Node. js 安装程序)

默认情况下,下载的文件安装在您的下载文件夹中。点击它,然后点击运行按钮,如图 1-31 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig31_HTML.jpg

图 1-31

快动按钮

在 Node.js 安装弹出窗口中,点击下一步按钮,如图 1-32 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig32_HTML.jpg

图 1-32

Node.js 欢迎

点击接受最终用户许可协议,然后点击下一步按钮,如图 1-33 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig33_HTML.jpg

图 1-33

协议

接下来,我建议您使用图 1-34 所示的安装位置。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig34_HTML.jpg

图 1-34

安装位置

向导要求您选择一个包。保持默认设置,如图 1-35 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig35_HTML.jpg

图 1-35

默认包

接下来点击复选框,然后点击下一个按钮,如图 1-36 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig36_HTML.jpg

图 1-36

属国

然后点击安装按钮,如图 1-37 所示。

img/512020_1_En_1_Chapter/512020_1_En_1_Fig37_HTML.jpg

图 1-37

安装

安装完成后,运行以下命令检查版本并验证一切正常。

node –v
npm -v

摘要

在这一章中,我们学习了创建 MERN(MongoDB,Express,ReactJS,NodeJS)项目的所有不同技术。我们还学习了如何在不同的环境中部署它们,我们将在接下来的章节中使用它们。

二、使用 MERN 开发约会应用

欢迎来到第二章,在这里你将使用 MERN (MongoDB,Express,React,Node.js)框架构建一个约会应用。后端托管在 Heroku,前端站点使用 Firebase 托管。项目中的图标来自 Material-UI。

该 web 应用功能简单,是第一个 MERN 堆栈项目。部署在 Firebase 中的成品 app 的截图如图 2-1 所示。所有数据都来自 MongoDB 数据库,API 端点设置在 Node.js 中。

img/512020_1_En_2_Chapter/512020_1_En_2_Fig1_HTML.jpg

图 2-1

完成的应用

让我们先回顾一下 React 前端,然后转到后端。打开您的终端并创建一个dating-app-mern文件夹。在里面,使用 create-react-app 创建一个新的 app,名为 dating-app-frontend 。以下是完成此操作的命令。

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

Firebase 托管初始设置

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

React 基本设置

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

cd dating-app-frontend
npm start

接下来,让我们删除一些你不需要的文件。图 2-2 显示了该应用在 localhost 上的外观。

img/512020_1_En_2_Chapter/512020_1_En_2_Fig2_HTML.jpg

图 2-2

删除文件

让我们删除所有不必要的样板代码。index.js文件应该如下所示。

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

App.js只包含文字交友 App MERN 。来自App.css文件的所有内容都已被删除。

import './App.css';
function App() {
  return (
    <div className="app">
      <h1>Dating App MERN </h1>
    </div>
  );
}

export default App;

index.css中,更新 CSS,使margin: 0位于顶部。

* {
       margin: 0;
  }

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

img/512020_1_En_2_Chapter/512020_1_En_2_Fig3_HTML.jpg

图 2-3

初始应用

创建标题组件

让我们创建一个标题组件。首先,你必须安装 Material-UI ( https://material-ui.com ),它提供了图标。根据 Material-UI 文档,您需要进行两次 npm 安装。通过dating-app-frontend文件夹中的集成端子安装铁芯。

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

接下来,在src文件夹中创建一个components文件夹。在components文件夹中创建两个文件——Header.jsHeader.css—Header.js有三样东西:一个人物图标、一个徽标和一个论坛图标。该徽标来自项目的公共目录,默认情况下包含 React 徽标。

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

import React from 'react'
import './Header.css'
import PersonIcon from '@material-ui/icons/Person'
import IconButton from '@material-ui/core/IconButton'
import ForumIcon from '@material-ui/icons/Forum'
const Header = () => {
    return (
        <div className="header">
            <IconButton>
                <PersonIcon fontSize="large" className="header__icon" />
            </IconButton>
            <img className="header__logo" src="logo192.png" alt="header" />
            <IconButton>
                <ForumIcon fontSize="large" className="header__icon" />
            </IconButton>
        </div>
    )
}

export default Header

在本地主机上的App.js文件中包含Header组件。更新后的代码用粗体标记。

import './App.css';
import Header from './components/Header';

function App() {
  return (
    <div className="app">
      <Header  />
    </div>
  );
}

export default App;

Header.css文件包含以下内容,包括简单的样式,完成了头。

.header{
    display: flex;
    align-items: center;
    justify-content: space-between;
    z-index: 100;
    border-bottom: 1px solid #f9f9f9;
}

.header__logo{
    object-fit: contain;
    height: 40px;
}

.header__icon{
    padding: 20px;
}

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

img/512020_1_En_2_Chapter/512020_1_En_2_Fig4_HTML.jpg

图 2-4

标题组件

创建约会卡组件

现在让我们来研究第二个部分。在components文件夹中创建两个文件DatingCards.jsDatingCards.css。然后将DatingCards组件包含在App.js文件中。更新后的代码用粗体标记。

import './App.css';
import Header from './components/Header';
import DatingCards from './components/DatingCards';
function App() {
  return (
    <div className="app">
      <Header  />
     < DatingCards />
    </div>
  );
}

export default App;

在继续之前,您需要安装一个react-tinder-card包。该包具有提供滑动效果的功能。

npm i react-tinder-card

接下来,将内容放入DatingCards.js。在这里,在一个people状态变量中,您存储了四个人的姓名和图像。接下来,导入DatingCard,并将其作为组件使用。这里,你使用react-tinder-card文档中提到的道具。

需要swipedoutOfFrame功能。当遍历每个人时,使用imgUrl背景图像并在h3标签中显示姓名。

import React, { useState } from 'react'
import DatingCard from 'react-tinder-card'
import './DatingCards.css'
const DatingCards = () => {
    const [people, setPeople] = useState([
       { name: "Random Guy", imgUrl: "https://images.unsplash.com/photo-1520409364224-63400afe26e5?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=658&q=80" },
       { name: "Another Guy", imgUrl: "https://images.unsplash.com/photo-1519085360753-af0119f7cbe7?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=634&q=80" },
       { name: "Random Girl", imgUrl: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=634&q=80" },
       { name: "Another Girl", imgUrl: "https://images.unsplash.com/photo-1529626455594-4ff0802cfb7e?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=634&q=80" }
 ])
    const swiped = (direction, nameToDelete) => {
        console.log("receiving " + nameToDelete)
    }
    const outOfFrame = (name) => {
        console.log(name + " left the screen!!")
    }
    return (
        <div className="datingCards">
            <div className="datingCards__container">
                {people.map((person) => (
                    <DatingCard
                        className="swipe"
                        key={person.name}
                        preventSwipe={['up', 'down']}
                        onSwipe={(dir) => swiped(dir, person.name)}
                        onCardLeftScreen={() => outOfFrame(person.name)} >
                        <div style={{ backgroundImage: `url(${person.imgUrl})`}} className="card">
                            <h3>{person.name}</h3>
                        </div>
                    </DatingCard>
                ))}
            </div>
        </div>
    )
}

export default DatingCards

Localhost 显示了四个“人”,如图 2-5 所示,但是您需要设计所有的样式。

img/512020_1_En_2_Chapter/512020_1_En_2_Fig5_HTML.jpg

图 2-5

所有人

DatingCards.css文件中添加第一个样式,并使datingCards__container成为 flexbox。接下来,将每张卡片设计成包含图片和其他东西的样式。请注意,您正在为每张卡片设置position: relative,这将使元素相对于自身偏移,并提供宽度和高度。

.datingCards__container{
    display: flex;
    justify-content: center;
    margin-top: 10vh;
}

.card{
    position: relative;
    background-color: white;
    width: 600px;
    padding: 20px;
    max-width: 85vw;
    height: 50vh;
    box-shadow: 0px 18px 53px 0px rgba(0, 0, 0, 0.3);
    border-radius: 20px;
    background-size: cover;
    background-position: center;
}

图 2-6 显示了这在本地主机上的样子。

img/512020_1_En_2_Chapter/512020_1_En_2_Fig6_HTML.jpg

图 2-6

图像出现

让我们再添加三个样式,从这个 swipe 中可以得到一个 card 类中的类。使用position: absolute创造滑动效果的魔力。在DatingCards.css文件中添加以下内容。

.swipe{
    position: absolute;
}
.cardContent{
    width: 100%;
    height: 100%;
}
.card h3{
    position: absolute;
    bottom: 0;
    margin: 10px;
    color: white;
}

前端基本完成,如图 2-7 所示。它包含右扫和左扫功能。除了包含滑动按钮的页脚之外,一切都完成了。

img/512020_1_En_2_Chapter/512020_1_En_2_Fig7_HTML.jpg

图 2-7

几乎完成

创建滑动按钮组件

现在让我们创建SwipeButtons组件,它是页脚中的按钮。这些按钮增加了应用的风格。因为这是一个简单的应用,所以它们不会起作用。在components文件夹中创建两个文件SwipeButtons.jsSwipeButtons.css。你还需要把它包含在App.js文件中。

更新的内容用粗体标记。

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

SwipeButtons.js文件的内容很简单。有五个来自 Material-UI 的图标包裹在IconButton里面。

import React from 'react'
import './SwipeButtons.css'
import ReplayIcon from '@material-ui/icons/Replay'
import CloseIcon from '@material-ui/icons/Close'
import StarRateIcon from '@material-ui/icons/StarRate'
import FavoriteIcon from '@material-ui/icons/Favorite'
import FlashOnIcon from '@material-ui/icons/FlashOn'
import IconButton from '@material-ui/core/IconButton'
const SwipeButtons = () => {
    return (
        <div className="swipeButtons">
            <IconButton className="swipeButtons__repeat">
                <ReplayIcon fontSize="large" />
            </IconButton>
            <IconButton className="swipeButtons__left">
                <CloseIcon fontSize="large" />
            </IconButton>
            <IconButton className="swipeButtons__star">
                <StarRateIcon fontSize="large" />
            </IconButton>
            <IconButton className="swipeButtons__right">
                <FavoriteIcon fontSize="large" />
            </IconButton>
            <IconButton className="swipeButtons__lightning">
                <FlashOnIcon fontSize="large" />
            </IconButton>
        </div>
    )
}
export default SwipeButtons

接下来,在SwipeButtons.css文件中设置按钮的样式。首先,设计swipeButtons类的样式,并使用position: fixed使其灵活。在一个固定的位置,一个元素保持附着在指定的位置(在这个例子中是底部),甚至当用户滚动时。您还设计了由包创建的MuiIconButton-root类的样式。

SwipeButtons.css文件中,用不同的颜色设计每个按钮。

.swipeButtons{
    position: fixed;
    bottom: 10vh;
    display: flex;
    width: 100%;
    justify-content: space-evenly;
}

.swipeButtons .MuiIconButton-root{
    background-color: white;
    box-shadow: 0px 10px 53px 0px rgba(0, 0, 0, 0.3) !important;
}

.swipeButtons__repeat{
    padding: 3vw !important;
    color: #f5b748 !important;
}

.swipeButtons__left{
    padding: 3vw !important;
    color: #ec5e6f !important;
}

.swipeButtons__star{
    padding: 3vw !important;
    color: #62b4f9 !important;
}

.swipeButtons__right{
    padding: 3vw !important;
    color: #76e2b3 !important;
}

.swipeButtons__lightning{
    padding: 3vw !important;
    color: #915dd1 !important;
}

图 2-8 显示了本地主机上的项目。

img/512020_1_En_2_Chapter/512020_1_En_2_Fig8_HTML.jpg

图 2-8

前端完成

初始后端设置

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

mkdir dating-app-backend
cd dating-app-backend
git init

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

img/512020_1_En_2_Chapter/512020_1_En_2_Fig9_HTML.jpg

图 2-9

后端初始设置

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

node_modules

接下来,打开package.json."type" : "module"需要在 Node.js 中启用类似 React 的导入,这些模块被称为 ECMA 模块。带有 require 语句的初始模块称为 CommonJS 模块。你可以在 https://blog.logrocket.com/how-to-use-ecmascript-modules-with-node-js/ 了解更多。

您还需要包含一个启动脚本来运行server.js文件。更新的内容用粗体标记。

{
  "name": "dating-app-backend",
  "version": "1.0.0",
  "description": "The dating 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"
}

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

npm i express mongoose

MongoDB 设置

MongoDB 的设置与第一章中描述的相同。你需要遵循它并创建一个名为的新项目。

在继续之前,将nodemon安装在dating-app-backend文件夹中。每当您对server.js文件中的代码进行任何更改时,Node 服务器都会立即重启。

npm i nodemon

初始路线设置

让我们创建初始路由,它通常检查是否一切都设置正确。Node.js 中的 Express 包允许您创建路由,这是大多数互联网的工作方式。大多数后端语言,如 Node.js、Java,都提供了创建这些与数据库交互的路由的功能。初始路由不与数据库交互,只是在您使用 GET 请求访问它时返回一个文本。在dating-app-backend文件夹中创建一个server.js文件。在这里,您首先导入 Express 和 Mongoose 包。接下来,使用 Express 创建一个在端口 8001 上运行的port变量。

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

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

import express from 'express'
import mongoose from 'mongoose'

//App Config
const app = express()
const port = process.env.PORT || 8001

//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: 8001 控制台日志。为了检查路线是否正常工作,转到http://localhost:8001/查看终点文本(见图 2-10 )。

img/512020_1_En_2_Chapter/512020_1_En_2_Fig10_HTML.jpg

图 2-10

初始路线

数据库用户和网络访问

在 MongoDB 中,您需要创建一个数据库用户并提供网络访问。该过程与第一章中的过程相同。按照这些说明,获取用户凭证和连接 URL。

server.js中,创建一个connection_url变量,并将 URL 粘贴到从 MongoDB 获得的字符串中。输入您之前保存的密码,并提供一个数据库名称。更新后的代码用粗体标记。

...
//App Config
const app = express()
const port = process.env.PORT || 8001
const connection_url = 'mongodb+srv://admin:yourpassword@cluster0.lggjc.mongodb.net/datingDB?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 以 JSON 格式存储数据,而不是像 Oracle 这样的传统数据库中的常规表结构。您创建了 MongoDB 所需的模式文件。它告诉你如何在 MongoDB 中存储字段。

这里,cards被认为是一个集合名,您在数据库中存储一个类似于cardSchema的值。它由一个有名字的对象和imgUrl键组成。这些是您在 MongoDB 中使用的名称。创建一个dbCards.js文件,将以下内容放入其中。

import mongoose from 'mongoose'
const cardSchema = mongoose.Schema({
    name: String,
    imgUrl: String
})

export default mongoose.model('cards', cardSchema)

现在,您可以使用该模式来创建向数据库添加数据的端点。这里遵循 MVC 模式;这是 web 应用的传统流程。点击 https://medium.com/createdd-notes/understanding-mvc-architecture-with-react-6cd38e91fefd 了解更多信息。

接下来,使用一个 POST 请求,从用户那里获取任何数据,并将其发送到数据库。您可以使用任何端点。例如,如果你写了一篇关于脸书的文章并点击了 POST 按钮,那么一旦发出 POST 请求,你的文章就会被保存在脸书数据库中。

GET 端点从数据库中获取所有数据。同样,你可以给出任何端点。例如,当您浏览脸书的提要时,一个 GET 请求被发送到端点,端点又从脸书数据库获取所有的帖子。

server.js,中,创建一个到/dating/cards端点的 POST 请求。负载在req.body到 MongoDB。然后你用create()dbCard。如果成功,您会收到状态 201;否则,您会收到状态 500。更新的内容用粗体标记。

接下来,创建/dating/cards的 GET 端点,从数据库中获取数据。您在这里使用find(),如果成功,将收到状态 200(否则,状态 500)。更新的内容用粗体标记。

import express from 'express'
import mongoose from 'mongoose'
import Cards from './dbCards.js'
...

//API Endpoints
app.get("/", (req, res) => res.status(200).send("Hello TheWebDev"))
app.post('/dating/cards', (req, res) => {
    const dbCard = req.body
    Cards.create(dbCard, (err, data) => {
        if(err) {
            res.status(500).send(err)
        } else {
            res.status(201).send(data)
        }
    })
})

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

要查看路线,让我们使用邮递员应用。下载并安装它。

http://localhost:8001发送 GET 请求,检查它是否在 Postman 中工作,如图 2-11 所示。

img/512020_1_En_2_Chapter/512020_1_En_2_Fig11_HTML.jpg

图 2-11

初始路线检查

在处理 POST 请求之前,您需要完成两件事情。第一,实行 First 否则,当您稍后部署应用时,会出现跨来源错误。CORS(跨源资源共享)是限制从一个域访问另一个域的机制。假设你在http://example.com上,想访问 http://mybank.com/accountdetails 。CORS 不会允许你这么做的。只有 http://mybank.com 允许与http://example.com跨原点共享时才允许。

打开终端,在dating-app-backend文件夹中安装 CORS。

npm i cors

server.js中,导入 CORS 并与app.use()一起使用。你还需要使用express.json()中间件。它是必需的,因为您需要它来解析来自 MongoDB 的传入 JSON 对象以读取主体。

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

import express from 'express'
import mongoose from 'mongoose'
import Cors from 'cors'
import Cards from './dbCards.js'

...

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

...

在 Postman 中,将请求更改为 POST,然后添加http://localhost:8001/dating/cards端点。

接下来,点击身体,选择原始。从下拉菜单中选择 JSON(应用/json) 。在文本编辑器中,从DatingCards.js文件中复制数据。通过在关键字中添加双引号来生成数据 JSON。

接下来,点击发送按钮。如果一切正确,您将获得状态:201 已创建(见图 2-12 )。

img/512020_1_En_2_Chapter/512020_1_En_2_Fig12_HTML.jpg

图 2-12

邮寄路线

您需要测试 GET 端点。将请求更改为 GET,然后单击发送按钮。如果一切正常,你得到状态:200 OK (见图 2-13 )。

img/512020_1_En_2_Chapter/512020_1_En_2_Fig13_HTML.jpg

图 2-13

获取路线

将后端与前端集成在一起

让我们把后端钩到前端。使用axios包从前端调用。Axios 是一个 JavaScript 库,它向 REST 端点发出 API 请求。您刚刚在后端创建了两个端点。要访问它们,你需要 Axios。打开dating-app-frontend文件夹并安装。

npm i axios

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

import axios from 'axios'
const instance = axios.create({
    baseURL: "http://localhost:8001"
})

export default instance

DatingCards.js,中,去掉处于people状态的硬编码内容。然后导入本地的axios并使用useEffect钩子对/dating/cards端点进行 API 调用。收到数据后,使用setPeople()功能将其复位。更新后的代码用粗体标记。

import React, { useState, useEffect } from 'react'
import DatingCard from 'react-tinder-card'
import './DatingCards.css'
import axios from './axios'

const DatingCards = () => {
    const [people, setPeople] = useState([])
    useEffect(() => {
        async function fetchData() {
            const req = await axios.get("/dating/cards")
            setPeople(req.data)
        }
        fetchData()
    }, [])

    const swiped = (direction, nameToDelete) => {
        console.log("receiving " + nameToDelete)
    }
...

http://localhost:3000/看数据。应用现已完成(见图 2-14 )。

img/512020_1_En_2_Chapter/512020_1_En_2_Fig14_HTML.jpg

图 2-14

应用完成

将后端部署到 Heroku

转到 www.heroku.com 部署后端。你按照第一章中的相同步骤创建了一个名为 dating-mern-backend 的应用。

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

import axios from 'axios'
const instance = axios.create({
    baseURL: https://dating-mern-backend.herokuapp.com
})

export default instance

将前端部署到 Firebase

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

img/512020_1_En_2_Chapter/512020_1_En_2_Fig15_HTML.jpg

图 2-15

部署的应用

摘要

在这一章中,我们在 MERN 堆栈中创建了一个约会应用。我们在 ReactJS 中构建前端,并在 Firebase 中托管它。后端构建在 NodeJS 中,托管在 Heroku 中。数据库是在 MongoDB 中构建的。

三、使用 MERN 打造短视频应用

欢迎来到您的下一个 MERN 项目,在这里您将使用 MERN (MongoDB,Express,React,Node.js)框架构建一个非常棒的短视频应用。后端在 Heroku 托管,前端站点使用 Firebase 托管。Material-UI ( https://material-ui.com )提供项目中的图标。

这个 web 应用显示存储在 MongoDB 中的短视频,点击它就可以播放。您可以通过再次点按它来暂停它。这款网络应用还具有非常平滑的垂直滚动功能,可以显示更多视频。在图 3-1 中,可以看到 app 最终部署的版本。

img/512020_1_En_3_Chapter/512020_1_En_3_Fig1_HTML.jpg

图 3-1

部署版本

首先使用 React,然后移动到后端。打开您的终端并创建一个short-video-mern文件夹。在里面,使用create-react-app创建一个名为短视频前端的新应用。以下是命令。

mkdir short-video-mern
cd short-video-mern
npx create-react-app short-video-frontend

Firebase 托管初始设置

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

React 基本设置

回到 React 项目,将cd转到short-video-frontend目录。用npm start启动 React 应用。

cd short-video-frontend
npm start

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

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

img/512020_1_En_3_Chapter/512020_1_En_3_Fig2_HTML.jpg

图 3-2

初始应用

创建视频组件

接下来,在src文件夹中创建一个components文件夹。在components文件夹中创建两个文件Video.jsVideo.css。在Video.js文件中,添加一个video标签和一个垂直视频链接。我在我的频道上使用了我的 YouTube 短视频的链接。

以下是Video.js内容。

import React from 'react'
import './Video.css'
const Video = () => {
    return (
        <div className="video">
            <video
                src="https://res.cloudinary.com/dxkxvfo2o/video/upload/v1608169738/video1_cvrjfm.mp4"
                className="video__player"
                loop
            >
            </video>
        </div>
    )
}
export default Video

在本地主机上的App.js文件中包含Video组件。更新后的代码用粗体标记。

import './App.css';
import Video from './components/Video';
function App() {
  return (
    <div className="app">
            <div className="app__videos">
                <Video />
                <Video />
            </div>
    </div>
  );
}

export default App;

接下来,将基本样式放在App.css文件中,包括用于scroll-snap-type的样式,它们是用于滚动的。你还需要让一切居中。接下来,为app__videos类添加一些样式并隐藏滚动条。

html{
    scroll-snap-type: y mandatory;
}

.app{
    height: 100vh;
    background-color: black;
    display: grid;
    place-items: center;
}

.app__videos{
    position:relative;
    height: 800px;
    border-radius: 20px;
    overflow: scroll;
    width: 80%;
    max-width: 500px;
    scroll-snap-type: y mandatory;
}

.app__videos::-webkit-scrollbar{
    display: none;
}

.app__videos{
    -ms-overflow-style: none;
    scrollbar-width: none;
}

图 3-3 显示了该应用在 localhost 上的外观。

img/512020_1_En_3_Chapter/512020_1_En_3_Fig3_HTML.jpg

图 3-3

显示的视频

您还需要设计Video.css文件中的videovideo__player类的样式。您在这里再次使用了scroll-snap-type

.video{
    position: relative;
    background-color: white;
    width: 100%;
    height:100%;
    scroll-snap-align: start;
}

.video__player{
    object-fit: fill;
    width: 100%;
    height: 100%;
}

捕捉特征完成。当你滚动时,它平稳地把你带到下一个视频,如图 3-4 所示。此外,通过 CSS,边缘在所有方面都变得完美。

img/512020_1_En_3_Chapter/512020_1_En_3_Fig4_HTML.jpg

图 3-4

捕捉特征

目前,视频无法播放。要让它们播放,必须使用一个引用(或 ref)。React 在虚拟 DOM 上工作。一般情况下,只需要在特殊情况下访问 DOM(文档对象模型),使用 refs 访问 DOM 元素。在这种情况下,您需要访问<video> HTML 元素,以便能够访问play()pause()属性,这些属性只能通过引用获得。

首先,导入useRefuseState钩子以获得videoRef变量,该变量在 video 元素中使用,在这里创建一个onClick处理程序来触发一个handleVideoPress函数。

handleVideoPress函数内部,用playing状态变量检查视频是否播放,然后用videoRef.current.pause()设置暂停,将播放状态改为 false。你在else区块做相反的动作。

更新后的Video.js内容以粗体标记。

import React , { useRef, useState } from 'react'
import './Video.css'

const Video = () => {
    const [playing, setPlaying] = useState(false)
    const videoRef = useRef(null)
    const handleVideoPress = () => {
        if(playing){
            videoRef.current.pause()
            setPlaying(false)
        } else {
            videoRef.current.play()
            setPlaying(true)
        }
    }
    return (
        <div className="video">
            <video
                src="https://res.cloudinary.com/dxkxvfo2o/video/upload/v1608169738/video1_cvrjfm.mp4"
                className="video__player"
                loop
                ref={videoRef}
                onClick={handleVideoPress}
            >
            </video>
        </div>
    )
}

export default Video

点击视频在本地主机上播放。再次点按它以暂停。

创建视频页脚组件

让我们处理第二个组件,它显示了用户名、视频标题和视频页脚中的滚动滚动条。

components文件夹中创建两个文件VideoFooter.jsVideoFooter.css。然后将VideoFooter组件包含在Video.js文件中。更新后的代码用粗体标记。

import React , { useRef, useState } from 'react'
import './Video.css'
import VideoFooter from './VideoFooter'

const Video = () => {
    ...
    return (
        <div className="video">
            <video
                src="https://res.cloudinary.com/dxkxvfo2o/video/upload/v1608169738/video1_cvrjfm.mp4"
                className="video__player"
                loop
                ref={videoRef}
                onClick={handleVideoPress}
            >
            </video>
            <VideoFooter />
        </div>
    )
}

export default Video

接下来,在VideoFooter.js文件中添加一个包含用户名的h3标签和一个包含描述的p标签。

import React from 'react'
import './VideoFooter.css'

const VideoFooter = () => {
    return (
        <div className="videoFooter">
            <div className="videoFooter__text">
                <h3>@nabendu82</h3>
                <p>Macbook Air to new Windows editing beast</p>
            </div>
        </div>
    )
}

export default VideoFooter

接下来,在VideoFooter.css文件中设置它们的样式。

.videoFooter{
    position: relative;
    color: white;
    bottom: 150px;
    margin-left: 40px;
    display: flex;
}

.videoFooter__text{
    flex: 1;
}

.videoFooter__text > h3{
    padding-bottom: 20px;
}

.videoFooter__text > p{
    padding-bottom: 20px;
}

图 3-5 显示了本地主机上的文本。

img/512020_1_En_3_Chapter/512020_1_En_3_Fig5_HTML.jpg

图 3-5

初始页脚

让我们首先安装 Material-UI,它提供了图标。根据 Material-UI 文档进行两次 npm 安装。通过short-video-frontend文件夹中的集成端子安装铁芯。

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

是时候在VideoFooter.js文件中使用了。在videoFooter__ticker div 中包含音符图标MusicNoteIcon,它是从 Material-UI 导入的。

更新的内容用粗体标记。

import React from 'react'
import './VideoFooter.css'
import MusicNoteIcon from '@material-ui/icons/MusicNote'

const VideoFooter = () => {
    return (
        <div className="videoFooter">
            <div className="videoFooter__text">
                <h3>@nabendu82</h3>
                <p>Macbook Air to new Windows editing beast</p>
                <div className="videoFooter__ticker">
                    <MusicNoteIcon className="videoFooter__icon" />
                </div>
            </div>
        </div>
    )
}

export default VideoFooter

这个项目的特色是一个漂亮的跑马灯。为此,您在short-video-frontend文件夹中安装一个名为react-ticker的包。

npm i react-ticker

接下来,在VideoFooter.js文件中包含文档中的股票代码和唱片(或旋转光盘)图像。正如你在新闻频道底部看到的,滚动条在屏幕上移动文本。还显示了一个录制/旋转的光盘图像,您可以很快在其中添加漂亮的动画。

更新的内容用粗体标记。

import React from 'react'
import './VideoFooter.css'
import MusicNoteIcon from '@material-ui/icons/MusicNote'
import Ticker from 'react-ticker'

const VideoFooter = () => {
    return (
        <div className="videoFooter">
            <div className="videoFooter__text">
                <h3>@nabendu82</h3>
                <p>Macbook Air to new Windows editing beast</p>
                <div className="videoFooter__ticker">
                    <MusicNoteIcon className="videoFooter__icon" />
                    <Ticker mode="smooth">
                        {({ index }) => (
                          <>

                            <p>I am a Windows PC</p>
                          </>

                        )}
                    </Ticker>
                </div>
            </div>
            <img className="videoFooter__record" src="https://static.thenounproject.com/png/934821-200.png" alt="video footer" />
        </div>
    )
}

export default VideoFooter

接下来,在VideoFooter.css文件中为滚动条和录制的图像添加样式。在这里,您将滚动条与音乐图标对齐,并添加动画来移动录制的图像。

将以下内容添加到VideoFooter.css文件中。

.videoFooter__icon{
    position: absolute;
}

.videoFooter__ticker > .ticker{
    height: fit-content;
    margin-left: 30px;
    width: 60%;
}

.videoFooter__record{
    animation: spinTheRecord infinite 5s linear;
    height: 50px;
    filter: invert(1);
    position: absolute;
    bottom: 0;
    right: 20px;
}

@keyframes spinTheRecord {
    from {
        transform: rotate(0deg)
    }
    to {
        transform: rotate(360deg)
    }
}

图 3-6 显示了 localhost 上的页脚组件,包括一个滚动滚动条和旋转圆盘。

img/512020_1_En_3_Chapter/512020_1_En_3_Fig6_HTML.jpg

图 3-6

页脚完成

创建视频侧栏组件

现在让我们创建一个侧边栏组件,它在视频的右侧显示图标。

components文件夹中创建两个文件VideoSidebar.jsVideoSidebar.css。您还需要包含Video.js文件。更新后的代码用粗体标记。

import React , { useRef, useState } from 'react'
import './Video.css'
import VideoFooter from './VideoFooter'
import VideoSidebar from './VideoSidebar'

const Video = () => {
    ...
    return (
        <div className="video">
            <video
                src="https://res.cloudinary.com/dxkxvfo2o/video/upload/v1608169738/video1_cvrjfm.mp4"
                className="video__player"
                loop
                ref={videoRef}
                onClick={handleVideoPress}
            >
            </video>
            <VideoFooter />
            <VideoSidebar />
        </div>
    )
}

export default Video

接下来,更新VideoSidebar.js文件。这里,你使用了不同的材质界面图标。您还可以使用一个状态变量来保存 like 图标是否被按下;如果是这样,它会从空心图标变为实心图标,并且计数也会改变。

import React, { useState } from 'react'
import './VideoSidebar.css'
import FavoriteIcon from '@material-ui/icons/Favorite'
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder'
import MessageIcon from '@material-ui/icons/Message'
import ShareIcon from '@material-ui/icons/Share'

const VideoSidebar = () => {
    const [liked, setLiked] = useState(false)
    return (
        <div className="videoSidebar">
            <div className="videoSidebar__button">
                { liked ? <FavoriteIcon fontSize="large" onClick={e => setLiked(false)} /> : <FavoriteBorderIcon fontSize="large" onClick={e => setLiked(true)} /> }
                <p>{liked ? 101 : 100}</p>
            </div>
            <div className="videoSidebar__button">
                <MessageIcon fontSize="large" />
                <p>345</p>
            </div>
            <div className="videoSidebar__button">
                <ShareIcon fontSize="large" />
                <p>109</p>
            </div>
        </div>
    )
}

export default VideoSidebar

接下来,更新VideoSidebar.css文件。

.videoSidebar{
    position: absolute;
    top: 50%;
    right: 10px;
    color: white;
}

.videoSidebar__button{
    padding: 20px;
    text-align: center;
}

图 3-7 展示了这些可爱的图标,视频侧边栏就做好了。

img/512020_1_En_3_Chapter/512020_1_En_3_Fig7_HTML.jpg

图 3-7

侧栏已完成

使组件动态化

来自App.js文件的所有数据被传递给子组件。您使组件成为动态的,以便可以向它们传递道具。像在 React 中一样,使用 props 将数据从父组件传递到子组件。视频侧边栏是第一个要处理的组件。在VideoSidebar.js中,传递数字作为道具。

更新的内容用粗体标记。

...
const VideoSidebar = ({ likes, shares, messages }) => {
    const [liked, setLiked] = useState(false)
    return (
        <div className="videoSidebar">
            <div className="videoSidebar__button">
                { liked ? <FavoriteIcon fontSize="large" onClick={e => setLiked(false)} /> : <FavoriteBorderIcon fontSize="large" onClick={e => setLiked(true)} /> }
                <p>{liked ? likes + 1 : likes }</p>
            </div>
            <div className="videoSidebar__button">
                <MessageIcon fontSize="large" />
                <p>{messages}</p>
            </div>
            <div className="videoSidebar__button">
                <ShareIcon fontSize="large" />
                <p>{shares}</p>
            </div>
        </div>
    )
}

export default VideoSidebar

同样,在VideoFooter.js文件中传递字符串作为道具。

更新的内容用粗体标记。

...
const VideoFooter = ({ channel, description, song }) => {
    return (
        <div className="videoFooter">
            <div className="videoFooter__text">
                <h3>@{channel} </h3>
                <p>{description}</p>
                <div className="videoFooter__ticker">
                    <MusicNoteIcon className="videoFooter__icon" />
                    <Ticker mode="smooth">
                        {({ index }) => (
                          <>

                            <p>{song}</p>
                          </>

                        )}
                    </Ticker>
                </div>
            </div>
            <img className="videoFooter__record" src="https://static.thenounproject.com/png/934821-200.png" alt="video footer" />
        </div>
    )
}

export default VideoFooter

您希望进一步从应用组件钻取道具,以获得不同的视频文件。让我们将这些道具添加到Video.js文件中并使用它们。

更新的内容用粗体标记。

...

const Video = ({ url, channel, description, song, likes, shares, messages }) => {
   ...
    return (
        <div className="video">
            <video
                src={url}
                className="video__player"
                loop
                ref={videoRef}
                onClick={handleVideoPress}
            >
            </video>
            <VideoFooter channel={channel} description={description} song={song}  />
            <VideoSidebar likes={likes} shares={shares} messages={messages}  />
        </div>
    )
}

export default Video

App.js中,你通过所有的道具,可以通过两个不同的视频。

更新的内容用粗体标记。

...
function App() {
  return (
    <div className="app">
      <div className="app__videos">
        <Video
          url="https://res.cloudinary.com/dxkxvfo2o/video/upload/v1608169738/video1_cvrjfm.mp4"
          channel="nabendu82"
          description="Macbook Air to new Windows editing beast"
          song="I am a Windows PC"
          likes={345}
          shares={200}
          messages={90}
        />
        <Video
          url="https://res.cloudinary.com/dxkxvfo2o/video/upload/v1608169739/video2_mecbdo.mp4"
          channel="thewebdev"
          description="Tuesday morning editing on kdenlive in Windows"
          song="Kdenlive is great"
          likes={445}
          shares={290}
          messages={109}
        />
      </div>
    </div>
  );
}
export default App;

前端完成了,该开始后端了。

初始后端设置

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

mkdir short-video-backend
cd short-video-backend
git init

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

img/512020_1_En_3_Chapter/512020_1_En_3_Fig8_HTML.jpg

图 3-8

初始服务器设置

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

node_modules

接下来,打开package.json."type" : "module"需要在 Node.js 中启用类似 React 的导入,包括一个启动脚本来运行server.js文件。

更新的内容用粗体标记。

{
  "name": "short-video-backend",
  "version": "1.0.0",
  "description": " The short video 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"
}

在开始之前,您需要安装两个软件包。打开终端,在short-video-backend文件夹中安装 Express 和 Mongoose。正如第二章所讨论的,Express 是 Node.js 框架,通过它你可以轻松构建后端代码。Mongoose 是绑定 Node.js 和 MongoDB 所需的库,因此它是负责在 Node.js 代码中创建模式的桥梁。

npm i express mongoose

MongoDB 设置

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

在继续之前,将nodemon安装在short-video-backend文件夹中。它帮助server.js中的变化瞬间重启 Node 服务器。

npm i nodemon

初始路线设置

接下来,在short-video-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/查看端点文本,如图 3-9 所示。

img/512020_1_En_3_Chapter/512020_1_En_3_Fig9_HTML.jpg

图 3-9

本地主机

数据库用户和网络访问

在 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:yourpassword@cluster0.ryj4g.mongodb.net/shortVideoDB?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 中的存储方式。在short-video-backend文件夹中创建一个dbModel.js文件。

这里,shortVideos被认为是一个集合名,您在数据库中存储一个类似于shortVideoSchema的值。它由一个带有 URL、频道、描述、歌曲、喜欢、共享和消息键的对象组成。

import mongoose from 'mongoose'
const shortVideoSchema = mongoose.Schema({
    url: String,
    channel: String,
    description: String,
    song: String,
    likes: String,
    shares: String,
    messages: String
})

export default mongoose.model('shortVideos', shortVideoSchema)

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

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

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

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

import express from 'express'
import mongoose from 'mongoose'
import Videos from './dbModel.js'
...

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

app.post('/v2/posts', (req, res) => {
    const dbVideos = req.body
    Videos.create(dbVideos, (err, data) => {
        if(err)
            res.status(500).send(err)
        else
            res.status(201).send(data)
    })
})

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

为了检查路线,让我们使用真棒邮递员应用。向http://localhost:9000发送 GET 请求,检查它是否在 Postman 中工作(见图 3-10 )。

img/512020_1_En_3_Chapter/512020_1_En_3_Fig10_HTML.jpg

图 3-10

获取请求

在处理 POST 请求之前,您需要完成两件事情。首先,实施 CORS。打开终端,在short-video-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 Videos from './dbModel.js'

...

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

...

在 Postman 中,将请求更改为 POST,然后添加http://localhost:9000/v2/posts端点。

接下来,点击身体,选择原始。从下拉菜单中选择 JSON(应用/json) 。在文本编辑器中,从App.js文件中复制数据。通过在关键字中添加双引号来生成数据 JSON。

然后,点击发送按钮。如果一切正确,你得到状态:201 已创建,如图 3-11 所示。

img/512020_1_En_3_Chapter/512020_1_En_3_Fig11_HTML.jpg

图 3-11

成功消息发布

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

img/512020_1_En_3_Chapter/512020_1_En_3_Fig12_HTML.jpg

图 3-12

成功消息获取

将后端与前端集成在一起

让我们用axios包把后端钩到前端。打开short-video-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钩子对/v2/posts端点进行 API 调用。一旦收到数据,使用setVideos()将其存储在videos状态变量中。

在 return 语句中,去掉硬编码的东西。之后,映射视频数组,并将道具传递给视频组件。

更新的内容用粗体标记。

import React, { useState, useEffect } from 'react';
import './App.css';
import Video from './components/Video';
import axios from './components/axios';

function App() {
  const [videos, setVideos] = useState([])
  useEffect(() => {
    async function fetchData() {
        const res = await axios.get("/v2/posts")
        setVideos(res.data)
        return res
    }
    fetchData()
  }, [])

  return (
    <div className="app">
      <div className="app__videos">
        {videos.map(({ url, channel, description, song, likes, shares, messages }) => (
            <Video
              key={url}
              url={url}
              channel={channel}
              description={description}
              song={song}
              likes={likes}
              shares={shares}
              messages={messages}
            />
          ))}
      </div>
    </div>
  );
}

export default App;

可以看到http://localhost:3000/的数据。应用现在已经完成。但是在喜欢的数量上有一个小问题;它显示 3451 而不是 346(见图 3-13 )。

img/512020_1_En_3_Chapter/512020_1_En_3_Fig13_HTML.jpg

图 3-13。

出现此问题的原因是从数据库中传递字符串数字。在VideoSidebar.js中,在喜欢的前面加一个 + ,把字符串改成数字。

...

            <div className="videoSidebar__button">
                { liked ? <FavoriteIcon fontSize="large" onClick={e => setLiked(false)} /> : <FavoriteBorderIcon fontSize="large" onClick={e => setLiked(true)} /> }
                <p>{liked ? +likes + 1 : likes}</p>
            </div>

'''

将后端部署到 Heroku

转到 www.heroku.com 部署后端。按照你在第一章中所做的相同步骤,创建一个名为短视频后端的应用。

成功部署后,转到链接。图 3-14 显示了正确的文本。

img/512020_1_En_3_Chapter/512020_1_En_3_Fig14_HTML.jpg

图 3-14。

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

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

将前端部署到 Firebase

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

img/512020_1_En_3_Chapter/512020_1_En_3_Fig15_HTML.jpg

图 3-15。

摘要

在本章中,我们创建了一个短视频分享应用。我们在 ReactJS 中构建前端,并在 Firebase 中托管它。后端构建在 NodeJS 中,托管在 Heroku 中。数据库是在 MongoDB 中构建的。

四、使用 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 数据库中存储图像和文章的链接。

六、使用 MERN 构建一个受欢迎的社交网络

欢迎来到最后的 MERN 项目,在这里你将使用 MERN 框架构建一个令人敬畏的流行社交网络。后端托管在 Heroku,前端站点托管在 Firebase。Firebase 还处理身份验证功能。Material-UI 提供了该项目中的图标。您还可以使用样式化的组件和 CSS。

使用 Pusher 是因为 MongoDB 不是像 Firebase 那样的实时数据库,并且您希望帖子反映某人点击提交的时刻。

在这个项目中,您将构建一个具有 Google 身份验证的社交媒体应用。这款应用的外观和感觉类似于一个流行的社交网络。在这里,你可以贴一张图片和描述性的文字。最终托管的 app 如图 6-1 所示。

img/512020_1_En_6_Chapter/512020_1_En_6_Fig1_HTML.jpg

图 6-1

最终应用

转到您的终端并创建一个popular-social-mern文件夹。在里面,使用 create-react-app 创建一个名为 popular-social-frontend 的新应用。这些命令如下所示。

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

Firebase 托管初始设置

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

因为你也在使用认证功能,你需要做第四章中提到的额外配置,并获取firebaseConfig,你需要复制它。

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

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

React 基本设置

回到 React 项目,将cd转到popular-social-frontend目录。然后,用npm start启动 React app。

cd popular-social-frontend
npm start

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

img/512020_1_En_6_Chapter/512020_1_En_6_Fig2_HTML.jpg

图 6-2

初始应用

添加样式化组件

您将使用著名的 CSS-in-JS 库 styled-components ( https://styled-components.com )来设计项目的样式。这是在 React 项目中使用 CSS 的最流行的替代方法之一。打开集成终端并将其安装在popular-social-frontend文件夹中。

npm i styled-components

然后,在App.js文件中导入样式化的组件。代替 div 的是AppWrapper组件。后有风格AppWrapper的功能。更新的内容用粗体标记。

import styled from 'styled-components'

function App() {
  return (
    <AppWrapper>
      <h1>Popular Social Network MERN</h1>
    </AppWrapper >
  );
}

const AppWrapper = styled.div`
  background-color: #f1f2f5;
`
export default App;

创建标题组件

让我们创建一个组件,在应用中显示一个漂亮的标题。为此,在src文件夹中创建一个components文件夹,然后在components文件夹中创建一个Header.js文件。

图标来自素材-UI ( https://material-ui.com )。您需要进行两次 npm 安装,然后在popular-social-frontend文件夹中安装核心和图标。

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

Header.js里放了很多代码,但主要是静态代码,用的是素材 UI 图标。请注意,所有文件中都使用了样式化的组件。

样式化的组件就像 SCSS,可以在父元素中嵌套内部 div。例如,HeaderCenter样式组件包含了header__option div 的样式。另外,注意像悬停这样的伪元素是由&:hover给出的。

import React from 'react'
import styled from 'styled-components'
import SearchIcon from '@material-ui/icons/Search'
import HomeIcon from '@material-ui/icons/Home'
import FlagIcon from '@material-ui/icons/Flag'
import SubscriptionsOutlinedIcon from '@material-ui/icons/SubscriptionsOutlined'
import StorefrontOutlinedIcon from '@material-ui/icons/StorefrontOutlined'
import SupervisedUserCircleIcon from '@material-ui/icons/SupervisedUserCircle'
import { Avatar, IconButton } from '@material-ui/core'
import AddIcon from '@material-ui/icons/Add'
import ForumIcon from '@material-ui/icons/Flag'
import NotificationsActiveIcon from '@material-ui/icons/NotificationsActive'
import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
const Header = () => {
    return (
        <HeaderWrapper>
            <HeaderLeft>
                <img src="logo192.png" alt="Popular" />
            </HeaderLeft>
            <HeaderInput>
                <SearchIcon />
                <input placeholder="Search Popular" type="text" />
            </HeaderInput>
            <HeaderCenter>
                <div className="header__option header__option--active">
                    <HomeIcon fontsize="large" />
                </div>
                <div className="header__option">
                    <FlagIcon fontsize="large" />
                </div>
                <div className="header__option">
                    <SubscriptionsOutlinedIcon fontsize="large" />
                </div>
                <div className="header__option">
                    <StorefrontOutlinedIcon fontsize="large" />
                </div>
                <div className="header__option">
                    <SupervisedUserCircleIcon fontsize="large" />
                </div>
            </HeaderCenter>
            <HeaderRight>
                <div className="header__info">
                    <Avatar src="https://pbs.twimg.com/profile_img/1020939891457241088/fcbu814K_400x400.jpg " />
                   <h4>Nabendu</h4>

                </div>
                <IconButton>
                    <AddIcon />
                </IconButton>
                <IconButton>
                    <ForumIcon />
                </IconButton>
                <IconButton>
                    <NotificationsActiveIcon />
                </IconButton>
                <IconButton>
                    <ExpandMoreIcon />
                </IconButton>
            </HeaderRight>
        </HeaderWrapper>
    )
}
const HeaderWrapper = styled.div`
        display: flex;
        padding: 15px 20px;
        justify-content: space-between;
        align-items: center;
        position: sticky;
        background-color: white;
        z-index: 100;
        top: 0;
        box-shadow: 0px 5px 8px -9px rgba(0, 0, 0, 0.75);
    `

const HeaderLeft = styled.div`
        display: flex;
        justify-content: space-evenly;
        img {
            height: 40px;
        }
    `

const HeaderInput = styled.div`
        display: flex;
        align-items: center;
        background-color: #eff2f5;
        padding: 10px;
        margin-left: 10px;
        border-radius: 33px;
        input {
            border: none;
            background-color: transparent;
            outline-width: 0;
        }

    `
const HeaderCenter = styled.div`
        display: flex;
        flex: 1;
        justify-content: center;
        .header__option{
            display: flex;
            align-items: center;
            padding: 10px 30px;
            cursor: pointer;
            .MuiSvgIcon-root{
                color: gray;
            }
            &:hover{
                background-color: #eff2f5;
                border-radius: 10px;
                align-items: center;
                padding: 0 30px;
                border-bottom: none;
                .MuiSvgIcon-root{
                    color: #2e81f4;
                }
            }
        }

        .header__option--active{
            border-bottom: 4px solid #2e81f4;
            .MuiSvgIcon-root{
                color: #2e81f4;
            }
        }
    `
const HeaderRight = styled.div`
        display: flex;
        .header__info {
            display: flex;
            align-items: center;
            h4 {
                margin-left: 10px;
            }
        }
    `
export default Header

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

import styled from 'styled-components'
import Header from './components/Header'

function App() {
  return (
    <AppWrapper>
      <Header />
    </AppWrapper >
  );
}

const AppWrapper = styled.div`
  background-color: #f1f2f5;
`
export default App;

图 6-3 显示,这个头在 localhost 上看起来棒极了。

img/512020_1_En_6_Chapter/512020_1_En_6_Fig3_HTML.jpg

图 6-3

漂亮的头球

创建侧栏组件

让我们创建组件来显示一个漂亮的包含用户头像和一些静态信息的左侧栏。在components文件夹中创建一个Sidebar.js文件,并将以下内容放入其中。内容是静态的,主要包含传递给另一个SidebarRow组件的材质 UI 图标。

import React from 'react'
import SidebarRow from './SidebarRow'
import LocalHospitalIcon from '@material-ui/icons/LocalHospital'
import EmojiFlagsIcon from '@material-ui/icons/EmojiFlags'
import PeopleIcon from '@material-ui/icons/People'
import ChatIcon from '@material-ui/icons/Chat'
import StorefrontIcon from '@material-ui/icons/Storefront'
import VideoLibraryIcon from '@material-ui/icons/VideoLibrary'
import ExpandMoreOutlined from '@material-ui/icons/ExpandMoreOutlined'
import styled from 'styled-components'
const Sidebar = () => {
    return (
        <SidebarWrapper>
            <SidebarRow src="https://pbs.twimg.com/profile_img/1020939891457241088/fcbu814K_400x400.jpg" title="Nabendu" />
            <SidebarRow Icon={LocalHospitalIcon} title="COVID-19 Information Center" />
            <SidebarRow Icon={EmojiFlagsIcon} title="Pages" />
            <SidebarRow Icon={PeopleIcon} title="Friends" />
            <SidebarRow Icon={ChatIcon} title="Messenger" />
            <SidebarRow Icon={StorefrontIcon} title="Marketplace" />
            <SidebarRow Icon={VideoLibraryIcon} title="Videos" />
            <SidebarRow Icon={ExpandMoreOutlined} title="More" />
        </SidebarWrapper>
    )
}

const SidebarWrapper = styled.div``
export default Sidebar

components文件夹中创建一个SidebarRow.js文件。注意MuiSvgIcon-root类在每个材质界面上都有。您的目标是添加自定义样式。

import React from 'react'
import { Avatar } from '@material-ui/core'
import styled from 'styled-components'
const SidebarRow = ({ src, Icon, title }) => {
    return (
        <SidebarRowWrapper>
            {src && <Avatar src={src} />}
            {Icon && <Icon />}
            <p>{title}</p>
        </SidebarRowWrapper>
    )
}
const SidebarRowWrapper = styled.div`
    display: flex;
    align-items: center;
    padding: 10px;
    cursor: pointer;
    &:hover {
        background-color: lightgray;
        border-radius: 10px;
    }
    p{
        margin-left:20px;
        font-weight: 600;
    }
    .MuiSvgIcon-root{
        font-size:xx-large;
        color: #2e81f4;
    }`
export default SidebarRow

App.js文件中,在app__body div 中添加一个侧边栏组件,并在样式化组件中为其添加样式。更新的内容用粗体标记。

import styled from 'styled-components'
import Header from './components/Header'
import Sidebar from './components/Sidebar'

function App() {
  return (
    <AppWrapper>
      <Header />
      <div className="app__body">
        <Sidebar />
      </div>
    </AppWrapper >
  );
}

const AppWrapper = styled.div`
  background-color: #f1f2f5;
  .app__body {
    display: flex;
  }
`
export default App;

图 6-4 显示了 localhost 上的侧边栏。

img/512020_1_En_6_Chapter/512020_1_En_6_Fig4_HTML.jpg

图 6-4

不错的侧边栏

创建提要组件

让我们看看应用中的中间部分,它添加并显示了所有的帖子。在components文件夹中创建一个Feed.js文件。把下面的内容放进去。一个FeedWrapper风格的组件正在包装一个Stories组件。

import React from 'react'
import Stories from './Stories'
import styled from 'styled-components'
const Feed = () => {
    return (
        <FeedWrapper>
            <Stories />
        </FeedWrapper>
    )
}
const FeedWrapper = styled.div`
    flex: 1;
    padding: 30px 150px;
    display: flex;
    flex-direction: column;
    align-items: center;
`
export default Feed

接下来,在components文件夹中创建一个Stories.js文件。在这里,您正在将imageprofileSrc,title传递给Story组件。

import React from 'react'
import Story from './Story'
import styled from 'styled-components'
const Stories = () => {
    return (
        <StoriesWrapper>
            <Story
image="https://images.unsplash.com/photo-1602524206684-fdf6393c7d89?ixid=MXwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1350&q=80"               profileSrc="https://pbs.twimg.com/profile_img/1020939891457241088/fcbu814K_400x400.jpg"
                title="Nabendu"
            />
            <Story
                image="https://images.unsplash.com/photo-1602526430780-782d6b1783fa?ixid=MXwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1350&q=80"                profileSrc="https://pbs.twimg.com/profile_img/1020939891457241088/fcbu814K_400x400.jpg"
                title="TWD"
            />
            <Story
                image="https://www.jonesday.com/-/media/files/publications/2019/05/when-coding-is-criminal/when-coding-is-criminal.jpg?h=800&w=1600&la=en&hash=5522AA91198A168017C511FCBE77E201"                profileSrc="https://pbs.twimg.com/profile_img/1020939891457241088/fcbu814K_400x400.jpg"
                title="Nabendu"
            />
        </StoriesWrapper>
    )
}
const StoriesWrapper = styled.div`
    display: flex;
`
export default Stories

接下来,在components文件夹中创建Story.js文件。在这里,你展示道具。请注意,StoryWrapper在背景图像中使用了道具,这显示了样式化组件的威力。如果图像是在 props 中传递的,则使用三元运算符来显示图像。

import { Avatar } from '@material-ui/core'
import React from 'react'
import styled from 'styled-components'
const Story = ({ image, profileSrc, title }) => {
    return (
        <StoryWrapper imageUrl={`${image}`}>
            <Avatar src={profileSrc} className='story__avatar' />
            <h4>{title}</h4>
        </StoryWrapper>
    )
}
const StoryWrapper = styled.div`
    background-image: url(${props => props.imageUrl ? props.imageUrl : ''});
    position: relative;
    background-position: center center;
    background-size: cover;
    background-repeat: no-repeat;
    width: 120px;
    height: 200px;
    box-shadow: 0px 5px 17px -7px rgba(0,0,0,0.75);
    border-radius: 10px;
    margin-right: 10px;
    cursor: pointer;
    transition: transform 100ms ease-in;
    &:hover {
        transform: scale(1.07);
    }
    .story__avatar {
        margin: 10px;
        border: 5px solid #2e81f4;
    }
    h4 {
        position: absolute;
        bottom: 20px;
        left: 20px;
        color: white;
    }
`
export default Story

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

import styled from 'styled-components'
import Header from './components/Header'
import Sidebar from './components/Sidebar'
import Feed from './components/Feed'

function App() {
  return (
    <AppWrapper>
      <Header />
      <div className="app__body">
        <Sidebar />
        <Feed />
      </div>
    </AppWrapper >
  );
}

const AppWrapper = styled.div`
 ...
`
export default App;

图 6-5 显示这些故事在 localhost 上看起来很棒。

img/512020_1_En_6_Chapter/512020_1_En_6_Fig5_HTML.jpg

图 6-5

不错的形象

添加小部件

通过从脸书的页面插件中添加一个小部件来完成 web 应用的前端。将这个添加到右边栏,这样应用看起来就完整了。使用脸书开发者帐户( https://developers.facebook.com/docs/plugins/page-plugin/ )连接,这样你就可以在任何 web 应用中使用它。

您需要给出脸书页面的 URL、宽度和高度,然后向下滚动并单击 Get Code 按钮。我使用了我的盖茨比烹饪书页面,如图 6-6 所示。

img/512020_1_En_6_Chapter/512020_1_En_6_Fig6_HTML.jpg

图 6-6

添加小部件

将打开一个弹出窗口。你需要点击 iFrame 标签获取代码,如图 6-7 所示。

img/512020_1_En_6_Chapter/512020_1_En_6_Fig7_HTML.jpg

图 6-7

获取 iFrame

components文件夹中创建一个Widget.js文件。包括早期的 IFrame,但稍有修改。

import styled from 'styled-components'
const Widget = () => {
    return (
        <WidgetWrapper>
        <iframe            src="https://www.facebook.com/plugins/page.php?href=https%3A%2F%2Fwww.facebook.com%2Fgatsbycookbook%2F&tabs=timeline&width=340&height=1500&small_header=false&adapt_container_width=true&hide_cover=true&show_facepile=true&appId=332535087157151"
            width="340"
            height="1500"
            style={{ border: "none", overflow: "hidden" }}
            scrolling="no"
            frameborder="0"
            allow="encrypted-media"
            title="Facebook Widget"
        >
        </iframe>
        </WidgetWrapper>
    )
}
const WidgetWrapper = styled.div``
export default Widget

图 6-8 显示了本地主机上的一个漂亮的小部件。

img/512020_1_En_6_Chapter/512020_1_En_6_Fig8_HTML.jpg

图 6-8

显示的小部件

创建信使组件

接下来,让我们通过实现组件来完成Feed.js文件,用户可以通过该组件为文章编写描述并上传图像。这里又添加了两个组件。在components文件夹中创建一个新的Messenger.js文件。

首先将它包含在Feed.js文件中。更新的内容用粗体标记。

import React from 'react'
import Stories from './Stories'
import styled from 'styled-components'
import Messenger from './Messenger'
const Feed = () => {
    return (
        <FeedWrapper>
            <Stories />
             < Messenger />
        </FeedWrapper>

    )
}
const FeedWrapper = styled.div`
  ...
`
export default Feed

让我们创建Messenger.js文件。这里,你主要有MessengerTopMessengerBottom组件。在MessengerTop中,你主要有一个文本框,一个文件,和一个按钮。你用 CSS 中的display: none使按钮不可见。一旦设置了后端,大部分功能都在其中了。

MessengerBottom组件主要是显示图标的静态组件。

import React, { useState } from 'react'
import { Avatar, Input } from '@material-ui/core'
import VideocamIcon from '@material-ui/icons/Videocam'
import PhotoLibraryIcon from '@material-ui/icons/PhotoLibrary'
import InsertEmoticonIcon from '@material-ui/icons/InsertEmoticon'
import styled from 'styled-components'
const Messenger = () => {
    const [input, setInput] = useState('')
    const [image, setImage] = useState(null)
    const handleChange = e => {
        if(e.target.files[0])
            setImage(e.target.files[0])
    }
    const handleSubmit = e => {
        e.preventDefault()
    }
return (
        <MessengerWrapper>
            <MessengerTop>
                <Avatar src=" https://pbs.twimg.com/profile_img/1020939891457241088/fcbu814K_400x400.jpg " />
         <form>
                    <input
                        type="text"
                        className="messenger__input"
                        placeholder="What's on your mind?"
                        value={input}
                        onChange={e => setInput(e.target.value)}
                    />
                    <Input

                        type="file"
                        className="messenger__fileSelector"
                        onChange={handleChange}
                    />
                    <button onClick={handleSubmit} type="submit">Hidden</button>
                </form>
            </MessengerTop>
            <MessengerBottom>
                <div className="messenger__option">
                    <VideocamIcon style={{ color: 'red' }} />
                    <h3>Live Video</h3>
                </div>
                <div className="messenger__option">
                    <PhotoLibraryIcon style={{ color: 'green' }} />
                    <h3>Photo/Video</h3>
                </div>
                <div className="messenger__option">
                    <InsertEmoticonIcon style={{ color: 'orange' }} />
                    <h3>Feeling/Activity</h3>
                </div>
            </MessengerBottom>
        </MessengerWrapper>
    )
}
const MessengerWrapper = styled.div`
    display: flex;
    margin-top: 30px;
    flex-direction: column;
    background-color: white;
    border-radius: 15px;
    box-shadow: 0px 5px 7px -7px rgba(0,0,0,0.75);
    width: 100%;
`
const MessengerTop = styled.div`
    display: flex;
    border-bottom: 1px solid #eff2f5;
    padding: 15px;
    form {
        flex: 1;
        display: flex;
        .messenger__input {

            flex: 1;
            outline-width: 0;
            border: none;
            padding: 5px 20px;
            margin: 0 10px;
            border-radius: 999px;
            background-color: #eff2f5;
        }
        .messenger__fileSelector{
            width: 20%;
        }
        button {
            display: none;
        }
    }
`
const MessengerBottom = styled.div`
    display: flex;
    justify-content: space-evenly;
    .messenger__option{
        padding: 20px;
        display: flex;
        align-items: center;
        color: gray;
        margin: 5px;
        h3{
            font-size: medium;
            margin-left: 10px;
        }
        &:hover{
            background-color: #eff2f5;
            border-radius: 20px;
            cursor: pointer;
        }
    }
`
export default Messenger

本地主机几乎完成了,Messenger 组件显示正确(见图 6-9 )。

img/512020_1_En_6_Chapter/512020_1_En_6_Fig9_HTML.jpg

图 6-9

信使组件

创建帖子组件

接下来,让我们在 web 应用中显示帖子。Post组件在Feed.js文件中。它现在是硬编码的,但很快就会来自后端。

更新的内容用粗体标记。

...
import Post from './Post'

const Feed = () => {
    return (
        <FeedWrapper>
            <Stories />
             < Messenger />

            <Post     profilePic="https://pbs.twimg.com/profile_img/1020939891457241088/fcbu814K_400x400.jpg"       message="Awesome post on CSS Animation. Loved it"
            timestamp="1609512232424"                imgName="https://res.cloudinary.com/dxkxvfo2o/image/upload/v1598295332/CSS_Animation_xrvhai.png"
            username="Nabendu"
            />
            <Post                profilePic="https://pbs.twimg.com/profile_img/1020939891457241088/fcbu814K_400x400.jpg"                message="BookList app in Vanilla JavaScript"
            timestamp="1509512232424"                imgName="https://res.cloudinary.com/dxkxvfo2o/image/upload/v1609138312/Booklist-es6_sawxbc.png"
            username="TWD"
            />
        </FeedWrapper>
    )
}
const FeedWrapper = styled.div`
  ...
`
export default Feed

components文件夹中创建一个新的Post.js文件。在这里,PostTop部分显示了头像、用户名和时间。PostBottom显示消息和图像。

接下来,显示PostOptions中的图标。

import { Avatar } from '@material-ui/core'
import React from 'react'
import styled from 'styled-components'
import ThumbUpIcon from '@material-ui/icons/ThumbUp'
import ChatBubbleOutlineIcon from '@material-ui/icons/ChatBubbleOutline'
import NearMeIcon from '@material-ui/icons/NearMe'
import AccountCircleIcon from '@material-ui/icons/AccountCircle'
import ExpandMoreOutlined from '@material-ui/icons/ExpandMoreOutlined'
const Post = ({ profilePic, message, timestamp, imgName, username }) => {
    return (
        <PostWrapper>
            <PostTop>
                <Avatar src={profilePic} className="post__avatar" />
                <div className="post__topInfo">
                    <h3>{username}</h3>
                    <p>{new Date(parseInt(timestamp)).toUTCString()}</p>
                </div>
            </PostTop>

            <PostBottom>
                <p>{message}</p>
            </PostBottom>
            {
                imgName ? (
                    <div className="post__image">
                        <img src={imgName} alt="Posts" />
                    </div>
                ) : (
                        console.log('DEBUG >>> no image here')
                    )
            }
            <PostOptions>
                <div className="post__option">
                    <ThumbUpIcon />
                    <p>Like</p>
                </div>
                <div className="post__option">
                    <ChatBubbleOutlineIcon />
                    <p>Comment</p>
                </div>
                <div className="post__option">
                    <NearMeIcon />
                    <p>Share</p>
                </div>
                <div className="post__option">
                    <AccountCircleIcon />
                    <ExpandMoreOutlined />
                </div>
            </PostOptions>
        </PostWrapper>
    )
}

const PostWrapper = styled.div`
    width: 100%;
    margin-top: 15px;
    border-radius: 15px;
    background-color: white;
    box-shadow: 0px 5px 7px -7px rgba(0,0,0,0.75);
    .post__image{

        img{
            width: 100%
        }
    }
`
const PostTop = styled.div`
    display: flex;
    position: relative;
    align-items: center;
    padding: 15px;
    .post__avatar{
        margin-right: 10px;
    }
    .post__topInfo{
        h3{
            font-size: medium;
        }
        p{
            font-size: small;
            color: gray;
        }
    }
`
const PostBottom = styled.div`
    margin-top: 10px;
    margin-bottom:10px;
    padding: 15px 25px;
`
const PostOptions = styled.div`
    padding: 10px;
    border-top: 1px solid lightgray;
    display: flex;
    justify-content: space-evenly;
    font-size: medium;
    color: gray;
    cursor: pointer;
    padding: 15px;
    .post__option {
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 5px;
        flex: 1;
        p {
            margin-left: 10px;
        }
        &:hover {
            background-color: #eff2f5;
            border-radius: 10px;
        }
    }
`
export default Post

图 6-10 显示了本地主机上的帖子。

img/512020_1_En_6_Chapter/512020_1_En_6_Fig10_HTML.jpg

图 6-10

显示的帖子

Google 身份验证设置

让我们来看看谷歌认证,它允许你登录应用并发布。在这里,您使用第四章中的流程,并将其添加到 Firebase 控制台。

firebase.js文件中,初始化应用并使用auth, provider和数据库。更新的内容用粗体标记。

import firebase from 'firebase'
const firebaseConfig = {
   ...
};
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

您还需要在终端中安装 Firebase 的所有依赖项。但是要确保你在popular-social-frontend文件夹中。

npm i firebase

创建登录组件

让我们在components文件夹中创建一个Login.js文件。Login.js文件是一个简单的功能组件,显示了一个徽标和一个登录按钮。和以前一样,您正在使用样式化的组件。

import React from 'react'
import styled from 'styled-components'
import { Button } from '@material-ui/core'

const Login = () => {
    const signIn = () => {}
    return (
        <LoginWrapper>
            <div className="login__logo">
                <img src="logo512.png" alt="login" />
                <h1>Popular Social</h1>
            </div>
            <Button type='submit' className="login__btn" onClick={signIn}>Sign In</Button>
        </LoginWrapper>
    )
}
const LoginWrapper = styled.div`
    display: grid;
    place-items: center;
    height: 100vh;
    .login__logo {
        display: flex;
        flex-direction: column;
        img {
            object-fit: contain;
            height: 150px;
            max-width: 200px;
        }
    }
    .login__btn {
        width: 300px;
        background-color: #2e81f4;
        color: #eff2f5;
        font-weight: 800;
        &:hover {
            background-color: white;
            color: #2e81f4;
        }
    }
`
export default Login

接下来,如果没有当前用户,显示Login组件。您创建一个临时状态变量来显示在App.js文件中。更新的内容用粗体标记。

...
import { useState } from 'react'
import Login from './components/Login'
function App() {
  const [user, setUser] = useState(null)

  return (
    <AppWrapper>
      {user ? (

          <Header />
          <div className="app__body">
            <Sidebar />
            <Feed />
            <Widget />
          </div>

      ) : (
        <Login />
      )}
    </AppWrapper>
  );
}
const AppWrapper = styled.div`...`
export default App;

图 6-11 显示了本地主机上的登录屏幕。

img/512020_1_En_6_Chapter/512020_1_En_6_Fig11_HTML.jpg

图 6-11

登录屏幕

Login.js文件中,需要从本地 Firebase 文件中导入authprovider。然后用一个signInWithPopup()的方法得到结果。更新的内容用粗体标记。

...
import { Button } from '@material-ui/core'
import { auth, provider } from '../firebase'
const Login = () => {
    const signIn = () => {
        auth.signInWithPopup(provider)
            .then(result => console.log(result))
            .catch(error => alert(error.message))
    }
    return (...)
}
const LoginWrapper = styled.div`...`
export default Login

点击 localhost 上的登录按钮,弹出 Gmail 认证窗口。点击 Gmail 用户名后,你会在控制台中看到所有登录用户的详细信息,如图 6-12 所示。

img/512020_1_En_6_Chapter/512020_1_En_6_Fig12_HTML.jpg

图 6-12

登录详细信息

使用 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)

接下来,在src文件夹中创建一个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 './StateProvider';
import reducer, { initialState } from './Reducer';

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

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

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

...
import { auth, provider } from '../firebase'
import { useStateValue } from '../StateProvider'
import { actionTypes } from '../Reducer'
const Login = () => {
    const [{}, dispatch] = useStateValue()

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

    return (...)
}
const LoginWrapper = styled.div`...`
export default Login

返回到App.js文件并使用useStateValue钩子。从中提取全局用户,并以您的登录为基础。更新的内容用粗体标记。

...
import { useStateValue } from './StateProvider';
function App() {
  const [{ user }, dispatch] = useStateValue()
  return (...);
}

const AppWrapper = styled.div`...`
export default App;

如果你在 localhost 上登录,它会带你进入应用,如图 6-13 所示。

img/512020_1_En_6_Chapter/512020_1_En_6_Fig13_HTML.jpg

图 6-13

已登录

在其他组件中使用 Redux 数据

你可以访问用户的数据,所以你可以在任何地方使用它。让我们使用用户的 Google 图片作为头像和 Google 用户名,而不是在Header.js文件中硬编码的那个。更新的内容用粗体标记。

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

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

    return (
        <HeaderWrapper>
            ...
            <HeaderCenter>
           ...
            </HeaderCenter>
            <HeaderRight>
                <div className="header__info">
                    <Avatar src={user.photoURL} />
                    <h4>{user.displayName}</h4>
                </div>
           ...
            </HeaderRight>
        </HeaderWrapper>

    )
}
const HeaderWrapper = styled.div`...`
export default Header

还有,使用用户的谷歌图片作为Messenger.js文件中的头像。

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

const Messenger = () => {
    const [input, setInput] = useState('')
    const [image, setImage] = useState(null)
    const [{ user }, dispatch] = useStateValue()
        ...
    return (
        <MessengerWrapper>
            <MessengerTop>
                <Avatar src={user.photoURL} />
                <form>
        ...
                </form>
            </MessengerTop>
            <MessengerBottom>
       ...
            </MessengerBottom>
        </MessengerWrapper>
    )
}
const MessengerWrapper = styled.div`...`
export default Messenger

Sidebar.js文件包括用户的用户名和头像。

...
import { useStateValue } from '../StateProvider'
const Sidebar = () => {
    const [{ user }, dispatch] = useStateValue()

    return (
        <SidebarWrapper>

            <SidebarRow src={user.photoURL} title={user.displayName} />
            <SidebarRow Icon={LocalHospitalIcon} title="COVID-19 Information Center" />
               ...
        </SidebarWrapper>
    )
}
const SidebarWrapper = styled.div`
`
export default Sidebar

图 6-14 显示了用户在本地主机上所有正确位置的谷歌图片和用户名。

img/512020_1_En_6_Chapter/512020_1_En_6_Fig14_HTML.jpg

图 6-14

登录详细信息

初始后端设置

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

mkdir popular-social-backend
cd popular-social-backend
git init

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

img/512020_1_En_6_Chapter/512020_1_En_6_Fig15_HTML.jpg

图 6-15

初始后端设置

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

node_modules

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

{
  "name": "popular-social-backend",
  "version": "1.0.0",
  "description": "Popular Social 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"
}

开始之前你需要安装一些软件包。打开终端,在popular-social-backend文件夹中安装corsexpressgridfs-streammongoosemultermulter-gridfs-storagenodemonpathbody-parser,pusher

npm i body-parser cors express gridfs-stream mongoose multer multer-gridfs-storage nodemon path pusher

MongoDB 设置

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

初始路线设置

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

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

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

//imports
import express from 'express'
import mongoose from 'mongoose'
import cors from 'cors'
import multer from 'multer'
import GridFsStorage from 'multer-gridfs-storage'
import Grid from 'gridfs-stream'
import bodyParser from 'body-parser'
import path from 'path'
import Pusher from 'pusher'

//app config
Grid.mongo = mongoose.mongo
const app = express()
const port = process.env.PORT || 9000
//middleware
app.use(bodyParser.json())
app.use(cors())

//DB Config
//api routes
app.get("/", (req, res) => res.status(200).send("Hello TheWebDev"))
//listen
app.listen(port, () => console.log(`Listening on localhost: ${port}`))

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

img/512020_1_En_6_Chapter/512020_1_En_6_Fig16_HTML.jpg

图 6-16

路线测试

数据库用户和网络访问

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

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

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

//imports
...

//app config
Grid.mongo = mongoose.mongo
const app = express()
const port = process.env.PORT || 9000
const connection_url = 'mongodb+srv://admin:<password>@cluster0.quof7.mongodb.net/myFirstDatabase?retryWrites=true&w=majority'
//middleware
...

在 MongoDB 中存储图像

您使用 GridFS 来存储图像。您之前通过multer-gridfs-storage包安装了它。gridfs-stream包负责读取并呈现给用户的流。

项目中使用了两个连接。第一个用于图片上传,第二个用于其他 GET 和 POSTs。server.js中更新的代码用粗体标记。

...
//middleware
app.use(bodyParser.json())
app.use(cors())
//DB Config
const connection = mongoose.createConnection(connection_url, {
    useNewUrlParser: true,
    useCreateIndex: true,
    useUnifiedTopology: true
})
mongoose.connect(connection_url, {
    useNewUrlParser: true,
    useCreateIndex: true,
    useUnifiedTopology: true
})

//api routes
...

完成上传图像的代码。首先,创建一个gfs变量,然后使用conn变量连接到数据库。接下来,使用Grid连接到数据库,然后创建一个图像集合来存储图片。

接下来,创建storage变量,它用一个对象调用一个GridFsStorage函数。这里使用了connection_url变量。在承诺中,通过附加当前日期来创建一个唯一的文件名。创建一个包含文件名桶名fileInfo对象,作为之前创建的集合图像。

使用multer包通过传递之前创建的变量来上传图像。

使用 POST 请求构建端点来上传图像,并上传之前创建的变量。server.js中更新的代码用粗体标记。

...
//DB Config
const connection = mongoose.createConnection(connection_url, {
...
})
let gfs

connection.once('open', () => {

    console.log('DB Connected')
    gfs = Grid(connection.db, mongoose.mongo)
    gfs.collection('images')
})
const storage = new GridFsStorage({
    url: connection_url,
    file: (req, file) => {
        return new Promise((resolve, reject) => {
            const filename = `image-${Date.now()}${path.extname(file.originalname)}`
            const fileInfo = {
                filename: filename,
                bucketName: 'images'
            }
            resolve(fileInfo)
        })
    }
})
const upload = multer({ storage })
mongoose.connect(connection_url, {
  ...
})

//api routes
app.get("/", (req, res) => res.status(200).send("Hello TheWebDev"))
app.post('/upload/image', upload.single('file'),(req, res) => {
    res.status(201).send(req.file)
})

//listen
...

在 Postman 中检查端点。向http://localhost:9000/upload/image提交发布请求。

选择正文,然后选择表单数据。接下来,从文件下拉菜单中选择一个文件,然后点击选择文件。这将打开一个弹出窗口,您必须在其中选择一个图像文件(参见图 6-17 )。

img/512020_1_En_6_Chapter/512020_1_En_6_Fig17_HTML.jpg

图 6-17

发布请求

点击发送按钮。如果一切顺利,你会在 Postman 中看到图像细节,如图 6-18 所示。

img/512020_1_En_6_Chapter/512020_1_En_6_Fig18_HTML.jpg

图 6-18

张贴图像

也可以在 MongoDB 中查看,图片保存为images.chunks,细节在images.files,如图 6-19 。

img/512020_1_En_6_Chapter/512020_1_En_6_Fig19_HTML.jpg

图 6-19

图像块

创建获取文件的路径。为此,创建一 img/single GET route,它带有一个参数 filename。然后使用findOne`方法找到文件。

如果文件存在,使用gfs.createReadStream()读取文件。然后使用管道将 res 传递给这个读取流。server.js中更新的代码用粗体标记。

...
//api routes
app.get("/", (req, res) => res.status(200).send("Hello TheWebDev"))
app.post('/upload/image', upload.any('file'),(req, res) => {
    res.status(201).send(req.file)
})

app.getimg/single',(req, res) => {
    gfs.files.findOne({ filename: req.query.name }, (err, file) => {
        if(err) {
            res.status(500).send(err)
        } else {
            if(!file || file.length === 0) {
                res.status(404).json({ err: 'file not found' })
            } else {
                const readstream = gfs.createReadStream(file.filename)
                readstream.pipe(res)
            }
        }
    })
})

//listen
...

接下来,让我们在 Postman 中测试 GET 路由以接收图像。

在 Postman 中,打开对http://localhost:90img/single的 GET 请求。在 Params 下,,而是来自 MongoDB 记录的图像。一旦按下发送按钮,图像就会返回(参见图 6-20 )。

img/512020_1_En_6_Chapter/512020_1_En_6_Fig20_HTML.jpg

图 6-20

获取请求

MongoDB 模式和路由

直到现在,这个过程都是获取图像并保存在 MongoDB 中。现在您已经有了图像细节,您可以将它与其他帖子细节一起保存在 MongoDB 中。

为此,您需要创建保存帖子的路径。首先,为文章创建模型。然后在popular-social-backend文件夹中创建一个postModel.js文件。

在这里,您用需要传递的参数创建一个模式,然后导出它。

import mongoose from 'mongoose'

const postsModel = mongoose.Schema({
    user: String,
    imgName: String,
    text: String,
    avatar: String,
    timestamp: String
})

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

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

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

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

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

...
import Posts from './postModel.js'
...
app.post('/upload/post', (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('/posts', (req, res) => {
    Posts.find((err, data) => {
        if(err) {
            res.status(500).send(err)
        } else {
            data.sort((b,a) => a.timestamp - b.timestamp)
            res.status(200).send(data)
        }
    })
})
//listen
app.listen(port, () => console.log(`Listening on localhost: ${port}`))

将后端与前端集成在一起

你希望在应用初始加载时获取所有消息,然后推送这些消息。您需要达到 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

Feed.js文件中进行必要的导入。之后,你就有了一个postsData状态变量。接下来,从useEffect调用一次syncFeed函数。

syncFeed函数执行对 posts 端点的 GET 调用,并用setPostsData设置postsDatares.data

...
import React, { useEffect, useState } from 'react'
import axios from '../axios'

const Feed = () => {
    const [postsData, setPostsData] = useState([])
    const syncFeed = () => {
        axios.get('/posts')
            .then(res => {
                console.log(res.data)
                setPostsData(res.data)
            })
    }

    useEffect(() => {
        syncFeed()
    }, [])

    return (
        <FeedWrapper>
            <Stories />
            <Messenger />
            {
                postsData.map(entry => (
                    <Post
                        profilePic={entry.avatar}
                        message={entry.text}
                        timestamp={entry.timestamp}
                        imgName={entry.imgName}
                        username={entry.user}
                    />
                ))
            }
        </FeedWrapper>
    )
}

const FeedWrapper = styled.div`...`

export default Feed

Messenger.js,中添加axiosFormData的导入,它们附加了新的图像。

更新handleSubmit()。在这里,检查您已经上传的图像,然后在表单中附加图像和图像名称。

使用axios.post将图像发送到/upload/image端点。在 then 部分,创建一个postData对象,从用户输入的输入中获取文本。imgName包含来自res.data.filename的图像名称。用户头像取自 Firebase 数据,而时间戳来自Date.now()

postData对象调用savePost()。请注意,这里有一个else,在这里您没有将图像发送到savePost()。这适用于用户创建没有任何图像的帖子的情况。

savePost()中,您使用postData并对/upload/post端点进行 POST 调用。更新的内容用粗体标记。

...
import axios from '../axios'
import FormData from 'form-data'

const Messenger = () => {
   ...
    const handleSubmit = e => {
        e.preventDefault()
        if(image) {
            const imgForm = new FormData()
            imgForm.append('file',image, image.name)
            axios.post('/upload/image', imgForm, {
                headers: {
                    'accept': 'application/json',
                    'Accept-Language': 'en-US,en;q=0.8',
                    'Content-Type': `multipart/form-data; boundary=${imgForm._boundary}`
                }
            }).then(res => {
                const postData = {

                    text: input,
                    imgName: res.data.filename,
                    user: user.displayName,
                    avatar: user.photoURL,
                    timestamp: Date.now()
                }
                savePost(postData)
            })
        } else {
            const postData = {
                text: input,
                user: user.displayName,
                avatar: user.photoURL,
                timestamp: Date.now()
            }
            savePost(postData)
        }
        setInput('')
        setImage(null)
    }

    const savePost = async postData => {
        await axios.post('/upload/post', postData)
            .then(res => {
                console.log(res)
            })
    }
return (...)
}

const MessengerWrapper = styled.div`...`
export default Messenger

下一个变化是在Post.js文件中,您通过将图像名称作为参数传递来显示从http://localhost:90img/single端点获得的图像。Post.js文件中更新的内容用粗体标出。

...
const Post = ({ profilePic, message, timestamp, imgName, username }) => {
    return (
           ...
            {
                imgName ? (
                    <div className="post__image">
                        <img src={`http://localhost:90img/single?name=${imgName}`} alt="Posts" />
                    </div>
                ) : (

                        console.log('DEBUG >>> no image here')
                    )
            }
           ...
        </PostWrapper>
    )
}
...

现在您有了一个可以上传图片和发布消息的应用。它存储在 MongoDB 中,并显示在主页上。但是你有一个问题,帖子没有实时反映。您必须刷新应用(参见图 6-21 )。

img/512020_1_En_6_Chapter/512020_1_En_6_Fig21_HTML.jpg

图 6-21

问题

配置推动器

由于 MongoDB 不是实时数据库,所以是时候给 app 添加一个推送器,用于实时数据。因为你已经完成了第四章的设置,按照同样的说明,创建一个名为的应用。

将推杆添加到后端

由于 Pusher 已经安装在后端,您只需要在server.js文件中为它添加代码。使用 Pusher 初始化代码,该代码可从 Pusher 网站获得。您可以通过在server.js中创建一个新的 Mongoose 连接来使用它。在这里,您使用changeStream来监控帖子的所有变化。如果有任何变化,触发推动器。

...
//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 for pusher')
    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}`))

将推杆添加到前端

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

npm i pusher-js

将推杆导入Feed.js中,然后使用唯一代码。然后用useEffect订阅帖子。如果改变了,调用syncFeed(),它再次从/posts端点获取所有的帖子。更新后的代码用粗体标记。

...
import Pusher from 'pusher-js'

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

const Feed = () => {
    const [postsData, setPostsData] = useState([])

    const syncFeed = () => {
        axios.get('/posts')
            .then(res => {

                console.log(res.data)
                setPostsData(res.data)
            })
    }

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

    useEffect(() => {
        syncFeed()
    }, [])

    return (...)
}

const FeedWrapper = styled.div`...`

export default Feed

现在回到应用,你可以实时发布任何内容。

将后端部署到 Heroku

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

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

img/512020_1_En_6_Chapter/512020_1_En_6_Fig22_HTML.jpg

图 6-22

后端已部署

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

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

将前端部署到 Firebase

是时候在 Firebase 中部署前端了。遵循与第一章相同的程序。

你需要更新Post.js文件。更新的内容用粗体标记。

    ...
            {
                imgName ? (
                    <div className="post__image">
                        <img src={` https://popular-social-backend.herokuapp.cimg/single?name=${imgName}`} alt="Posts" />
                    </div>
                ) : (
                        console.log('DEBUG >>> no image here')
                    )
            }
    ...

在这个过程之后,站点就可以正常运行了。

将前端部署到 Firebase

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

img/512020_1_En_6_Chapter/512020_1_En_6_Fig23_HTML.jpg

图 6-23

最终部署地点

摘要

在这一章中,你创建了一个简单而实用的社交网络。Firebase 在网上主办的。您学习了添加 Google 身份验证,通过它您可以使用 Google 帐户登录。您还了解了如何在 MongoDB 中存储图像,以及如何使用 Pusher 为 MongoDB 提供实时功能。

posted @ 2024-10-01 20:54  绝不原创的飞龙  阅读(101)  评论(0)    收藏  举报