React-和-Firebase-入门指南-全-

React 和 Firebase 入门指南(全)

原文:Beginning React and Firebase

协议:CC BY-NC-SA 4.0

一、使用 Firebase 设置和部署 ReactJS 项目

在这一章中,你将学习 Firebase,它是 Google 提供的一套工具。您还将学习如何通过 Firebase 托管部署一个简单的 React 应用。

Firebase 简介

Firebase 不仅仅是一个数据库,而是一套工具;它通常被称为后端即服务(BaaS)。Firebase 包含各种服务,如下所示:

  • 认证:用户登录和身份

  • 实时数据库:实时、云托管的 NoSQL 数据库

  • 云 Firestore :实时云托管 NoSQL 数据库

  • 云存储:大规模可扩展文件存储

  • 云功能:无服务器、事件驱动的后端功能

  • Firebase hosting :全球虚拟主机

  • ML 套件:一个用于常见机器学习任务的 SDK

Firebase 使前端开发人员可以轻松地将后端集成到他们的应用中,而无需创建任何 API 路由和其他后端代码。图 1-1 显示了一个传统 web 应用的例子,它从客户端应用向服务器发出 API 请求。剩下的代码由服务器处理。正如你在图 1-1 中看到的,Firebase 消除了后端工作,你可以直接与 Firebase 通信,Firebase 通过 SDK 托管在谷歌平台上。

img/512002_1_En_1_Fig1_HTML.jpg

图 1-1

Firebase(火力基地)

以 ReactJS 为前端,在 Firebase 后端构建项目非常容易。如果你在 MERN 做同一个项目(意味着 MongoDB、Express、ReactJS、NodeJS),它会花费更多的时间,而且会复杂得多,因为你需要在 NodeJS 中做后端 API。

我发现在 Firebase 中另一件容易做的事情是认证部分。身份验证曾经是 JWT 身份验证中最复杂的部分之一,但是使用 Firebase,您只需要几行代码。更好的是,您可以获得所有类型的身份验证。

Firebase 托管对于你的 ReactJS 应用来说也是非常容易使用的,这就是我们在这本书里将要讨论的。

创建 Firebase 帐户

要使用 Firebase,你只需要一个谷歌账户。所以,去 https://firebase.google.com/ 的火基地,点击去右上角的控制台。你需要使用你的谷歌账户登录,如图 1-2 所示。

img/512002_1_En_1_Fig2_HTML.jpg

图 1-2

Firebase 站点

设置托管

点击页面中的添加项目链接,如图 1-3 所示。因为我有很多项目,所以图中显示了它们。第一次,您将只能看到添加项目链接。

img/512002_1_En_1_Fig3_HTML.jpg

图 1-3

添加项目

在打开的页面上,给项目起一个类似 final-space-react 的名字,点击继续按钮,如图 1-4 所示。

img/512002_1_En_1_Fig4_HTML.jpg

图 1-4

命名项目

在下一页面中,禁用 Google Analytics 后点击创建项目按钮,如图 1-5 所示。我们正在禁用谷歌分析,因为我们在这里创建一个演示项目。如果您打算在生产中部署您的应用,您应该保持它处于启用状态。

img/512002_1_En_1_Fig5_HTML.jpg

图 1-5

创建项目

一段时间后,您将看到如图 1-6 所示的屏幕。在这里,你需要点击继续按钮。

img/512002_1_En_1_Fig6_HTML.jpg

图 1-6

持续的

现在点击屏幕左上角的设置图标,如图 1-7 所示。之后,点击项目设置

img/512002_1_En_1_Fig7_HTML.jpg

图 1-7

项目设置

在下一页中,点击页面底部的代码图标,如图 1-8 所示。

img/512002_1_En_1_Fig8_HTML.jpg

图 1-8

代码图标

在下一页上,输入您之前输入的应用的名称,在我的例子中是 final-space-react 。此外,单击 Firebase hosting 的复选框。之后点击注册 app 按钮,如图 1-9 所示。

img/512002_1_En_1_Fig9_HTML.jpg

图 1-9

选择 Firebase 托管

在下一页,只需点击下一个按钮(图 1-10 )。

img/512002_1_En_1_Fig10_HTML.jpg

图 1-10

下一步按钮

在下一页,您将看到从终端全局安装firebase-tools的命令(图 1-11 )。因此,打开任何终端,从任何地方运行该命令。请注意,这是机器上的一次性设置,因为我们使用了-g选项。-g选项指定需要在全球范围内安装。

img/512002_1_En_1_Fig11_HTML.jpg

图 1-11

全球安装 Firebase

暂时忽略下一组命令,点击继续到控制台按钮(图 1-12 )。

img/512002_1_En_1_Fig12_HTML.jpg

图 1-12

继续到控制台

从终端部署一个简单的 ReactJS 项目

在本节中,您将学习如何部署一个简单的 ReactJS 应用,该应用从一个简单的 API 获取数据。打开 https://github.com/nabendu82/final-space-react 然后点击代码和剪贴板复制图标(图 1-13 )。

img/512002_1_En_1_Fig13_HTML.jpg

图 1-13

开源代码库

现在,转到任意终端,使用以下命令克隆项目:

git clone https://github.com/nabendu82/final-space-react.git

之后,切换到项目的目录并运行npm i来安装所有的依赖项,如下所示:

cd final-space-react
npm i

现在从终端运行firebase login。如果你是第一次运行它,它会给你一个弹出消息。之后,运行firebase init命令。键入Y继续(图 1-14 )。

img/512002_1_En_1_Fig14_HTML.jpg

图 1-14

Firebase 登入

接下来,使用箭头键下到 Hosting,按空格键选择 Hosting,然后按 Enter,如图 1-15 所示。

img/512002_1_En_1_Fig15_HTML.jpg

图 1-15

作战

然后选择Use an existing project,回车,如图 1-16 所示。

img/512002_1_En_1_Fig16_HTML.jpg

图 1-16

现有项目

这里需要选择正确的项目,在我这里是final-space-react-c84fa(图 1-17 )。

img/512002_1_En_1_Fig17_HTML.jpg

图 1-17

选择最终空间 React 项目

接下来选择公共目录,也就是build。下一个选项是Yes单页应用,GitHub 部署,在这里你选择No(图 1-18 )。

img/512002_1_En_1_Fig18_HTML.jpg

图 1-18

构建项目

现在,您需要在终端中运行npm run build来进行生产优化构建,命令如下:

npm run build

n

最后一个命令是firebase deploy将项目部署到 Firebase,如下所示:

firebase deploy

现在,你可以进入 https://final-space-react-c84fa.web.app/ (或者调整到你的项目名称)查看 app 正确运行,如图 1-19 。

img/512002_1_En_1_Fig19_HTML.jpg

图 1-19

完整应用

摘要

在这一章中,你学习了来自 Google 的令人敬畏的 Firebase 工具套件。之后,您学习了如何在 Firebase 中部署 React 项目。

二、使用 React 和 Firebase 构建待办事项应用

在前一章中,您学习了通过 Firebase 部署 React 应用。在本章中,您将学习如何在 ReactJS 中构建一个出色的待办事项列表应用,将数据存储在后端,特别是 Firebase Firestore 数据库中。主机也将在 Firebase。

我们将展示如何为项目中的图标使用材质 UI,我们将在这个项目中使用一个useRef钩子。图 2-1 显示了应用的样子。用户将能够输入一个待办事项,并将其存储在 firebase 数据库中一个可爱的列表中。所以,这个列表是永久的,不会在浏览器刷新后改变。

img/512002_1_En_2_Fig1_HTML.jpg

图 2-1

完成的应用

入门指南

首先,使用create-react-app命令创建一个名为todo-react-firebase的新应用。具体来说,命令如下:

npx create-react-app todo-react-firebase

初始 Firebase 设置

由于我们的前端站点也将通过 Firebase 托管,我们将创建基本设置,而create-react-app命令创建我们的 React 应用。按照第一章中的相同步骤设置 Firebase。

在设置程序中点击继续到控制台按钮后,还需要一个额外的设置步骤。您需要向下滚动并点击配置单选按钮,然后复制firebaseConfig部分的所有数据。这是必需的,因为我们将在我们的项目中使用 Firebase 数据库(图 2-2 )。

img/512002_1_En_2_Fig2_HTML.jpg

图 2-2

Firebase 配置

现在,打开 VS Code 中的代码,在src文件夹中创建一个名为firebase.js的文件。将以下代码粘贴到文件中:

const firebaseConfig = {
        apiKey: "AIxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        authDomain: "todo-react-xxxxxxxx.firebaseapp.com",
        projectId: "todo-react-xxxxxx",
        storageBucket: "todo-react-xxxxxxxxxxxxxxxxx.com",
        messagingSenderId: "33xxxxxxxxxxx",
        appId: "1:xxxxxxxxxxxx:9xxxxxxxxxxxxx6d0"
};

基本 React 设置

现在,我们将对 ReactJS 进行基本设置。在todo-react-firebase目录中,用npm start启动 React 应用。接下来,我们将删除一些文件,因为我们不需要它们。它们实际上是标志和其他测试的一部分,我们不会在这个项目中使用。图 2-3 显示要删除的文件。

img/512002_1_En_2_Fig3_HTML.jpg

图 2-3

删除选项

我们将删除所有不必要的样板代码,因此我们的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文件只包含“TODO React Firebase”文本,如下所示:

import './App.css';

function App() {
    return (
        <div className="App">
        <h1>TODO React Firebase</h1>
        </div>
    );
}

export default App;

现在,我们的应用在 localhost 中将看起来如图 2-4 所示。

img/512002_1_En_2_Fig4_HTML.jpg

图 2-4

本地主机应用

本地待办事项列表

完成前一部分的设置后,我们将继续我们的待办事项应用。我们将更新我们的App.js文件,以包含基本待办事项列表的逻辑。这里,我们使用两个状态变量:todosinput。我们使用useState钩子来声明它们。todos包含一个包含两个字符串的数组,input包含一个空字符串。

接下来,在return语句中,我们使用 React 的受控输入来更新输入框的输入。接下来,我们有一个按钮和一个分配给该按钮的点击事件。当我们点击它时,我们运行一个名为addTodo()的函数,用setTodos改变todos的状态。这里,它将已经存在的内容附加到用户键入的内容中。

我们使用一个表单来包装我们的输入和按钮,按钮类型是submit。因此,如果我们在输入框中键入任何内容,然后按键盘上的 Enter 键,它就会工作。因此,我们需要在addTodo()函数中使用e.preventDefault()

import { useState } from 'react';
import './App.css';

function App() {
const [todos, setTodos] = useState([
    'Make a react firebase project',
    'Record a coding video'
])

const [input, setInput] = useState('')

const addTodo = e => {
    e.preventDefault()
    setTodos([...todos, input])
    setInput('')
}

return (
    <div className="App">
        <h1>TODO React Firebase</h1>
        <form>
            <input value={input} onChange={e => setInput(e.target.value)}/>
            <button type="submit" onClick={addTodo}>Add Todo</button>
        </form>
        <ul>
            {todos.map(todo => <li>{todo}</li>)}
        </ul>
    </div>
);
}

export default App;

现在,在 localhost 中,我们将默认获得两个项目,因为它们处于我们的初始状态todos。但是当我们输入的时候,我们会得到新的条目,如图 2-5 所示。

img/512002_1_En_2_Fig5_HTML.jpg

图 2-5

本地主机中的列表

我们将为图标使用材质用户界面。因此,根据文档,我们需要运行两个npm install命令。我们将通过集成终端安装coreicons,如下图所示:

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

现在,我们将在我们的项目中使用material-ui中的图标。我们已经用来自material-uiButtonInput替换了我们的ButtonInput字段,并且我们在顶部导入了它们。更新后的代码在这里用粗体标记:

import { Button, FormControl, Input, InputLabel } from '@material-ui/core';

function App() {
...
...

return (
    <div className="App">
    <h1>TODO React Firebase</h1>
    <form>
              <FormControl>
               <InputLabel>Write a TODO</InputLabel>
               <Input value={input} onChange={e => setInput(e.target.value)}/>
              </FormControl>
              <Button type="submit" onClick={addTodo} variant="contained" color="primary" disabled={!input}>Add Todo</Button>
    </form>
    <ul>
           {todos.map(todo => <li>{todo}</li>)}
    </ul>
    </div>
);
}

export default App;

现在,我们的 web 应用看起来不错(图 2-6 )。

img/512002_1_En_2_Fig6_HTML.jpg

图 2-6

更新的 web 应用

接下来,我们将待办事项列表移动到一个单独的组件中。因此,在components文件夹中创建一个名为Todo.js的新文件。我们会将单独的待办事项作为props发送给它。更新后的代码在这里以粗体显示:

import { Button, FormControl, Input, InputLabel } from '@material-ui/core';
import Todo from './components/Todo';

function App() {
...
...

return (
    <div className="App">
    <h1>TODO React Firebase</h1>
    <form>
    ...
    ...
    </form>
    <ul>
            {todos.map(todo => <Todo todo={todo} />)}
    </ul>
    </div>
);
}

export default App;

现在将下面的代码添加到Todo.js文件中。我们只是使用了一堆material-ui图标,显示了被称为todoprops。这些图标帮助我们使列表项更漂亮。

