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

108 阅读20分钟

React 和 Firebase 入门指南(二)

原文:Beginning React and Firebase

协议:CC BY-NC-SA 4.0

五、使用 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 中进行托管。