import { List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core'
import React from 'react'

const Todo = ({ todo }) => {
    return (
        <List className="todo__list">
            <ListItem>
                <ListItemAvatar />
                <ListItemText primary={todo} secondary={todo} />
            </ListItem>
        </List>
    )
}

export default Todo

现在,在 localhost 中,我们将能够看到这些变化,并且我们的列表将看起来不错(图 2-7 )。

img/512002_1_En_2_Fig7_HTML.jpg

图 2-7

所有列表

现在,是时候将 Firebase 连接到项目上了。

使用 Firebase

现在,我们将开始为后端设置 Firebase。为此,我们将首先通过运行以下命令在终端中安装 Firebase 的所有依赖项:

npm i firebase

接下来,我们将更新我们的firebase.js文件以使用配置来初始化应用。之后,我们使用 Firestore 作为数据库。更新后的代码在这里以粗体突出显示:

import firebase from 'firebase'

const firebaseConfig = {
    ...
    ...
};

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

export { db }

现在,我们将返回到 Firebase,点击云 Firestore ,然后点击创建数据库按钮,如图 2-8 所示。

img/512002_1_En_2_Fig8_HTML.jpg

图 2-8

创建数据库

在下一个画面中,在 测试模式下选择开始,然后点击下一个按钮,如图 2-9 所示。

img/512002_1_En_2_Fig9_HTML.jpg

图 2-9

测试模式

在下一个屏幕上,点击使能按钮(图 2-10 )。

img/512002_1_En_2_Fig10_HTML.jpg

图 2-10

启用按钮

在下一个界面,点击开始采集,如图 2-11 所示。

img/512002_1_En_2_Fig11_HTML.jpg

图 2-11

开始收集

将会打开如图 2-12 所示的弹出窗口。我们需要在收藏 ID 字段中输入 todos ,然后单击下一步按钮。

img/512002_1_En_2_Fig12_HTML.jpg

图 2-12

待办事项

在下一个屏幕上,点击自动 ID 填写文档 ID 字段。在字段中输入 todo 。之后,点击保存按钮(图 2-13 )。

img/512002_1_En_2_Fig13_HTML.jpg

图 2-13

菲尔茨

这将把我们带回主屏幕。现在点击添加文档链接。这将再次打开同一个弹出窗口,我们将在其中添加另一个待办事项的详细信息。现在,我们的数据库中有两个待办事项(图 2-14 )。

img/512002_1_En_2_Fig14_HTML.jpg

图 2-14

两个待办事项

将 Firebase 添加到应用

现在我们将删除App.js中的硬编码待办事项,并使用来自 Firebase 数据库的数据。因此,回到App.js,删除todosuseState代码中的硬编码内容。我们还创建了所需的导入。

之后,在useEffect内,我们调用集合todos,然后我们拍摄快照。用 Firebase 的术语来说,它是实时数据,我们可以立即获得。然后,我们将通过setTodos()todos数组中设置这个数据。

另外,请注意useEffect在数组中有input。因此,任何时候用户添加一个todo,它都会立即显示在我们的应用中。

另外,请注意,我们已经使用todos改变了循环数据的方式。这样做是因为我们接收的数据是一个对象数组。更新后的代码在这里以粗体显示:

import { useState, useEffect } from 'react';
import Todo from './components/Todo';
import { db } from './firebase';

function App() {
const [todos, setTodos] = useState([])
const [input, setInput] = useState('')
useEffect(() => {
  db.collection('todos').onSnapshot(snapshot => {
     setTodos(snapshot.docs.map(doc => doc.data()))
  })
}, [input])

...
...
return (
   <div className="App">
   <h1>TODO React Firebase</h1>
         ...
         ...
   <ul>
   {todos.map(({ todo }) => <Todo todo={todo} />)}
   </ul>
   </div>
);
}

export default App;

现在,我们将添加功能,以便用户可以添加待办事项。为此,我们只需要使用add()将输入添加到集合中。另外,请注意,我们在添加待办事项的同时添加了服务器时间戳。我们这样做是因为我们需要按降序排列待办事项。更新后的代码在这里用粗体标记:

import { db } from './firebase';
import firebase from 'firebase';

function App() {
const [todos, setTodos] = useState([])
const [input, setInput] = useState('')
useEffect(() => {
  db.collection('todos').orderBy('timestamp','desc').onSnapshot(snapshot => {
    setTodos(snapshot.docs.map(doc => doc.data()))
  })
}, [input])

const addTodo = e => {
      e.preventDefault()
      db.collection('todos').add({
      todo: input,
      timestamp: firebase.firestore.FieldValue.serverTimestamp()
      })
      setInput('')
}

...
...

现在,我们需要删除 Firebase 中的旧集合,因为没有一个记录有时间戳(图 2-15 )。

img/512002_1_En_2_Fig15_HTML.jpg

图 2-15

删除收藏

它还会显示一个弹出窗口来确认这一点(图 2-16 )。

img/512002_1_En_2_Fig16_HTML.jpg

图 2-16

删除前确认

现在,我们还想获得键和删除所需的每个项目的 ID,这是我们将要实现的。这个键在 React for optimization 中是必不可少的,我们还会在控制台中得到一个警告。所以,我们需要改变我们在setTodos()中设置数据的结构。

现在,我们以不同的方式映射它,特别是当我们将单个项目传递给一个Todo组件时。下面是更新后的代码:

function App() {
...
useEffect(() => {
  db.collection('todos').orderBy('timestamp','desc').onSnapshot(snapshot => {
           setTodos(snapshot.docs.map(doc => ({
           id: doc.id,
          item: doc.data()
          })))
  })
}, [input])

...
console.log(todos);
return (
    <div className="App">
    <h1>TODO React Firebase</h1>
           ...
           ...
    <ul>
    {todos.map(it => <Todo key={it.id} arr={it} />)}
    </ul>
    </div>
);
}

export default App;

现在,在Todo.js文件中,我们得到了一个不同的结构,我们正在为此更新我们的文件。

我们还添加了删除功能,在该功能中,我们必须获取项目的 ID 并调用delete()。更新后的代码在这里用粗体标记:

import { db } from '../firebase'
import DeleteForeverIcon from '@material-ui/icons/DeleteForever'

const Todo = ({ arr }) => {
          return (
          <List className="todo__list">
          <ListItem>
                       <ListItemAvatar />
                       <ListItemText
                        primary={arr.item.todo}
                        secondary={arr.item.todo}
                        />
     </ListItem>
     <DeleteForeverIcon
             onClick={() => {db.collection('todos').doc(arr.id).delete()}}
     />
     </List>
     )
}

export default Todo

现在,在 localhost 中,我们可以添加和删除任何项目。另外,请注意控制台日志中的结构(图 2-17 )。

img/512002_1_En_2_Fig17_HTML.jpg

图 2-17

控制台日志

我们完成了应用,只剩下样式。现在让我们把它变得更漂亮。在App.js文件中,将className改为app。更新后的代码在这里用粗体标记:

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

export default App;

接下来,在App.css文件中,删除所有内容并插入如下所示的内容:

.app {
    display:grid;
    place-items: center;
}

现在,在Todo.js文件中,为Todo.css文件添加导入。另外,将删除图标的fontSize设置为large。更新后的代码在这里用粗体标记:

import './Todo.css'

const Todo = ({ arr }) => {
    return (
    <List className="todo__list">
             ...
    <DeleteForeverIcon fontSize='large'
             onClick={() => {db.collection('todos').doc(arr.id).delete()}}
    />
    </List>
    )
}

接下来,在Todo.css文件中,添加以下内容:

.todo__list{
    display:flex;
    justify-content: center;
    align-items: center;
    width: 800px;
    border: 1px solid lightgray;
    margin-bottom: 10px !important;
}

现在,在 localhost,这个应用看起来很完美(图 2-18 )。

img/512002_1_En_2_Fig18_HTML.jpg

图 2-18

添加样式后的应用

部署火力基地

为了部署应用,我们将遵循第一章中的相同步骤。完成后,我们可以看到从终端成功部署了 app(图 2-19 )。

img/512002_1_En_2_Fig19_HTML.jpg

图 2-19

完整的应用

摘要

在本章中,你创建了一个漂亮的待办事项应用。该应用的数据存储在 Firebase Firestore 数据库中,它甚至具有删除功能。

三、使用 React 和 Firebase 构建故事应用

在本章中,您将学习如何在 ReactJS 中构建一个 stories 应用。如今,故事应用非常受欢迎,每个大型社交媒体平台都有能力让用户将短视频故事添加到他们的平台上。在我们的应用中,我们将能够滚动存储在 Firebase Firestore 数据库中的短视频。最终的应用将如图 3-1 所示。

img/512002_1_En_3_Fig1_HTML.jpg

图 3-1

正在使用的最终应用

主机和数据库将在 Firebase。我们也将在项目中使用图标的材料用户界面。

因此,使用create-react-app命令创建一个名为stories-firebase-app的新应用。具体来说,打开任何终端并运行以下命令:

npx create-react-app stories-firebase-app

初始 Firebase 设置

由于我们的前端站点也将通过 Firebase 托管,我们将创建基本设置,而create-react-app命令创建我们的 React 应用。继续按照第一章中列出的步骤创建应用。我已经创建了一个名为stories-firebase-app的应用(图 3-2 )。

img/512002_1_En_3_Fig2_HTML.jpg

图 3-2

创建应用

现在,点击屏幕左上角的设置图标。之后点击项目设置按钮(图 3-3 )。

img/512002_1_En_3_Fig3_HTML.jpg

图 3-3

设置

现在,向下滚动,点击配置单选按钮,然后复制firebaseConfig部分的所有代码(图 3-4 )。

img/512002_1_En_3_Fig4_HTML.jpg

图 3-4

。firebaseConfig 程式码

基本 React 设置

我们的 React 设置应该已经完成了。所以,回到终端,将cd放入新创建的stories-firebase-app目录。

之后,在 VS 代码中打开目录,在src文件夹中创建一个名为firebase.js的文件,并将之前 Firebase 屏幕中的内容粘贴到那里。代码如下所示:

const firebaseConfig = {
        apiKey: "AIxxxxxxxxxxxxxxxxxxxxxxxxxxKT4",
        authDomain: "stories-xxxxxx.xxxxxxxx.com",
        projectId: "stories-xxxxxx",
        storageBucket: "stories-fxxxxxxxx.com",
        messagingSenderId: "50xxxxxxxx63",
        appId: "1:507xxxxxxx63:web:0c9xxxxxxxxxxda8e"
};

stories-firebase-app目录下,用npm start启动 React app。接下来,我们将删除一些文件,如图 3-5 所示,因为我们不需要它们。

img/512002_1_En_3_Fig5_HTML.jpg

图 3-5

删除一些代码

我们将删除所有不必要的样板代码,因此我们的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文件只包含“Stories app React”文本。我们已经从App.css文件中删除了所有其他内容。这里显示了App.css文件的更新代码:

import './App.css';

function App() {
    return (
        <div className="app">
        <h1>Stories app React</h1>
        </div>
    );
}
export default App;

index.css文件中,更新 CSS 以通篇使用margin: 0。具体来说,在顶部添加以下代码:

* {
   margin: 0;
}

应用的基本结构

我们现在将在应用中创建基本结构。所以,用下面的内容更新App.js。我们首先添加图像和标题。这里,我们创建了两个div:app__topapp__videos。现在,app__top包含一个图像和一个h1

import './App.css';

function App() {
    return (
        <div className="app">
        <div className="app__top">
            <img src="logo192.png" alt="App Logo" className="app__logo"/>
            <h1>Shorts</h1>
        </div>
        <div className="app__videos">

        </div>
        </div>
    );
}

export default App;

接下来,我们将在App.css文件中添加新内容。在这里,我们使用网格将所有东西放置在中心。我们在两个地方也有scroll-snap-type: y mandatory的风格。它用于在我们的应用中提供平滑滚动的滚动功能。

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

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

.app__top {
    margin-bottom: -150px;
}
.app__top > h1 {
    text-align: center;
    color: white;
}

.app__logo {
    height: 12vh;
}

.app__videos {
    position:relative;
    height: 65vh;
    background-color: white;
    width: 70%;
    border-radius: 20px;
    max-width: 450px;
    max-height: 1200px;
    overflow: scroll;
    scroll-snap-type: y mandatory;
}

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

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

现在,我们的应用在 localhost 中将看起来如图 3-6 所示。

img/512002_1_En_3_Fig6_HTML.jpg

图 3-6

应用大纲

在应用中显示短视频

在上一节设置了基本布局后,我们现在将开始创建在我们的应用中显示短视频的功能。

首先,在src文件夹中创建一个components文件夹,并在src文件夹中创建两个名为VideoCard.jsVideoCard.css的文件。

接下来,在VideoCard.js文件中,放置video标签和一个垂直视频链接。我从我的频道上的一个简短的 YouTube 视频中复制了链接。

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

const VideoCard = () => {
    return (
        <div className="videoCard">
            <video
                src="https://res.cloudinary.com/dxkxvfo2o/video/upload/v1608169738/video1_cvrjfm.mp4"
                className="videoCard__player"
                alt="Short Video App"
                loop
            />
        </div>
    )
}

export default VideoCard

现在,我们将在VideoCard.css文件中添加以下代码。这里,我们再次需要添加scroll-snap-align: start来实现视频的平滑滚动功能。

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

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

现在,在App.js中,添加两个VideoCard组件,因为我们需要不止一个视频来查看捕捉功能。更新后的代码在这里以粗体显示:

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

function App() {
         return (
         <div className="app">
         <div className="app__top">
                 ...
         </div>
         <div className="app__videos">
                 <VideoCard />
                 <VideoCard />
          </div>
         </div>
         );
}

export default App;

现在,视频通过抓拍功能完美显示(图 3-7 )。

img/512002_1_En_3_Fig7_HTML.jpg

图 3-7

视频抓拍

现在我们的视频无法播放,因为我们还没有实现onClick功能。为了让它们播放,我们需要使用一个引用(或 ref )。ref 是必需的,因为我们将实现用户在屏幕上单击鼠标时的暂停和播放功能。我们将首先导入useRefuseState钩子,然后我们将添加一个videoRef变量。我们在video元素中使用了videoRef,我们还创建了一个触发函数handleVideoPressonClick处理程序。

handleVideoPress函数中,我们使用一个名为playing的状态变量来检查视频是否正在播放。我们将videoRef.current.pause()设置为暂停,并将playing状态改为false。我们在else部分做相反的事情。更新后的代码在这里用粗体标记:

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

const VideoCard = () => {
        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="videoCard">
                  <video
                  ...
                  loop
                  ref={videoRef}
                  onClick={handleVideoPress}
                  />
        </div>
       )
}

export default VideoCard

现在,在 localhost 中,只需点击视频,它就会播放。再次点按以暂停播放。

创建标题组件

我们将为图标使用材质 UI,这是我们接下来要用到的。因此,我们需要根据文档做两个npm install。我们将使用以下命令通过终端安装coreicons:

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

我们现在将为我们的视频组件创建标题。因此,在components文件夹中创建名为VideoHeader.jsVideoHeader.css的文件。

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

const VideoHeader = () => {
         return (
         <div className="videoHeader">

         </div>
         )
}

export default VideoHeader

现在,在VideoHeader.js文件中,放入以下内容。这里,我们使用material-ui来显示两个图标:ArrowBackIosCameraAltOutlined。更新的内容在此处标记为粗体:

import React from 'react'
import './VideoHeader.css'
import ArrowBackIosIcon from '@material-ui/icons/ArrowBackIos'
import CameraAltOutlinedIcon from '@material-ui/icons/CameraAltOutlined'

const VideoHeader = () => {
    return (
        <div className="videoHeader">
            <ArrowBackIosIcon />
            <h3>Shorts</h3>
            <CameraAltOutlinedIcon />
        </div>
    )
}

export default VideoHeader

接下来,我们将在VideoHeader.css文件中设置这些样式。

.videoHeader {
    display: flex;
    justify-content: space-between;
    align-items: center;
    position: absolute;
    width: 100%;
    color: white;
}

.videoHeader > * {
    padding: 20px;
}

现在,将这个VideoHeader组件包含在VideoCard.js文件中。更新的内容在此处标记为粗体:

import VideoHeader from './VideoHeader'

const VideoCard = () => {
    ...
    ...
        return (
        <div className="videoCard">
                <VideoHeader />
                  <video
                          ...
                  />
     </div>
     )
}

export default VideoCard

现在,在 localhost 中,我们看到了我们漂亮的 header 组件(图 3-8 )。

img/512002_1_En_3_Fig8_HTML.jpg

图 3-8

页眉

创建页脚组件

我们现在将为视频组件创建一个页脚。页脚组件将在应用的页脚显示一些图标。因此,在components文件夹中创建两个名为VideoFooter.jsVideoFooter.css的文件。

此外,我们还通过在VideoCard组件中传递来自App.js文件的属性进行了一些优化。我们传递两组不同的属性,在两个VideoCard组件中。更新的内容在这里用粗体标记:

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

function App() {
         return (
         <div className="app">
         <div className="app__top">
                  ...
         </div>
        <div className="app__videos">
        <VideoCard
                url="https://res.cloudinary.com/dxkxvfo2o/video/upload/v1608169738/video1_cvrjfm.mp4"
                channel="TWD"
                avatarSrc="https://pbs.twimg.com/profile_images/1020939891457241088/fcbu814K_400x400.jpg"
                song="I am a Windows PC"
                likes={950}
                shares={200}
         />

         <VideoCard
               url="https://res.cloudinary.com/dxkxvfo2o/video/upload/v1608169739/video2_mecbdo.mp4"
               channel="nabendu"
               avatarSrc="https://pbs.twimg.com/profile_images/1020939891457241088/fcbu814K_400x400.jpg"
               song="I am a good PC"
                likes={850}
                shares={150}
         />
         </div>
         </div>
        );
}

export default App;

那么在VideoCard.js文件中,我们将首先使用video中名为url的属性。此外,调用新的VideoFooter组件,在那里我们将传递其余的属性。更新的内容在这里用粗体标记:

import VideoFooter from './VideoFooter'
import VideoHeader from './VideoHeader'

const VideoCard = ({ url, channel, avatarSrc, song, likes, shares }) => {
    ...
    ...
       return (
       <div className="videoCard">
                <VideoHeader />
                <video
                src={url}
                className="videoCard__player"
                alt="Short Video App"
                loop
                ref={videoRef}
                onClick={handleVideoPress}
                />
                <VideoFooter
                channel={channel}
                likes={likes}
                shares={shares}
                avatarSrc={avatarSrc}
                song={song}
                />
       </div>
      )
}

export default VideoCard

现在,我们的VideoFooter.js文件将包含以下内容。我们使用了channelavatarSrc属性,展示了一个头像和一个频道名称。

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

const VideoFooter = ({ channel, avatarSrc, song, likes, shares }) => {
    return (
        <div className='videoFooter'>
            <div className="videoFooter__text">
                <Avatar src={avatarSrc} />
                <h3>
                    {channel} . <Button>Follow</Button>
                </h3>
            </div>
        </div>
    )
}

export default VideoFooter

接下来,我们将在VideoFooter.css文件中添加这些样式。

.videoFooter__text{
    position: absolute;
    bottom: 0;
    color: white;
    display: flex;
    margin-bottom: 20px;
}

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

.videoFooter__text > h3 > button {
    color: white;
    font-weight: 900;
    text-transform: inherit;
}

现在,在 localhost 中我们将开始设置页脚组件(图 3-9 )。

img/512002_1_En_3_Fig9_HTML.jpg

图 3-9

页脚

现在,让我们在项目中创建一个漂亮的 ticker。为此,我们将在项目中安装一个名为react-ticker的包。这个包允许我们显示移动的文本,比如新闻提要。我们可以使用集成终端通过以下命令进行安装:

npm i react-ticker

接下来,我们将根据文档将TickerMusicNoteIcon一起包含在我们的VideoFooter.js文件中,如下所示:

import MusicNoteIcon from '@material-ui/icons/MusicNote'
import Ticker from 'react-ticker'

const VideoFooter = ({ channel, avatarSrc, song, likes, shares }) => {
     return (
     <div className='videoFooter'>
     <div className="videoFooter__text">
             ..
     </div>
     <div className="videoFooter__ticker">
                   <MusicNoteIcon className="videoFooter__icon" />
                   <Ticker mode="smooth">
                   {({ index }) => (
                   <>

                   <h1>{song}</h1>
                   </>

                   )}
                   </Ticker>

     </div>
     </div>
     )
}

export default VideoFooter

接下来,我们将在VideoFooter.css文件中包含以下样式:

    .videoFooter{
        position: relative;
        bottom: 100px;
        margin-left: 20px;
    }

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

    .videoFooter__ticker h1{
        padding-top: 7px;
        font-size: 12px;
        color: white;
    }

    .videoFooter__icon{
        position: absolute;
        left: 5px;
        color: white;
    }

现在,我们将看到这个漂亮的滚动条在本地主机的屏幕上滚动(图 3-10 )。

img/512002_1_En_3_Fig10_HTML.jpg

图 3-10

心脏

现在,我们将在VideoFooter.js文件中添加一些剩余的元素来完成我们的应用。这里,我们添加了更多的图标,并使用了likesshares属性:

import Ticker from 'react-ticker'
import { Favorite, ModeComment, MoreHoriz, Send } from '@material-ui/icons'

const VideoFooter = ({ channel, avatarSrc, song, likes, shares }) => {
     return (
     <div className='videoFooter'>
             <div className="videoFooter__text">
                   ...
             </div>
             <div className="videoFooter__ticker">
                   ...
            </div>
            <div className="videoFooter__actions">
            <div className="videoFooter__actionsLeft">
            <Favorite fontSize="large" />
            <ModeComment fontSize="large" />
            <Send fontSize="large" />
            <MoreHoriz fontSize="large" />
            </div>
            <div className="videoFooter__actionsRight">
            <div className="videoFooter__stat">
                    <Favorite />
                    <p>{likes}</p>
            </div>
            <div className="videoFooter__stat">
                    <ModeComment />
                    <p>{shares}</p>
            </div>
            </div>
            </div>
     </div>
     )
}

export default VideoFooter

接下来,我们将在VideoFooter.css文件中添加一些新的样式,如下所示:

.videoFooter__actions{
    display: flex;
    position: absolute;
    width: 95%;
    color: white;
    justify-content: space-between;
}

.videoFooter__actionsLeft > .MuiSvgIcon-root{
    padding: 0 10px;
}

.videoFooter__actionsRight{
    display: flex;
}

.videoFooter__stat{
    display: flex;
    align-items: center;
    margin-right: 10px;
}

.videoFooter__stat > p{
    margin-left: 3px;
}

现在,我们的应用完成了我们刚刚添加的附加元素(图 3-11 )。

img/512002_1_En_3_Fig11_HTML.jpg

图 3-11

应用完成

设置 Firebase 数据库

我们现在将建立燃烧基地。首先要做的是通过从终端运行以下命令在我们的项目中安装 Firebase:

npm i firebase

接下来,我们将更新我们的firebase.js文件以使用配置来初始化应用。之后,我们可以使用 Firestore 作为数据库。此处的更新内容以粗体标记:

import firebase from 'firebase';

const firebaseConfig = {
    ...
};

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

export default db

现在,我们将返回 Firebase 并单击云 Firestore,然后单击创建数据库按钮(图 3-12 )。

img/512002_1_En_3_Fig12_HTML.jpg

图 3-12

创建数据库

在下一个屏幕上,在 测试模式下选择开始,然后点击下一步按钮(图 3-13 )。

img/512002_1_En_3_Fig13_HTML.jpg

图 3-13

测试模式

在下一个屏幕上,点击使能按钮(图 3-14 )。

img/512002_1_En_3_Fig14_HTML.jpg

图 3-14

启用按钮

在下一个屏幕上,点击开始采集(图 3-15 )。

img/512002_1_En_3_Fig15_HTML.jpg

图 3-15

开始收集

这将打开如图 3-16 所示的弹出窗口。我们需要给出收藏 ID,所以在收藏 ID 字段中输入视频,然后点击下一步按钮。

img/512002_1_En_3_Fig16_HTML.jpg

图 3-16

输入收藏 ID

在下一个屏幕上,点击自动 ID 创建文档 ID。还要添加字段网址频道avatarSrc歌曲喜欢分享。将App.js文件中的所有值输入值字段。另外,请注意,喜欢、分享和消息是number类型,其余是string类型。之后点击保存按钮(图 3-17 )。

img/512002_1_En_3_Fig17_HTML.jpg

图 3-17

创建收藏

这将把我们带回到主屏幕。现在点击添加文档链接。这将再次打开我们之前看到的同一个弹出窗口,在这里我们将添加来自App.js文件的另一个视频的细节(图 3-18 )。

img/512002_1_En_3_Fig18_HTML.jpg

图 3-18

另一个系列

现在,我们的数据库中有两个视频(图 3-19 )。

img/512002_1_En_3_Fig19_HTML.jpg

图 3-19

两个视频

将 Firebase 数据库与 React 集成

现在,返回到App.js并使用useState钩子创建一个名为 videos 的新状态。然后我们将映射它,并将参数传递给VideoCard组件。请注意,我们已经删除了硬编码的内容,因为我们不会从数据库中获得这些数据。更新的内容在这里用粗体标记:

import { useState } from 'react';
import './App.css';
import VideoCard from './components/VideoCard';

function App() {
  const [videos, setVideos] = useState([])

  return (
         <div className="app">
         <div className="app__top">
         ...
         </div>
         <div className="app__videos">
                 {videos.map(({ url, channel, avatarSrc, song, likes, shares }) => (
                 <VideoCard
                        url={url}
                        channel={channel}
                        avatarSrc={avatarSrc}
                        song={song}
                        likes={likes}
                        shares={shares}
                 />
                 ))}
       </div>
       </div>
  );
}

export default App;

现在,我们将在应用中使用存储在本地 Firebase 文件中的数据。之后,在useEffect中,我们调用集合videos,然后拍摄快照。用 Firebase 的术语来说,这是实时数据,我们可以立即获得。然后,我们将通过setVideos()在视频数组中设置这些数据。

另外,请注意useEffect在数组中有视频。因此,每当一个新的视频被添加到 Firebase 数据库中,它就会立即显示在我们的应用中。更新的内容在这里用粗体标记:

import { useEffect, useState } from 'react';
import './App.css';
import VideoCard from './components/VideoCard';
import db from './firebase';

function App() {
  const [videos, setVideos] = useState([])

  useEffect(() => {
     db.collection('videos').onSnapshot(snapshot => {
     setVideos(snapshot.docs.map(doc => doc.data()))
    })
  }, [videos])

  return (
  ...
  )

现在,我们的应用已经完成,我们正在从 Firebase 后端获取数据(图 3-20 )。

img/512002_1_En_3_Fig20_HTML.jpg

图 3-20

从数据库获取数据

通过 Firebase 部署和托管

现在,我们可以在 Firebase 中部署我们的应用。我们只是按照前面描述的相同步骤。

部署成功,我们的应用工作正常(图 3-21 )。

img/512002_1_En_3_Fig21_HTML.jpg

图 3-21

部署的应用

摘要

在本章中,您学习了如何创建故事视频应用。该应用的数据存储在 Firebase Firestore 数据库中,它还有一个很好的滚动功能。

四、使用 React 和 Firebase 构建存储应用

欢迎来到新的 ReactJS 项目,我们将在 ReactJS 中构建一个存储应用。存储应用用于将您的数据存储在云中作为备份。有许多流行的存储应用,如 Dropbox 和 Google Drive。

主机和数据库将在 Firebase。该应用的登录将通过谷歌认证。我们也将在项目中使用图标的材料用户界面。最终项目将如图 4-1 所示。

img/512002_1_En_4_Fig1_HTML.jpg

图 4-1

已完成项目

入门指南

使用create-react-app命令创建一个名为storage-firebase-app的新应用。具体来说,打开任何终端并提供以下命令:

npx create-react-app storage-firebase-app

初始 Firebase 设置

由于我们的前端站点也将通过 Firebase 托管,我们将创建基本设置,而这个create-react-app命令创建我们的 React 应用。在这一部分,我们将遵循与第一章相同的步骤。我已经创建了一个名为storage-firebase-app的应用(图 4-2 )。

img/512002_1_En_4_Fig2_HTML.jpg

图 4-2

创建存储应用

现在,点击屏幕左上角的设置图标。之后点击项目设置按钮,如图 4-3 。

img/512002_1_En_4_Fig3_HTML.png

图 4-3

项目设置

现在,向下滚动,点击配置单选按钮,然后复制firebaseConfig元素的所有代码,如图 4-4 所示。

img/512002_1_En_4_Fig4_HTML.jpg

图 4-4

火基配置

基本 React 设置

我们现在将完成 React 设置。所以,回到终端,将cd放入新创建的storage-firebase-app目录。

之后,在 VS 代码中打开目录,在src文件夹中创建一个名为firebase.js的文件。将前面 Firebase 屏幕中的以下内容粘贴到该文件中:

const  xxxxxxConfig = {
     apiKey: "AIXXXXXXXXXXXXXXXXXXXXXXXXXX",
     authDomain: "storage-XXXXXXXX.XXXXXXXXX.com",
     projectId: "storage- xxxxxx-app",
     storageBucket: "storage- xxxxxx-app.appspot.com",
     messagingSenderId: "14xxxxxxxx",
     appId: "1:142xxxxxxxxxxxxx:web:6xxxxxxxxxxx"
};

storage-firebase-app目录中,用npm start启动 React 应用。接下来,我们将删除一些文件,因为我们不需要它们。我们正在删除它们,因为它们显示了 React 徽标和其他东西,需要在开始项目之前进行清理。图 4-5 显示要删除的文件。

img/512002_1_En_4_Fig5_HTML.jpg

图 4-5

删除文件

我们将删除所有不必要的样板代码,我们的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文件仅包含“存储应用 React”文本。我们还删除了App.css文件中的所有内容。

import './App.css';

        function App() {
            return (
                <div className="app">
                <h1>Storage app React</h1>
                </div>
            );
        }
        export default App;

index.css文件中,更新 CSS 以对所有内容使用margin: 0,如下所示:

*{
  margin: 0;
}

创建标题

我们的 React 设置已经完成,我们将首先处理Header组件。因此,在src文件夹中创建一个名为components的文件夹。在components文件夹中创建一个Header.js文件。我们将首先将其导入到App.js文件中。

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

我们也将为图标使用材质界面。因此,我们需要根据文档做两个npm install

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

现在,我们的Header.js文件大部分是静态的。它将主要包含图标和标志。

这里我们有一个div叫做header,包含三个div,第一个是header__logo,包含一个图片和文字。下一个divheader__searchContainer,包含一个SearchIcon,一个输入框,和ExpandMoreIcon

第三个div,称为header__icons,包含四个图标:HelpOutlineIconSettingsIconAppsIconAvatar

import React from 'react'
import SearchIcon from '@material-ui/icons/Search'
import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
import HelpOutlineIcon from '@material-ui/icons/HelpOutline'
import SettingsIcon from '@material-ui/icons/Settings'
import AppsIcon from '@material-ui/icons/Apps'
import { Avatar } from '@material-ui/core'
import './Header.css'

const Header = () => {
    return (
        <div className="header">
            <div className="header__logo">
                <img src="logo192.png" alt="logo" />
                <span>Storage</span>
            </div>
            <div className="header__searchContainer">
                <div className="header__searchBar">
                    <SearchIcon />
                    <input type="text" placeholder='Search in Storage' />
                    <ExpandMoreIcon />
                </div>
            </div>
            <div className="header__icons">
                <span>
                    <HelpOutlineIcon />
                    <SettingsIcon />
                </span>
                <AppsIcon />
                <Avatar className="header__iconsAvatar" />
            </div>
        </div>
    )
}

export default Header

现在,在同一个文件夹中创建一个名为Header.css的文件,并向其中添加以下内容。这里,我们使用了很多 flexboxes 来设计我们的页眉。

.header {
    display: flex;
    height: 60px;
    border-bottom: 1px solid rgb(219, 219, 219);
    width: 100vw;
}

.header>div {
    padding: 12px;
}

.header__logo {
    display: flex;
    justify-content: flex-start;
    align-items: center;
}

.header__logo>img {
    height: 100%;
    object-fit: contain;
}

.header__logo>span {
    color: gray;
    font-size: 20px;
    font-weight: 500;
    margin-left: 16px;
}

.header__searchContainer {
    flex: 1;
    display: flex;
    align-items: center;
    padding: 8px;
}

.header__searchBar {
    width: 45%;
    height: 120%;
    border-radius: 6px;
    background-color: rgb(237, 237, 237);
    display: flex;
    align-items: center;
    padding: 0 8px;
}

.MuiSvgIcon-root {
    color: rgb(82, 82, 82);
}

.header__searchBar>input {
    flex: 1;
    height: 60%;
    font-size: 16px;
    color: lightgray;
    background: none;
    border: none;
    margin-left: 12px;
}

.header__searchBar>input:focus {
    outline: none;
    color: black;
}

.header__icons {
    display: flex;
    align-items: center;
    margin-right: -30px;
}

.header__icons .MuiSvgIcon-root {
    font-size: 28px;
    color: rgb(82, 82, 82);
    margin: 4px;
}

.header__icons>span{
    margin-right: 20px;
}

.header__iconsAvatar{
    margin-right: 24px;
}

现在,我们的头已经完成,看起来像 localhost 上的图 4-6 。

img/512002_1_En_4_Fig6_HTML.jpg

图 4-6

我们在本地主机上的标题

创建侧栏

现在我们的 header 组件已经完成,我们将创建Siderbar组件。为此,首先将Siderbar组件导入到我们的App.js文件中。这里的代码以粗体显示:

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

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

接下来,在components文件夹中创建一个名为Sidebar.js的文件,并在其中添加以下内容。这里,我们调用两个组件:FileComponentSidebarItem。在SidebarItem组件中,我们也传递属性,其中之一就是图标。

import React from 'react';
import FileComponent from './FileComponent';
import SidebarItem from './SidebarItem';
import InsertDriveFileIcon from '@material-ui/icons/InsertDriveFile';
import ImportantDevicesIcon from '@material-ui/icons/ImportantDevices';
import PeopleAltIcon from '@material-ui/icons/PeopleAlt';
import QueryBuilderIcon from '@material-ui/icons/QueryBuilder';
import StarBorderIcon from '@material-ui/icons/StarBorder';
import DeleteOutlineIcon from '@material-ui/icons/DeleteOutline';
import StorageIcon from '@material-ui/icons/Storage';
import './Sidebar.css';

const Sidebar = () => {
    return (
        <div className="sidebar">
            <FileComponent />
            <div className="sidebar__itemsContainer">
                <SidebarItem arrow icon={(<InsertDriveFileIcon />)} label={'My Drive'} />
                <SidebarItem arrow icon={(<ImportantDevicesIcon />)} label={'Computers'} />
                <SidebarItem icon={(<PeopleAltIcon />)} label={'Shared with me'} />
                <SidebarItem icon={(<QueryBuilderIcon />)} label={'Recent'} />
                <SidebarItem icon={(<StarBorderIcon />)} label={'Starred'} />
                <SidebarItem icon={(<DeleteOutlineIcon />)} label={'Bin'} />
                <hr/>
                <SidebarItem icon={(<StorageIcon />)} label={'Storage'} />
            </div>
        </div>

    )
}

export default Sidebar

现在,在同一个components文件夹中创建一个FileComponent.js文件。它包含AddIconNew图标。

import React from 'react'
import AddIcon from '@material-ui/icons/Add'
import './FileComponent.css'

const FileComponent = () => {
    return (
        <div className="file">
            <div className="file__container">
                <AddIcon fontSize='large' />
                <p>New</p>
            </div>
        </div>
    )
}

export default FileComponent

现在,在components文件夹中创建一个名为SidebarItem.js的文件。需要三个属性:arrowiconlabel。只有当arrow属性通过时,我们才显示ArrowRightIcon

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

import ArrowRightIcon from '@material-ui/icons/ArrowRight';

const SidebarItem = ({ arrow, icon, label }) => {
    return (
        <div className='sidebarItem'>
            <div className="sidebarItem__arrow">
                {arrow && (<ArrowRightIcon />)}
            </div>

            <div className='sidebarItem__main'>
                {icon}
                <p>{label}</p>
            </div>
        </div>

)
}

export default SidebarItem

图 4-7 显示了图标在本地主机上的样子。

img/512002_1_En_4_Fig7_HTML.jpg

图 4-7

本地主机上的图标

现在,是时候改变我们的风格了。因此,在Sidebar.css文件中添加以下代码:

.sidebar{
    width: 15%;
    height: 100vh;
    margin-right: 5px;
}

hr{
    background-color: rgb(197, 197, 197);
    height: 1px;
    border: none;
}

接下来,在SidebarItem.css文件中,添加以下内容:

.sidebarItem{
    display: flex;
    padding: 10px 0;
    border-radius: 0 100px 100px 0;
}

.sidebarItem:hover{
    background-color: rgba(0, 0, 0, 0.04);
}

.sidebarItem__arrow{
    width: 28px;
    margin-left: 12px;
}

.sidebarItem__main{
    display: flex;
}

.sidebarItem__main>p{
    margin-left: 12px;
}

FileComponent.css中,增加以下内容:

.file {
    display: flex;
    align-items: center;
    padding: 12px 0;
    padding-left: 20px;
}

.file__container {
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 6px 32px 6px 8px;
    border-radius: 50px;
    box-shadow: 0 1px 2px 0 rgba(60, 64, 67, 0.302), 0 1px 3px 1px rgba(60, 64, 67, 0.149);
    cursor: pointer;
}

.file__container>p{
    margin-left: 14px;
}

侧边栏看起来很棒,如图 4-8 所示。

img/512002_1_En_4_Fig8_HTML.jpg

图 4-8

补充报道

使用 Firebase 上传文件

我们需要有一些上传文件的逻辑,但为此我们首先需要在我们的项目中有 Firebase。

首先要做的是通过从终端运行以下命令在我们的项目中安装 Firebase:

npm i firebase

接下来,我们将更新我们的firebase.js文件,以使用配置来初始化应用。之后,我们可以使用 Firestore 作为数据库。我们还在项目中使用谷歌认证和存储。

import firebase from 'firebase'

  const firebaseConfig = {
      ...
      ...
  };

  const firebaseApp = firebase.initializeApp(firebaseConfig)

  const auth = firebase.auth()
  const provider = new firebase.auth.GoogleAuthProvider()
  const storage = firebase.storage()
  const db = firebaseApp.firestore()

  export { auth, provider, db, storage }

回到FileComponent.js文件,我们将导入必要的模块。我们在这里使用来自材质 UI 的Modal。更新后的代码在这里用粗体标记:

import React, { useState } from 'react'
import AddIcon from '@material-ui/icons/Add'
import './FileComponent.css'
import firebase from 'firebase'
import { storage, db } from '../firebase'
import { makeStyles } from '@material-ui/core/styles';
import Modal from '@material-ui/core/Modal';
function getModalStyle() {
     return {
     top: `50%`,
     left: `50%`,
     transform: `translate(-50%, -50%)`,
     };
}
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),
     },
}));

const FileComponent = () => {
     const classes = useStyles();
     const [modalStyle] = useState(getModalStyle);
     const [open, setOpen] = useState(false);
     const [file, setFile] = useState(null)
     const [uploading, setUploading] = useState(false)
     const handleOpen = () => { setOpen(true); };
     const handleClose = () => { setOpen(false); };
     return (
     <div className="file">
             <div className="file__container">
             <AddIcon fontSize='large' />
             <p>New</p>
             </div>
      </div>
     )
}

export default FileComponent

现在,在FileComponent.jsreturn块中,我们将显示ModalModal的内容将是一个输入类型文件和一个按钮。更新后的代码在这里用粗体标记:

...
...
     return (
         <div className="file">
                 <div className="file__container" onClick={handleOpen}>
                 <AddIcon fontSize='large' />
                 <p>New</p>
                 </div>
                 <Modal
                 open={open}
                 onClose={handleClose}
                 aria-labelledby="simple-modal-title"
                 aria-describedby="simple-modal-description"
                 >
                 <div style={modalStyle} className={classes.paper}>
                 <p>Select files you want to upload!</p>
                 {
                       uploading ? (
                       <p>Uploading...</p>
                       ) : (
                       <>
                       <input type="file" onChange={handleChange} />
                       <button onClick={handleUpload}>Upload</button>
                       </>

                       )
                 }
                 </div>
                 </Modal>
       </div>

       )
...
...

接下来,我们将在FileComponent.js文件中创建handleChangehandleUpload函数。在handleChange函数中,我们将文件设置为setFile(),在handleUpload函数中,我们获取上传的文件并将它的各种元素保存在fileUrl中,比如它的标题。这些将在我们的应用中稍后显示。更新后的代码在这里用粗体标记:

...
...
const FileComponent = () => {
  ...
  ...

     const handleChange = (e) => {
     if (e.target.files[0]) {
    setFile(e.target.files[0])
    }

    }

    const handleUpload = () => {
    setUploading(true)
    storage.ref(`files/${file.name}`).put(file).then(snapshot => {
    console.log(snapshot)
    storage.ref('files').child(file.name).getDownloadURL().then(url => {
                    db.collection('myFiles').add({
                    timestamp: firebase.firestore.FieldValue.serverTimestamp(),
                    caption: file.name,
                    fileUrl: url,
                    size: snapshot._delegate.bytesTransferred,
                    })
                    setUploading(false)
                    setOpen(false)
                    setFile(null)
    })

    storage.ref('files').child(file.name).getMetadata().then(meta => {
    console.log(meta.size)
    })

    })
    }

    return (
    ...
    ...
    )

}

export default FileComponent

为了让我们的代码工作,我们需要在 Firebase 中设置存储。因此,从 Firebase 控制台,点击存储,然后开始。现在会弹出如图 4-9 所示的窗口,需要点击下一步按钮。

img/512002_1_En_4_Fig9_HTML.jpg

图 4-9

入门指南

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

img/512002_1_En_4_Fig10_HTML.jpg

图 4-10

完成的

Firebase 需要验证才能上传文件。由于我们还没有设置认证,我们需要改变规则,如图 4-11 所示。

img/512002_1_En_4_Fig11_HTML.jpg

图 4-11

规则

现在,回到 localhost,单击 New 按钮,将显示一个弹出窗口。在弹出的界面中,您可以上传任何文件,如图 4-12 所示。

img/512002_1_En_4_Fig12_HTML.jpg

图 4-12

上传文件的弹出窗口

选择一个文件后点击上传按钮,将该文件上传到 Firebase,如图 4-13 所示。

img/512002_1_En_4_Fig13_HTML.jpg

图 4-13

Firebase 上传

使用 FileViewer 组件显示文件

我们有上传文件的逻辑,但是现在我们想在我们的项目中显示文件。

我们还需要首先启用 Firestore。为此,返回到 Firebase 控制台,点击云 Firestore ,然后点击创建数据库按钮,如图 4-14 所示。

img/512002_1_En_4_Fig14_HTML.jpg

图 4-14

创建数据库

在下一个画面中,在 测试模式下选择开始,然后点击下一个按钮,如图 4-15 所示。

img/512002_1_En_4_Fig15_HTML.jpg

图 4-15

测试模式

之后,在下一个屏幕上,点击使能按钮,如图 4-16 所示。

img/512002_1_En_4_Fig16_HTML.jpg

图 4-16

启用按钮

现在,创建一个名为FilesViewer.js的文件,并在其中添加以下内容。在这里,我们通过调用useEffect钩子内的FilesViewer.js从 Firebase 获取所有文件细节。获得数据后,我们对其进行映射,并将其传递给我们接下来将创建的FileItem组件。

import React, { useEffect, useState } from 'react'
import './FilesViewer.css'
import { db } from '../firebase'
import FileItem from './FileItem'

const FilesViewer = () => {
    const [files, setFiles] = useState()

useEffect(() => {
        db.collection('myFiles').onSnapshot(snapshot => {
            setFiles(snapshot.docs.map(doc => ({
                id: doc.id,
                item: doc.data()
            })))
        })
    }, [])

    return (
        <div className='fileViewer'>
            <div className="fileViewer__row">
            </div>
            <div className="fileViewer__titles">
                <div className="fileViewer__titles--left">
                    <p>Name</p>
                </div>
                <div className="fileViewer__titles--right">
                    <p>Last modified</p>
                    <p>File size</p>
                </div>
            </div>

            {
                files.map(({ id, item }) => (
                    <FileItem id={id} caption={item.caption} timestamp={item.timestamp} fileUrl={item.fileUrl} size={item.size} />
                ))
            }
        </div>
    )
}

export default FilesViewer

接下来,创建一个名为FileItem.js的文件,并在其中添加以下内容。在这里,我们只是显示数据。但是其中一个主要的是readableFileSizeStr函数。通过这个功能,我们可以显示正确的数字。

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

import InsertDriveFileIcon from '@material-ui/icons/InsertDriveFile';

const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

const FileItem = ({ id, caption, timestamp, fileUrl, size }) => {
    const fileDate = `${timestamp?.toDate().getDate()} ${monthNames[timestamp?.toDate().getMonth() + 1]} ${timestamp?.toDate().getFullYear()}`

const readableFileSizeStr = (fileSizeInBytes) => {
        let i = -1;
        const byteUnits = [' kB', ' MB', ' GB', ' TB', 'PB', 'EB', 'ZB', 'YB'];
        do {
            fileSizeInBytes = fileSizeInBytes / 1024;
            i++;
        } while (fileSizeInBytes > 1024);
        return Math.max(fileSizeInBytes, 0.1).toFixed(1) + byteUnits[i];
    };

return (
        <div className='fileItem'>
            <a href={fileUrl} target="_blank" rel="noreferrer" download>
                <div className="fileItem--left">
                    <InsertDriveFileIcon />
                    <p>{caption}</p>
                </div>
                <div className="fileItem--right">
                    <p>{fileDate}</p>
                    <p>{readableFileSizeStr(size)}</p>
                </div>
            </a>
        </div>
    )
}

export default FileItem

现在,回到App.js,我们已经包含了FilesViewer组件,还添加了一个叫做app__maindiv来包含它和Sidebar组件。更新后的代码以粗体显示:

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

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

export default App;

接下来,在App.css中,添加以下样式:

.app__main{
    display: flex;
}

现在,回到 localhost,我们必须上传另一个文件;然后我们会看到文件细节,如图 4-17 所示。

img/512002_1_En_4_Fig17_HTML.jpg

图 4-17

文件详细信息

现在,我们将在这些组件中添加样式。首先在FileItem.css文件中添加样式。

.fileItem{
    height: 55px;
    border-bottom: 1px solid rgb(219, 219, 219);
    border-top: 1px solid rgb(219, 219, 219);
    width: 100%;
}

.fileItem>a{
    height: 100%;
    display: flex;
    text-decoration: none;
    color: rgb(85, 78, 78);
}

.fileItem>a>div{
    display: flex;
align-items: center;
}

.fileItem>a>div>*{
    margin: 10px;
}

.fileItem--left{
    flex: 1;
}

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

.fileViewer{
width: 100%;
}

.fileViewer__row{
    height: 250px;
    display: flex;
    align-items: center;
}

.fileViewer__titles{
    display: flex;
    margin-bottom: 5px;
    color: rgb(85, 78, 78);
}

.fileViewer__titles>div>*{
    margin: 5px;
}

    .fileViewer__titles--left{
        flex: 1;
}

.fileViewer__titles--right{
    display: flex;
}

现在,我们的文件在 localhost 上看起来很棒,如图 4-18 所示。

img/512002_1_En_4_Fig18_HTML.jpg

图 4-18

布局

创建文件卡组件

我们现在创建一个FileCard组件来显示项目中文件的漂亮图标。

创建一个名为FileCard.js的文件,并将以下内容放入其中。在这里,我们只是显示一个大图标和文件名,从父组件传递过来。

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

import InsertDriveFileIcon from '@material-ui/icons/InsertDriveFile';

const FileCard = ({ name }) => {
    return (
        <div className='fileCard'>
            <div className="fileCard--top">
                <InsertDriveFileIcon style={{ fontSize: 130 }} />
            </div>
            <div className="fileCard--bottom">
                <p>{name}</p>
            </div>
        </div>
    )
}

export default FileCard

接下来,在FileCard.css中,放入以下样式:

.fileCard {
    height: 190px;
    width: 240px;
    border-radius: 10px;
    border: 1px solid rgb(219, 219, 219);
    margin: 5px;
}

.fileCard--top {
    height: 70%;
    border-bottom: 1px solid rgb(219, 219, 219);
    display: flex;
    justify-content: center;
    align-items: center;
}

.fileCard--bottom {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 30%;
}

.fileCard--bottom>p {
    width: 90%;
    overflow: hidden;
    white-space: nowrap;
}

现在,我们需要在FilesViewer.js文件中导入FileCard组件。这里,我们映射了所有的文件,但只选择了五个,并将name值发送给FileCard组件。更新后的代码在这里被标记为粗体:

...
import FileCard from './FileCard'

const FilesViewer = () => {
    ...
    ...

      return (
      <div className='fileViewer'>
         <div className="fileViewer__row">
         {
         files.slice(0, 5).map(({ id, item }) => (
         <FileCard key={id} name={item.caption} />
         ))

         }
         </div>
         ...
         ...
     </div>

     )
}

export default FilesViewer

现在,我们可以在 localhost 上看到一个大图标,如图 4-19 所示。

img/512002_1_En_4_Fig19_HTML.jpg

图 4-19

大图标

创建侧图标组件

我们现在创建一个SideIcons组件,在侧面显示一些漂亮的图标。创建一个名为SideIcons.js的文件,并将以下内容放入其中:

import React from 'react'
import './SideIcons.css'
import AddIcon from '@material-ui/icons/Add'

const SideIcons = () => {
    return (
        <div className='sideIcons'>
            <div className="sideIcons__top">
                <img src="https://cdn4.iconfinder.com/data/icons/logos-brands-in-colors/48/google-calendar-512.png" alt="Calendar" />
                <img src="https://assets.materialup.com/uploads/64f5506e-2577-4d19-9425-11a1e1fa31a8/0x0ss-85.jpg" alt="Keep" />
                <img src="https://www.androidpolice.com/wp-content/uploads/2018/03/nexus2cee_new-tasks-icon.png" alt="Tasks" />
            </div>
            <hr />
            <div className="sideIcons__plusIcon">
                <AddIcon />
            </div>
        </div>
    )
}

export default SideIcons

接下来,在SideIcons.css中,放入以下样式:

.sideIcons{
    width: 50px;
    display: flex;
    flex-direction: column;
    align-items: center;
    border-left: 1px solid rgb(219, 219, 219);
}

.sideIcons__top{
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
}

.sideIcons__top>img{
    object-fit: contain;
    width: 30px;
    margin: 10px 0;
}

.sideIcons>hr{
    margin: 12px 0;
    width: 90%;
}

.sideIcons__plusIcon{
    display: flex;
    align-items: center;
}

现在,导入App.js文件中的代码。此处的更新代码以粗体标记:

...
import SideIcons from './components/SideIcons';

function App() {
  return (
     <div className="app">
     <Header />
     <div className="app__main">
     <Sidebar />
     <FilesViewer />
     <SideIcons />
     </div>
     </div>
  );
}
export default App;

现在,我们可以在右边栏看到这些漂亮的图标,如图 4-20 所示。

img/512002_1_En_4_Fig20_HTML.jpg

图 4-20

右侧的图标

添加 Google 身份验证

现在,我们的应用几乎完成了,但我们仍然需要添加谷歌认证。因此,进入 Firebase 控制台,点击认证选项卡,然后点击开始按钮,如图 4-21 所示。

img/512002_1_En_4_Fig21_HTML.jpg

图 4-21

身份验证入门

在下一个界面中,点击 Google 旁边的编辑配置图标,如图 4-22 所示。

img/512002_1_En_4_Fig22_HTML.jpg

图 4-22

点击谷歌

在弹出的消息中,点击启用按钮,之后输入您的 Gmail ID,点击保存按钮,如图 4-23 所示。

img/512002_1_En_4_Fig23_HTML.jpg

图 4-23

正在登录

接下来在App.js文件中,我们只需要从本地 Firebase 文件中导入authprovider。之后,我们使用名为signInWithPopup()的方法来启用认证。

之后,在return块中,我们使用一个三元运算符来显示所有组件,如果我们有一个用户的话。如果没有找到用户,我们显示一个登录名div。此处的更新代码以粗体标记:

import { auth, provider } from "./firebase";
import { useState } from 'react';

function App() {
  const [user, setUser] = useState(null)
  const handleLogin = () => {
     if (!user) {
     auth.signInWithPopup(provider).then(result => setUser(result.user))
      .catch(error => alert(error.message));
    }
  }

  return (
       <div className="app">
       {user ? (
       <>

       <Header userPhoto={user?.photoURL}/>
       <div className="app__main">
       <Sidebar />
       <FilesViewer />
       <SideIcons />
       </div>
       </>

       ) : (
       <div className='app__login'>
       <img src="logo512.png" alt="Storage" />
       <button onClick={handleLogin}>Log in to Storage</button>
       </div>
       )}
       </div>
  );
}
export default App;

现在,在App.css文件中,添加这些额外的样式:

.app__login {
    width: 100vw;
    height: 100vh;
    display: grid;
    place-items: center;
}

.app__login>button{
    border: none;
    font-size: 24px;
    background-color: rgb(67, 130, 244);
    color: white;
    padding: 10px 20px;
    border-radius: 6px;
    transition: all 0.2s;
}

.app__login>button:hover{
    cursor: pointer;
    background-color: rgb(49, 94, 179);
    transform: scale(1.1);
}

因为我们将把userPhoto属性传递给Header组件,所以我们将在Header.js文件中使用它。此处的更新代码以粗体标记:

...

const Header = ({ userPhoto }) => {
return (
         <div className="header">
              ...
              ...
             <div className="header__icons">
             <span>
             <HelpOutlineIcon />
             <SettingsIcon />
             </span>
             <AppsIcon />
             <Avatar className="header__iconsAvatar"src={userPhoto} />
             </div>
     </div>
     )
}

export default Header

现在,当我们转到 localhost 并点击登录存储时,我们会看到谷歌认证弹出窗口,如图 4-24 所示。

img/512002_1_En_4_Fig24_HTML.jpg

图 4-24

谷歌认证弹出窗口

当我们点击 Gmail ID 时,我们将进入我们的应用。在这里,我们可以在右上角看到已登录的用户图像,如图 4-25 所示。

img/512002_1_En_4_Fig25_HTML.jpg

图 4-25

登录用户

通过 Firebase 部署和托管

现在,我们可以按照前面描述的相同步骤在 Firebase 中部署我们的应用。部署成功且工作正常,如图 4-26 所示。

img/512002_1_En_4_Fig26_HTML.jpg

图 4-26

存储应用

摘要

在本章中,您学习了如何制作一个存储应用,您可以通过 Google 认证登录并上传文件。我们用 React 创建了 web 应用,并将数据存储在 Firebase 存储中。您还学习了如何在 Firebase 中进行托管。

五、使用 React 和 Firebase 构建一个与职业相关的社交媒体应用

欢迎来到一个新的 ReactJS 项目,这将是一个内置在 ReactJS 中的与职业相关的社交媒体应用。此外,我们将使用 Redux 和许多其他精彩的技术来创建这个应用。

主机和数据库将在 Firebase。我们也将在项目中使用图标的材料用户界面。

图 5-1 显示了完整的应用。

img/512002_1_En_5_Fig1_HTML.jpg

图 5-1

完成的应用

入门指南

使用create-react-app命令创建一个名为career-firebase-app的新应用。具体来说,打开任何终端并输入以下命令。注意,我们使用template redux将 Redux 包含在我们的项目中。

npx create-react-app career-firebase-app --template redux

初始 Firebase 设置

由于我们的前端站点也将通过 Firebase 托管,我们将创建基本设置,而这个create-react-app命令创建我们的 React 应用。我们将遵循与第一章相同的步骤。我已经创建了一个名为career-firebase-app的应用(图 5-2 )。

img/512002_1_En_5_Fig2_HTML.jpg

图 5-2

职业-firebase-应用

我们还将像上一章那样启用云 Firestore。最后,复制firebaseConfig的所有代码,如图 5-3 所示。(你可以在前一章找到这样做的步骤。)

img/512002_1_En_5_Fig3_HTML.jpg

图 5-3

配置

基本 React 设置

我们的 React 设置将在此时完成。所以,回到终端,将cd放入新创建的career-firebase-app目录。

之后,在 VS 代码中打开目录,在src文件夹中创建一个名为firebase.js的文件,并将之前 Firebase 屏幕中的内容粘贴到其中。

const  xxxxxxConfig = {
     apiKey: "AXXXXXXXXXXXXXXXXXXXXXXXX",
     authDomain: "career- xxxxxx-app. xxxxxxapp.com",
     projectId: "career- xxxxxx-app",
     storageBucket: "career- xxxxxx-app.appspot.com",
     messagingSenderId: "106xxxxxxxxxxxxxx",
     appId: "1:10xxxxxxxxxxx:web:1xxxxxxxxxxxxxx"
};

接下来,我们将进行清理过程,这与我们在上一章中所做的类似。首先我们将删除不必要的文件并更改index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import store from './app/store';
import { Provider } from 'react-redux';

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

然后我们再改App.js,如下图:

import React from 'react';
import './App.css';

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

export default App;

现在两个文件都包含了最少的内容。此外,删除App.css中的所有内容,并使index.css中的页边距为零。在这个过程之后,我们的本地主机将如图 5-4 所示。

img/512002_1_En_5_Fig4_HTML.jpg

图 5-4

初始应用

创建标题

我们的 React 设置已经完成,我们将首先处理Header组件。因此,在src文件夹中创建一个名为components的文件夹。在components文件夹中创建Header.jsHeader.css文件。但是我们将首先在App.js文件中导入Header组件。

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

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

export default App;

我们将为图标使用材质用户界面。因此,我们需要根据文档做两个npm install。我们将通过集成终端安装coreicons

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

现在,我们的Header.js文件大部分是静态的。文件将主要包含图标和标志。

这里我们有一个div叫做header,包含两个div,第一个是header__left,包含一个图像,另一个div包含Search作为输入。下一个divheader__right,它包含对另一个组件HeaderOption的调用。

import { Search, Home, SupervisorAccount, BusinessCenter, Chat, Notifications } from '@material-ui/icons'
import React from 'react'
import './Header.css'
import HeaderOption from './HeaderOption'

const Header = () => {
    return (
        <div className="header">
            <div className="header__left">
                <img src="logo192.png" alt="logo"/>
                <div className="header__search">
                    <Search />
                    <input type="text"/>
                </div>
            </div>
            <div className="header__right">
                <HeaderOption Icon={Home} title="Home" />
                <HeaderOption Icon={SupervisorAccount} title="My Network" />
                <HeaderOption Icon={BusinessCenter} title="Jobs" />
                <HeaderOption Icon={Chat} title="Messaging" />
                <HeaderOption Icon={Notifications} title="Notifications" />
                <HeaderOption avatar="https://pbs.twimg.com/profile_images/1020939891457241088/fcbu814K_400x400.jpg" title="me" />
            </div>
        </div>
    )
}

export default Header

现在,在同一个文件夹中创建一个名为Header.css的文件,并在其中添加以下内容。这里,我们使用了很多 flexboxes 来设计我们的页眉。

.header{
    position: sticky;
    top: 0;
    display: flex;
    background-color: white;
    justify-content: space-evenly;
    border-bottom: 0.1px solid lightgray;
    padding: 10px 0;
    width: 100%;
    z-index: 999;
}

.header__left{
    display: flex;
}

.header__left > img{
    object-fit: contain;
    height: 40px;
    margin-right: 10px;
}

.header__search{
    padding: 10px;
    display: flex;
    align-items: center;
    border-radius: 5px;
    height: 22px;
    color:gray;
    background-color: #eef3f8;
}

.header__search > input{
    outline: none;
    border: none;
    background: none;
}

.header__right{
    display: flex;
}

接下来,创建HeaderOption.js文件,该文件将包含头像、图标、标题和属性,如下所示:

import { Avatar } from '@material-ui/core'
import React from 'react'
import './HeaderOption.css'

const HeaderOption = ({ avatar, Icon, title }) => {
    return (
        <div className="headerOption">
            {Icon && <Icon className="headerOption__icon" />}
            {avatar && <Avatar className="headerOption__icon" src={avatar} />}
            <h3 className="headerOption__title">{title}</h3>
        </div>
    )
}

export default HeaderOption

现在,在HeaderOption.css文件中为此创建样式。

.headerOption{
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-right: 20px;
    color:gray;
    cursor: pointer;
}

.headerOption:hover{
    color: black;
}

.headerOption__title{
    font-size: 12px;
    font-weight: 400;
}

.headerOption__icon{
    object-fit: contain;
    height: 25px !important;
    width: 25px !important;
}

现在,在 localhost 上,我们可以看到如图 5-5 所示的漂亮标题。

img/512002_1_En_5_Fig5_HTML.jpg

图 5-5

我们的标题

创建侧栏

我们现在将致力于Sidebar组件。因此,在文件夹components中创建Sidebar.jsSidebar.css文件。但是我们将首先在App.js文件中导入侧栏。更新后的代码以粗体显示,如下所示:

 import Sidebar from './components/Sidebar';

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

export default App;

接下来,我们还将为App.css文件中的appapp__body添加样式。

.app{
    background-color: #f3f2ef;
    display: flex;
    flex-direction: column;
    align-items: center;
}

.app__body{
    display: flex;
}

接下来将以下内容放入文件Sidebar.js。这里,侧边栏的主div包含三个div:sidebar__topsidebar__statssidebar__bottom

  • sidebar__top包含图像、头像、姓名和电子邮件。对于图像,我已经将一个图像放在了public文件夹中,这样我们就可以直接使用它了。

  • sidebar__stats包含两个被称为sidebar__statdiv,每个包含一个文本和一个数字段。

  • 到目前为止,sidebar__bottom只包含一个带有“最近”一词的p标签。

import { Avatar } from '@material-ui/core'
import React from 'react'
import './Sidebar.css'

const Sidebar = () => {
    return (
        <div className="sidebar">
            <div className="sidebar__top">
                <img src="background.jpg" alt="Background" />
                <Avatar className="sidebar__avatar" />
                <h2>Nabendu Biswas</h2>
                <h4>nabendu.biswas@gmail.com</h4>
            </div>
            <div className="sidebar__stats">
                <div className="sidebar__stat">
                    <p>Who viewed you</p>
                    <p className="sidebar__statNumber">2,544</p>
                </div>
                <div className="sidebar__stat">
                    <p>Views on post</p>
                    <p className="sidebar__statNumber">2,300</p>
                </div>
            </div>
            <div className="sidebar__bottom">
                <p>Recent</p>
            </div>
        </div>
    )
}

export default Sidebar

现在,将以下样式放入Sidebar.css文件:

.sidebar{
    position: sticky;
    top: 80px;
    flex: 0.2;
    border-radius: 10px;
    text-align: center;
    height: fit-content;
}

.sidebar__avatar{
    margin-bottom: 10px;
}

.sidebar__top{
    display: flex;
    flex-direction: column;
    align-items: center;
    border: 1px solid lightgray;
    border-bottom: 0;
    border-top-left-radius: 10px;
    border-top-right-radius: 10px;
    background-color: white;
    padding-bottom: 10px;
}

.sidebar__top > img{
    margin-bottom: -20px;
    width: 100%;
    height: 60px;
    border-top-left-radius: 10px;
    border-top-right-radius: 10px;
    object-fit: cover;
}

.sidebar__top > h4{
    color: gray;
    font-size: 12px;
}

.sidebar__top > h2{
    font-size: 18px;
}

.sidebar__stats{
    padding: 10px;
    margin-bottom: 10px;
    border: 1px solid lightgray;
    background-color: white;
    border-bottom-left-radius: 10px;
    border-bottom-right-radius: 10px;
}

.sidebar__stat{
    margin-top: 10px;
    display: flex;
    justify-content: space-between;
}

.sidebar__stat > p{
    color: gray;
    font-size: 13px;
    font-weight: 600;
}

.sidebar__statNumber{
    font-weight: bold;
    color: #0a66c2 !important;
}

.sidebar__bottom{
    text-align: left;
    padding: 10px;
    border: 1px solid lightgray;
    background-color: white;
    border-radius: 10px;
    margin-top: 10px;
}

现在,我们的侧边栏看起来像 localhost 上的图 5-6 。

img/512002_1_En_5_Fig6_HTML.jpg

图 5-6

我们的侧栏

现在,我们将把sidebar__bottom中的所有项目放到Sidebar.js文件中。在这里,我们创建了函数recentItem,并向它传递不同的属性。更新后的代码在这里以粗体显示:

const Sidebar = () => {
    const recentItem = (topic) => (
    <div className="sidebar__recentItem">
         <span className="sidebar__hash">#</span>
         <p>{topic}</p>
     </div>
     )

     return (
     <div className="sidebar">
              ...
              ...
             <div className="sidebar__bottom">
             <p>Recent</p>
             {recentItem("reactjs")}
             {recentItem("programming")}
             {recentItem("developer")}
             {recentItem("javascript")}
             {recentItem("design")}
             </div>
      </div>
     )
}

export default Sidebar

接下来,我们将在Sidebar.css文件中放入额外的样式,如下所示:

.sidebar__bottom > p{
    font-size: 13px;
    padding-bottom: 10px;
}

    .sidebar__recentItem{
        display: flex;
        font-size: 13px;
        color: gray;
        font-weight: bolder;
        cursor: pointer;
        margin-bottom: 5px;
        padding: 5px;
    }

    .sidebar__recentItem:hover{
        background-color: whitesmoke;
        border-radius: 10px;
        cursor: pointer;
        color: black;
    }

    .sidebar__hash{
        margin-right: 10px;
        margin-left: 5px;
    }

现在,我们的本地主机将看起来如图 5-7 所示,带有最近框。

img/512002_1_En_5_Fig7_HTML.jpg

图 5-7

最近框

创建提要组件

我们现在将致力于Feed组件。因此,在文件夹components中,在文件夹components中创建名为Feed.jsFeed.css的文件。但是我们将首先在App.js文件中导入Feed组件。更新后的代码在这里以粗体显示:

import Feed from './components/Feed';

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

export default App;

接下来,将以下内容放入文件Feed.js。这里,feed的主div包含一个叫做feed__inputContainerdiv,它包含两个div:?? 和 ??。

  • feed__input包含一个创建图标和一个表单。该表单包含一个输入和一个按钮。

  • feed__inputOptions正在调用带有Icontitlecolor属性的组件InputOptionIcon属性其实是一个材质 UI 图标。

import { CalendarViewDay, Create, EventNote, Image, Subscriptions } from '@material-ui/icons'
import React from 'react'
import './Feed.css'
import InputOption from './InputOption'

const Feed = () => {
    return (
        <div className="feed">
            <div className="feed__inputContainer">
                <div className="feed__input">
                    <Create />
                    <form>
                        <input type="text"/>
                        <button type="submit">Send</button>
                    </form>
                </div>
                <div className="feed__inputOptions">
                    <InputOption Icon={Image} title="Photo" color="#70B5F9" />
                    <InputOption Icon={Subscriptions} title="Video" color="#E7A33E" />
                    <InputOption Icon={EventNote} title="Event" color="#C0CBCD" />
                    <InputOption Icon={CalendarViewDay} title="Write Article" color="#7FC15E" />
                </div>
            </div>
        </div>
    )
}

export default Feed

现在,将以下样式放入Feed.css文件:

.feed{
    flex: 0.6;
    margin: 0 20px;
}

.feed__inputContainer{
    background-color: white;
    padding: 10px;
    padding-bottom: 20px;
    border-radius: 10px;
    margin-bottom: 20px;
}

.feed__input{
    border: 1px solid lightgray;
    border-radius: 30px;
    display: flex;
    padding: 10px;
    color: gray;
    padding-left: 15px;
}

.feed__input > form{
    display: flex;
    width: 100%;
}

.feed__input > form > input{
    border: none;
    flex: 1;
    margin-left: 10px;
    outline-width: 0;
    font-weight: 600;
}

.feed__input > form > button{
    display: none;
}

.feed__inputOptions{
    display: flex;
    justify-content: space-evenly;
}

现在,创建一个名为InputOption.js的文件,并将以下内容放入其中。该组件主要用于显示不同的图标和传递给它的属性。

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

const InputOption = ({ Icon, title, color }) => {
    return (
        <div className="inputOption">
            <Icon style={{ color }} />
            <h4>{title}</h4>
        </div>
    )
}

export default InputOption

现在,我们将在InputOption.css文件中为此创建样式。

.inputOption{
    display: flex;
    align-items: center;
    margin-top: 15px;
    color: gray;
    padding: 10px;
    cursor: pointer;
}

.inputOption:hover{
    background-color: whitesmoke;
    border-radius: 10px;
}

.inputOption > h4{
    margin-left: 5px;
}

我们的Feed组件已经完成,看起来像 localhost 上的图 5-8 。

img/512002_1_En_5_Fig8_HTML.jpg

图 5-8

添加文章部分

构建 Post 部分

我们现在将致力于Post部分。因此,在文件夹components中,创建名为Post.jsPost.css的文件。但是我们将首先把Post组件导入到Feed.js文件中。另外,请注意,我们向它传递了三个属性:namedescriptionmessage。更新后的代码在这里以粗体显示:

import Post from './Post'

const Feed = () => {
        return (
        <div className="feed">
        <div className="feed__inputContainer">
               ...
               ...
        </div>
        <Post name="Nabendu Biswas" description="This is a test" message="This is awesome thing to do" />
        </div>
        )
}

export default Feed

接下来将以下内容放入文件Post.js。这里,post的主div包含三个div:post__headerpost__bodypost__buttons

  • post__header包含一个头像图标和另一个名为post__infodiv,后者包含一个h2p。我们在这里展示了namedescription属性。

  • post__body显示消息属性。

  • post__buttons正在调用带有Icontitlecolor属性的组件InputOptionIcon属性其实是一个材质 UI 图标。

import { Avatar } from '@material-ui/core'
import { ChatOutlined, SendOutlined, ShareOutlined, ThumbUpAltOutlined } from '@material-ui/icons'
import React from 'react'
import InputOption from './InputOption'
import './Post.css'

const Post = ({ name, description, message, photoUrl }) => {
    return (
        <div className="post">
            <div className="post__header">
                <Avatar />
                <div className="post__info">
                    <h2>{name}</h2>
                    <p>{description}</p>
                </div>
            </div>
            <div className="post__body">
                <p>{message}</p>
            </div>
            <div className="post__buttons">
                <InputOption Icon={ThumbUpAltOutlined} title="Like" color="gray" />
                <InputOption Icon={ChatOutlined} title="Comment" color="gray" />
                <InputOption Icon={ShareOutlined} title="Share" color="gray" />
                <InputOption Icon={SendOutlined} title="Send" color="gray" />
            </div>
        </div>
    )
}

export default Post

现在,我们将在一个Post.css文件中设计这个组件。

.post{
    background-color: white;
    padding: 15px;
    margin-bottom: 10px;
    border-radius: 10px;
}

.post__header{
    display:flex;
    margin-bottom: 10px;
}

.post__info{
    margin-left: 10px;
}

.post__info > p{
    font-size: 12px;
    color: gray;
}

.post__info > h2{
    font-size: 15px;
}

.post__body{
    overflow-wrap: anywhere;
}

.post__buttons{
    display: flex;
    justify-content: space-evenly;
}

现在,在 localhost 上,我们会看到一个漂亮的发布部分,如图 5-9 所示。

img/512002_1_En_5_Fig9_HTML.jpg

图 5-9

张贴示例

将 Firebase 与 React 集成

我们现在将把 Firebase 集成到我们的项目中。首先要做的是在我们的项目中安装 Firebase,方法是从终端运行以下命令。

npm install firebase

接下来,您将更新我们的firebase.js文件,以使用配置来初始化应用。之后,使用 Firestore 作为数据库。我们也在项目中使用认证。

import firebase from 'firebase'

const firebaseConfig = {
    ...
    ...
};

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

export { auth, db }

现在,回到Feed.js,我们首先进口所需的东西。之后,我们将创建两个状态变量:postsinput

现在,在useEffect内部,我们将调用 Firebase 来获取posts集合,然后拍摄快照。用 Firebase 的术语来说,它是实时数据,我们可以立即获得。然后,我们将通过setPosts()posts数组中设置这个数据。

我们还有一个sendPost(),马上就要和onClick挂钩了。这里,我们向 Firebase 添加一个帖子。消息将从输入字段中获取,而timestamp是服务器时间戳。我们正在对其余的字段进行硬编码。

现在,在Feed.js文件的return语句中,我们将valueonChange添加到输入字段,将onClick添加到按钮。

之后,我们通过posts数组进行映射,并将不同的属性从 Firebase 传递给Post组件。更新后的代码在这里以粗体显示:

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

const Feed = () => {
      const [posts, setPosts] = useState([])
      const [input, setInput] = useState('')

      useEffect(() => {
      db.collection('posts').orderBy('timestamp', 'desc').onSnapshot(snapshot => {
      setPosts(snapshot.docs.map(doc => ({
      id: doc.id,
      data: doc.data()
      })))
      })
      }, [])

      const sendPost = e => {
      e.preventDefault()
      db.collection('posts').add({
      name: 'Nabendu Biswas',
      description: 'This is a test',
      message: input,
      photoUrl: '',
      timestamp: firebase.firestore.FieldValue.serverTimestamp()
      })
      setInput('')
      }

      return (
      <div className="feed">
      <div className="feed__inputContainer">
      <div className="feed__input">
          <Create />
          <form>
          <input value={input} onChange={e => setInput(e.target.value)} type="text" />
          <button onClick={sendPost} type="submit">Send</button>
          </form>
      </div>
      <div className="feed__inputOptions">
                  ...
      </div>
      </div>
      {posts.map(({ id, data }) => (
      <Post
          key={id}
          name={data.name}
          description={data.description}
          message={data.message}
          photoUrl={data.photoUrl}
      />
      ))}
      </div>
      )
}

export default Feed

现在,每当我们在输入框中键入一些内容并按 Enter 键时,这些内容就会实时显示在我们的应用中,如图 5-10 所示。

img/512002_1_En_5_Fig10_HTML.jpg

图 5-10

实时

集成冗余

我们现在将把 Redux 集成到我们的项目中。Redux 将用于获取用户详细信息并将它们存储在全局状态中,以便它们在所有组件中都可用。

因为我们已经在创建项目时添加了 Redux,所以我们需要删除一些样板代码。在features\counter文件夹中,删除Counter.jsCounter.module.css文件。

接下来,将counterSlice.js文件移动到features文件夹,并删除空的counter文件夹。

现在,在store.js文件中,更改名称,因为我们需要用户而不是计数器。此外,将counterSlice.js文件名改为userSlice.js。更新后的代码在这里以粗体显示:

import { configureStore } from '@reduxjs/toolkit';
import userReducer from '../features/userSlice';

export const store = configureStore({
  reducer: {
     user: userReducer,
  },
});

现在,用以下内容更新userSlice.js。这里,我们有用户的初始状态。在那之后,我们有loginlogout在减速器里面。两者都改变了用户状态。

我们正在导出loginlogout,我们将很快使用它们来改变状态。我们也在导出selectUser,通过它我们可以得到任意时间点的用户状态。这方面的代码如下所示:

import { createSlice } from '@reduxjs/toolkit';

export const userSlice = createSlice({
  name: 'user',
  initialState: {
    user: null,
  },
  reducers: {
    login: (state, action) => {
      state.user =  action.payload;
    },
    logout: state => {
      state.user = null;
    },
  },
});

export const { login, logout } = userSlice.actions;

export const selectUser = state => state.user.user;

export default userSlice.reducer;

现在,当我们转到 localhost 并打开 Redux devtool 时,我们可以看到用户的全局 Redux 状态,如图 5-11 所示。

img/512002_1_En_5_Fig11_HTML.jpg

图 5-11

全球状态

构建登录页面

在本节中,我们将构建我们的登录页面并使用 Redux。因此,在components文件夹中创建两个名为Login.jsLogin.css的文件。

App.js文件中进行以下更改。这里,我们首先导入useSelectorselectUser,然后导入Login组件。

此外,我们正在使用来自react-reduxuseSelector钩子。在return元素中,如果用户不可用,我们将显示Login组件或其他组件。更新后的代码在这里以粗体显示:

import { useSelector } from 'react-redux';
import { selectUser } from './features/userSlice';
import Login from './components/Login';

function App() {
  const user = useSelector(selectUser)

  return (
     <div className="app">
     <Header />
     {!user ? (<Login />) : (
     <div className="app__body">
     <Sidebar />
     <Feed />
     </div>
     )}
     </div>
  );
}

export default App;

现在,在Login.js文件中,放入以下内容。这里,我们展示了一个图像和一个包含四个输入字段和一个按钮的表单。

我们在表单外还有一个段落,它包含一个要注册的跨度。

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

const Login = () => {
    const register = () => {}
    const loginToApp = () => {}

return (
        <div className="login">
            <img src="logo512.png" alt="logo"/>
            <form>
                <input type="text" placeholder="Full name (required if registering)" />
                <input type="text" placeholder="Profile pic URL (optional)" />
                <input type="email" placeholder="Email" />
                <input type="password" placeholder="Password" />
                <button type="submit" onClick={loginToApp}>Sign In</button>
            </form>
            <p>Not a member?{' '}
                <span onClick={register} className="login__register">Register Now</span>
            </p>
        </div>
    )
}

export default Login

另外,在Login.css文件中添加以下样式:

.login{
    display: grid;
    place-items: center;
    margin-left: auto;
    margin-right: auto;
    padding-top: 100px;
    padding-bottom: 100px;
}

.login > img{
    object-fit: contain;
    height: 70px;
    margin-top: 20px;
    margin-bottom: 20px;
}

.login > form{
    display: flex;
    flex-direction: column;
}

.login > form > input{
    width: 350px;
    height: 50px;
    font-size: 20px;
    padding-left: 10px;
    margin-bottom: 10px;
    border-radius: 5px;
}

.login > form > button{
    width: 365px;
    height: 50px;
    font-size: large;
    color: #fff;
    background-color: #0074b1;
    border-radius: 5px;
}

.login__register{
    color: #0177b7;
    cursor: pointer;
}

.login > p{
    margin-top: 20px;
}

现在,我们的登录屏幕在 localhost 上将如图 5-12 所示。

img/512002_1_En_5_Fig12_HTML.jpg

图 5-12

登录屏幕

添加电子邮件验证

现在,我们将向我们的应用添加电子邮件身份验证,因此我们必须首先从 Firebase 控制台启用它。

因此,点击认证选项卡,然后点击开始按钮,如图 5-13 所示。

img/512002_1_En_5_Fig13_HTML.jpg

图 5-13

证明

之后,将鼠标悬停在 Email/Password 上,点击编辑图标,如图 5-14 所示。

img/512002_1_En_5_Fig14_HTML.jpg

图 5-14

电子邮件配置

在弹出的窗口中,点击使能按钮,然后点击保存按钮,如图 5-15 所示。

img/512002_1_En_5_Fig15_HTML.jpg

图 5-15

启用按钮

现在,在Login.js文件中,我们将为emailpasswordnameprofilePic创建四个不同的状态变量。

我们还在这里完成了我们的register函数。在函数内部,如果用户没有输入名字,我们将返回。之后,我们使用 Firebase 的createUserWithEmailAndPassword来注册用户。

注册完成后,我们使用 Redux 的dispatch函数发送登录信息来设置全局状态。更新后的代码在这里以粗体显示:

import React, { useState }  from 'react'
import { useDispatch } from 'react-redux'
import { auth } from '../firebase'
import { login } from '../features/userSlice'

const Login = () => {
     const [email, setEmail] = useState('')
     const [password, setPassword] = useState('')
     const [name, setName] = useState('')
     const [profilePic, setProfilePic] = useState('')
     const dispatch = useDispatch()
     const register = () => {
     if(!name) return alert('Please enter a Full Name')
     auth.createUserWithEmailAndPassword(email,password)
     .then(userAuth => userAuth.user.updateProfile({ displayName: name, photoURL: profilePic })
     .then(() => {
     dispatch(login({ email: userAuth.user.email, uid: userAuth.user.uid, displayName: name, photoUrl: profilePic }))
     }))
     }

     const loginToApp = (e) => {}

return (
     <div className="login">
     <img src="logo512.png" alt="logo"/>
     <form>
     <input value={name} onChange={e => setName(e.target.value)} type="text" placeholder="Full name (required if registering)" />
     <input value={profilePic} onChange={e => setProfilePic(e.target.value)} type="text" placeholder="Profile pic URL (optional)" />
     <input value={email} onChange={e => setEmail(e.target.value)} type="email" placeholder="Email" />
     <input value={password} onChange={e => setPassword(e.target.value)} type="password" placeholder="Password" />
     <button type="submit" onClick={loginToApp}>Sign In</button>
     </form>
     ...
     </div>
     )
}

export default Login

现在,在 localhost 上,当我们给出全名、个人资料图片、电子邮件和密码,并点击“立即注册”时,我们将被直接带到所有组件,因为在App.js中,用户将不会是空白的,如图 5-16 所示。

img/512002_1_En_5_Fig16_HTML.jpg

图 5-16

用户登录

现在,我们希望保持登录,因为如果我们刷新,我们将返回到注册页面。

所以,在App.js文件中,我们将再次使用dispatchlogin方法。但是我们将从useEffect内部检查login,我们从onAuthStateChanged开始检查。更新后的代码在这里以粗体显示:

import React, { useEffect }  from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { login, selectUser } from './features/userSlice';
import { auth } from './firebase';

function App() {
  const user = useSelector(selectUser)
  const dispatch = useDispatch()

  useEffect(() => {
     auth.onAuthStateChanged(userAuth => {
     if(userAuth){
     dispatch(login({ email: userAuth.email, uid: userAuth.uid, displayName: userAuth.displayName, photoUrl: userAuth.photoUrl }))
     }
     })
  },[])

  return (
     ...
  );
}

export default App;

现在,我们将添加当我们单击标题中的图片时注销的功能。因此,在Header.js文件中,将代码更新如下。在这里,我们首先导入所需的东西,然后在 props 中发送onClick,它运行logoutApp()

logoutApp函数中,我们只是为 Redux 的注销和 Firebase 的auth.signout()分派。更新后的代码在这里以粗体显示:

import { useDispatch } from 'react-redux'
import { logout } from '../features/userSlice'
import { auth } from '../firebase'

const Header = () => {
     const dispatch = useDispatch()
     const logoutApp = () => {
     dispatch(logout())
     auth.signOut()
     }

     return (
     <div className="header">
             <div className="header__left">
                   ...
             </div>
             <div className="header__right">
                 ...
             <HeaderOption avatar="https://pbs.twimg.com/profile_images/1020939891457241088/fcbu814K_400x400.jpg" title="me" onClick={logoutApp} />
             </div>
         </div>
     )
}

export default Header

现在,我们必须更新HeaderOption.js文件,从这里我们将把onClick作为callback函数传递。更新后的代码在这里以粗体显示:

const HeaderOption = ({ avatar, Icon, title, onClick }) => {
     return (
     <div onClick={onClick} className="headerOption">
          ...
     </div>
     )
}

export default HeaderOption

现在,剩下的一件事是用户注册后登录。为此,在Login.js文件中,更新loginToApp()。在这个函数中,我们使用 Firebase 的signInWithEmailAndPassword来发送邮件和密码,然后在 Redux 中发送。

现在,我们可以转到 localhost,提供电子邮件和密码,然后单击登录按钮。更新后的代码在这里以粗体显示:

const Login = () => {
    ...

      const loginToApp = (e) => {
      e.preventDefault()
      auth.signInWithEmailAndPassword(email,password)
      .then((userAuth) => {
          dispatch(login({
          email: userAuth.user.email,
          uid: userAuth.user.uid,
          displayName: userAuth.user.displayName,
          photoUrl: userAuth.user.photoUrl
          }))
      })
      }

      return (
      ...
      )
}

export default Login

使用用户信息

现在我们已经获得了用户信息,我们将在应用的不同部分使用它。

我们将首先更改Sidebar.js文件中的信息。为了使用用户数据,我们需要用selectUser调用useSelector。在return语句中,我们在Avatarusernameemail中使用它。更新后的代码在这里以粗体显示:

import { useSelector } from 'react-redux'
import { selectUser } from '../features/userSlice'

const Sidebar = () => {
    const user = useSelector(selectUser)

  ...

    return (
    <div className="sidebar">
        <div className="sidebar__top">
        <img src="background.jpg" alt="Background" />
        <Avatar src={user?.photoUrl} className="sidebar__avatar">{user.email[0]}</Avatar>
        <h2>{user.displayName}</h2>
        <h4>{user.email}</h4>
        </div>
        ...
    </div>
    )
}

export default Sidebar

现在,在Header.js中,我们将在最后一个HeaderOption中传递一个布尔值,而不是传递硬编码的 URL。更新后的代码在这里以粗体显示:

const Header = () => {
  ...

    return (
    <div className="header">
        ...
        <div className="header__right">
            ...
        <HeaderOption avatar={true} title="me" onClick={logoutApp} />
        </div>
    </div>
    )
}

现在,在HeaderOption.js文件中,我们将再次使用uaeSelector来访问用户。之后,我们用emailphotoUrl的第一个字母。更新后的代码在这里以粗体显示:

import { useSelector } from 'react-redux'
import { selectUser } from '../features/userSlice'

const HeaderOption = ({ avatar, Icon, title, onClick }) => {
    const user = useSelector(selectUser)

    return (
    <div onClick={onClick} className="headerOption">
        {Icon && <Icon className="headerOption__icon" />}
        {avatar && <Avatar className="headerOption__icon" src={user?.photoUrl}>{user?.email[0]}</Avatar>}
        <h3 className="headerOption__title">{title}</h3>
    </div>
    )
}

export default HeaderOption

接下来,在Feed.js文件中,我们将再次使用uaeSelector来访问用户。然后我们在添加帖子的同时使用它。更新后的代码在这里以粗体显示:

import { useSelector } from 'react-redux'
import { selectUser } from '../features/userSlice'

const Feed = () => {
    const user = useSelector(selectUser)
  ...

    const sendPost = e => {
    e.preventDefault()
    db.collection('posts').add({
        name: user.displayName,
        description: user.email,
        message: input,
        photoUrl: user.photoUrl || '',
        timestamp: firebase.firestore.FieldValue.serverTimestamp()
    })
    setInput('')
    }

    return (
    ...
    )
}

export default Feed

现在,在Post.js中,我们使用从Feed组件传递来的photoUrl。更新后的代码在这里以粗体显示:

const Post = ({ name, description, message, photoUrl }) => {
return (
          <div className="post">
          <div className="post__header">
          <Avatar src={photoUrl}>{name[0]}</Avatar>
          <div className="post__info">
               <h2>{name}</h2>
               <p>{description}</p>
          <div>
          </div>
          </div>
)
}

export default Post

现在,为了让我们的应用看起来更好,需要在App.css中做一个小的修正。更新后的代码在这里以粗体显示:

.app__body{
  display: flex;
  margin-top: 35px;
  max-width: 1200px;
  margin-left: 20px;
  margin-right: 20px;
}

我们的应用几乎已经完成,用户数据看起来不错(图 5-17 )。

img/512002_1_En_5_Fig17_HTML.jpg

图 5-17

几乎完成

构建小部件部分

我们将构建最后一个部分,即小部件部分。在components文件夹中创建两个文件Widgets.jsWidgets.css。另外,在App.js文件中包含 Widgets 组件。更新后的代码在这里以粗体显示:

import Widgets from './components/Widgets';

function App() {
    ...
    ...

  return (
      <div className="app">
      <Header />
      {!user ? (<Login />) : (
      <div className="app__body">
      <Sidebar />
      <Feed />
      <Widgets />
      </div>
      )}
      </div>
  );
}

export default App;

现在,我们将把下面的内容放到Widgets.js文件中。这只是一个静态文件,其中有一个标题和一个信息图标。之后我们在调用函数newsArticle,用不同的属性。

import { FiberManualRecord, Info } from '@material-ui/icons'
import React from 'react'
import './Widgets.css'

const Widgets = () => {
    const newsArticle = (heading, subtitle) => (
        <div className="widgets__article">
            <div className="widgets__articleleft">
                <FiberManualRecord />
            </div>
            <div className="widgets__articleright">
                <h4>{heading}</h4>
                <p>{subtitle}</p>
            </div>
        </div>
    )

return (
    <div className="widgets">
      <div className="widgets__header">
        <h2>Tech News</h2>
        <Info />
      </div>
        {newsArticle("TWD at top with 500k subscriber", "Top news - 9099 readers")}
        {newsArticle("Qualcomm Snapdragon 775 Series", "Top news - 8760 readers")}
        {newsArticle("Amazfit T-Rex Pro Hands", "Top news - 999 readers")}
        {newsArticle("Apple Music Service Feature for iOS", "Top news - 899 readers")}
        {newsArticle("Mars Rover Perseverance Takes First Drive", "Top news - 799 readers")}
        {newsArticle("Twitter CEO Jack Dorsey Auctions Tweet", "Top news - 599 readers")}
    </div>
  )
}

export default Widgets

现在,我们将把它的样式放在Widgets.css文件中。

.widgets{
    position: sticky;
    top: 80px;
    flex: 0.2;
    background-color: white;
    border-radius: 10px;
    border: 1px solid lightgray;
    height: fit-content;
    padding-bottom: 10px;
}

.widgets__header{
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 10px;
}

.widgets__header > h2{
    font-size: 16px;
    font-weight: 400;
}

.widgets__article{
    display: flex;
    padding: 10px;
    cursor: pointer;
}

.widgets__article:hover{
    background-color: whitesmoke;
}

.widgets__articleleft{
    color: #0177b7;
    margin-left: 5px;
}

.widgets__articleleft > .MuiSvgIcon-root{
    font-size: 15px;
}

.widgets__articleright{
    flex: 1;
}

.widgets__articleright > h4{
    font-size: 14px;
}

.widgets__articleright > p{
    font-size: 12px;
    color: gray;
}

我们的应用现在已经完成了!看起来像图 5-18 。

img/512002_1_En_5_Fig18_HTML.jpg

图 5-18

我们的最终应用

通过 Firebase 部署和托管

我们可以在 Firebase 中部署我们的应用,我们将遵循与前面章节相同的步骤。

部署成功且工作正常(图 5-19 )。

img/512002_1_En_5_Fig19_HTML.jpg

图 5-19

部署

摘要

在本章中,你学习了如何制作一个与职业相关的社交媒体应用,你可以通过电子邮件登录。您了解了如何使用 React 创建 web 应用,还了解了如何使用 Redux。您还学习了如何在 Firebase 中进行托管。

posted @ 2024-10-01 21:06  绝不原创的飞龙  阅读(1)  评论(0编辑  收藏  举报