ReactJS入門 - Firebase 留言板設計實戰 - 筆記長也NotesHazuya

ReactJS入門 - Firebase 留言板設計實戰

2021-07-31 23:05:00   ReactJS

本篇將會是 React 入門的最後一篇文章,將會利用之前所學習過的技巧來設計一個簡單的留言板。

開始之前

本篇文章會透過提供程式碼範例來解說,建議各位可以先自行依照列出的需求做練習再看看是不是遇到什麼問題,再來參考解答。

程式碼部分,均會以 Functional Component 來設計,並且使用 ReactHook。也可以試著練習改成 Class Component 的設計。

技術規格

前端:React,後端:firebase,CSS 框架:Tocas UI

模板 TocasUI

這裡提供了很多範例:https://tocas-ui.com/examples/

我們使用的模板是這一個:https://examples.tocas-ui.com/pages/notes.html

如果有時間你也可以自己刻一個或是使用 React Bootstrap 或是 google material ui 等其他框架

需求描述

設計一個多人留言板,具有基本的留言功能,並做一個關於頁面,放置作者的訊息。

架構

為了可以容易練習到前面的東西,所以架構比較簡單一些

路由

路由名稱 功能描述 元件
/ 留言板首頁,可以留言與瀏覽留言 HomeComponent
/about 顯示關於作者的資訊 AboutComponent

元件

元件名稱 功能描述
AboutComponent 顯示關於作者資訊的元件。
MemoComponent 顯示已張貼的留言,組合多個不同的 MemoListComponent 以及 PostMemoComponent
PostMemoComponent 用於張貼留言
MemoListComponent 留言的個體(一則留言建立一個元件)
App(預設元件) 這一個元件將會被用來初始化 firebase 以及 設定路由,並做為預設的程式進入點。

Firebase

FireBase 是一個提供跨平台的雲端開發平台,可以讓開發者快速建立雲端的後端開發環境,包含本文將要介紹的即時資料庫(Realtime Database),是 FireBase 一個很熱門且核心的服務。

Realtime Database

即時資料庫是一種 noSql 的結構,儲存結構類似於陣列或是 json 格式,如下圖

有關留言板的內容都會放到這個資料庫當中。

建立 Firebase 專案

這邊先建立好一個 Firebase 的專案,稍後在實作的時候可以很快地開始使用。

首先,你要有一個 Google 帳號(應該大家都有),先進到 https://console.firebase.google.com/  然後點選建立專案

再來,會要求輸入專案名稱,基本上隨便取你喜歡的就可以了

點選繼續

這邊也不用特別更改設定,點選建立專案

基本上做到這邊 FiraBase 專案就完成了,會被帶入到增加應用程式的部分,我們點選網頁

填入應用程式名稱,勾勾可以不打勾,看個人

完成之後會要求放一段 JavaScript 程式碼,我們只需要使用到這一段:

var firebaseConfig = {
    apiKey: "",
    authDomain: "xx.firebaseapp.com",
    databaseURL: "https://xxx-default-rtdb.firebaseio.com",
    projectId: "xxx",
    storageBucket: "xxx.appspot.com",
    messagingSenderId: "xxx",
    appId: "xxx"
  };

先將這一段複製後,加上 export 並存在 react 的 src 工作目錄下並命名為 FireBaseConfig.js,稍後會使用到

var FireBaseConfig = {
    apiKey: "",
    authDomain: "xx.firebaseapp.com",
    databaseURL: "https://xxx-default-rtdb.firebaseio.com",
    projectId: "xxx",
    storageBucket: "xxx.appspot.com",
    messagingSenderId: "xxx",
    appId: "xxx"
  };
export default FireBaseConfig;

安裝 firebase 套件

先在專案下命令

npm i firebase

稍後會使用到

元件準備

由於專案本身並不大,我們先從大的元件開始向下篩寫程式碼。每一個元件都是一個獨立的 .js 檔案,並且所有的檔案都建立在 ./src 目錄下。

在按照我的流程篩寫程式碼的過程當中可能會遇到找不到檔案的狀況,建議先將上面的所有元件檔案都先建立好基本的版,再下去篩寫程式碼。

App.js

App 這個預設元件我們只做兩件事情:初始化 firebase、設定路由。

import React from 'react';
import { HashRouter, Route, Switch } from 'react-router-dom';
import firebase from 'firebase';
import FireBaseConfig from './FireBaseConfig.js';
import MemoComponenet from './MemoComponenet.js';
import AboutComponent from './AboutComponent.js';

const App = ()=>{
  if (!firebase.apps.length) {
      firebase.initializeApp(FireBaseConfig);  
  }

  return(
    <HashRouter>
      <Switch>
        <Route exact path="/" component={MemoComponenet} />
        <Route path="/about" component={AboutComponent} />
      </Switch>
    </HashRouter>
  )
}
export default App;

開始設定路由之前,要先安裝 react-router-dom 才可以,下指令

npm i react-router-dom

安裝好之後先引入 firebase 、 react-router-dom 當中的 HashRouter、Route 以及 Switch 三個元件、前面儲存的 FirebaseConfig 以及兩個 Route 會用到的 MemoComponent 、AboutComponent 兩個元件。

 關於 Firebase引入部分,應該要以個別功能為單位引入,否則會在 console (F12) 收到警告:

It looks like you're using the development build of the Firebase JS SDK.
When deploying Firebase apps to production, it is advisable to only import
the individual SDK components you intend to use.

個別引入會是這樣:

import firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";

不會只 import firebase 本身,本篇為了程式碼精簡所以直接引入 firebase 而已。

if (!firebase.apps.length)

這裡進行 firebase 的初始化,由於每次畫面更新有可能會導致重新渲染,所以會被重複執行初始化,因此要加上條件來判斷 firebase 是否已經被初始化

路由設定

本篇仍以 HashRouter 來設定,根目錄必須加上 exact 避免後面的路由都無法顯示。

MemoComponent.js

這個元件會是功能的核心,我們必須讓這個元件可以張貼留言(交給 PostMemoComponent 來做)以及印出留言(迭代 MemoListComponent)。

import React, { useState, useEffect, useRef } from 'react';
import MemoListComponent from './MemoListComponent.js';
import firebase from 'firebase';
import PostMemoComponent from './PostMemoComponenet.js';
import { Link } from 'react-router-dom';

const MemoComponenet = (props)=>{
    const [memoItems, setMemoItems] = useState();
    const renderCount = useRef(false);
    const [memoUpdate, setMemoUpdate] = useState(false);

    const updateMemo = ()=>{
        setMemoUpdate(!memoUpdate);
    }

    const getMemo = ()=>{
        var targData;
        firebase.database().ref('/memos').orderByChild('dateMark').once("value", e => {
            targData = e.val();
          }).then(
              ()=>{
                setMemoItems(targData);
              }
          );

    }

    useEffect(
        ()=>{
            if(renderCount.current === false){
                getMemo();
                renderCount.current = true;
            }
        },[]
    )

    useEffect(
        ()=>{
             getMemo();
        },[memoUpdate]
    )

    return(
        <>
            <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/2.3.3/tocas.css"></link>
            <PostMemoComponent callUpdateMemo={updateMemo}></PostMemoComponent>
            <div className="ts very padded relaxed stackable container grid">
                <div className="sixteen wide column">
                {
                    memoItems === undefined ? 
                    <div className="ts fluid container">
                        <br></br><br></br>
                        <div className="ts indeterminate progress">
                            <div className="bar"></div>
                        </div>
                        <p>載入中</p>
                    </div>
                    :
                    <div className="sixteen wide column">
                        <br></br>
                        <div className="ts stackable three waterfall cards">  
                            {
                                memoItems === null ? "" :
                                (
                                    Object.values(memoItems).reverse().map( (memoItem, key)=>(
                                        <MemoListComponent 
                                        key={key}
                                        memoContent={memoItem.content}
                                        memoDate={memoItem.date}
                                        name={memoItem.name}
                                        >    
                                        </MemoListComponent>

                                        )
                                    )
                                )
                            }
                        </div>
                    </div>
                }
                </div>
                <Link to="/about">關於我</Link>
            </div>
        </> 
    );
}
export default MemoComponenet;

這裡要引入幾個會用到的Hook、輸出留言的元件、發布新留言的元件、路由轉跳的 Link 以及 firebase。

memoItems

用來儲存從 firebase 拉回來的資料

renderCount

用來記錄是不是第一次渲染

memoUpdate

用以判斷是否有發佈新的留言

updateMemo

呼叫更新留言

getMemo

利用 firebase拉回資料,並依照 dateMark 排序(遞增)。

useEffect

這裡會用到兩個 useEffect ,一個用來首次載入留言板,另一個用來監測新的更新。

引入樣式表 

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/2.3.3/tocas.css"></link>
在 return 部分,記得要引入 Tocas 的樣式表,避免跑版
CSS 只要引入一次就好,因為 React 的 CSS 會影響其他子元件。

引入張留言元件

記得將更新留言的函式傳到元件,當使用者按下新增留言之後執行,通知更新留言。

memoItems === undefined ?  :

用以判斷留言是否已經被載入,沒有就顯示載入中。若已經載入則顯示留言。

memoItems === null ?  :

判斷有沒有留言,沒有就不做事情,輸出空字串

Object.values(memoItems).reverse().map()

這是 JavaScript 的原生函式,可以迭代物件。 而 reverse 則可以反轉。

                                        <MemoListComponent 
                                        key={key}
                                        memoContent={memoItem.content}
                                        memoDate={memoItem.date}
                                        name={memoItem.name}
                                        >    
                                        </MemoListComponent>

在這裡要注意,記得把要交給 MemoListComponent 的資料綁到元件上面去。

PostMemoComponenet.js

這裡只做一件事情:發表新的留言

import React, { useState } from 'react';
import firebase from 'firebase';

const PostMemoComponent = (props)=>{

    const [textDisabled, setTextDisabled] = useState(false);
    const [memoText, setMemoText] = useState("");
    const [name, setName] = useState("");
    const getDate = ()=>{

        let newDate = new Date();
        let date = newDate.getDate();
        let month = (newDate.getMonth() + 1);
        let year = newDate.getFullYear();
        let hour = newDate.getHours();
        let min = newDate.getMinutes();
        let sec = newDate.getSeconds();
        let day = newDate.getDay();
        return year + '-' + month + '-' + day + ' ' + hour + ':' + min + ':' + sec ;

    }
    const addMemo = ()=>{

        if (memoText==="") {
            alert('你沒有輸入任何東西');
        } else {
            setTextDisabled(true); 
            firebase.database().ref('/memos').push({
                content:memoText,
                date: getDate(),
                dateMark:Date.now(),
                name: name,
            }).then(() => {
                console.log('add data successful');
                setTextDisabled(false);
                setMemoText("");
                setName("");
                props.callUpdateMemo();
            });
        }
    }
    
    return(
        <div>
            <div className="ts very padded relaxed stackable container grid">
                <div className="sixteen wide column">
                    <br></br>
                    <h1 className="ts center aligned header">
                            我的秘密便利貼
                        <div className="sub header">
                            在這裡留下你想說ㄉ廢話。
                        </div>
                    </h1>

                    <div className="ts hidden divider"></div>
                </div>

                <div className="sixteen wide column">
                    <div className="ts segment">
                    <div className="ts borderless horizontally fitted fluid input">
                            <input disabled={textDisabled} value={name} placeholder="在此輸入你" onChange={(e)=>{ setName(e.target.value) }}/>
                        </div>
                        <div className="ts borderless horizontally fitted fluid input">
                            <textarea disabled={textDisabled} value={memoText} placeholder="在此輸入你想保存的話語⋯" onChange={(e)=>{ setMemoText(e.target.value) }}/>
                        </div>
                        <div className="ts secondary fitted menu">
                            <div className="stretched item">
                                <div className="ts tiny faded fitted basic message">
                                </div>
                            </div>
                            <div className="item">
                                <button disabled={textDisabled} className="ts mini primary button" onClick={addMemo}>送出</button>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>   
    )
}
export default PostMemoComponent;

一樣要將 firebase 引入。

textDisabled

在更新留言期間關閉表單輸入

memoText

使用者輸入的留言內容

name

使用者輸入的名稱

getDate

由於在資料庫我們儲存時間戳記,所以要顯示日期必須轉換

addMemo

將使用者輸入的留言新增至 firebase 的資料庫當中,使用 push 方法會讓新增的每一筆資料都有自己的 hashValue 作為 Key,新增完成後呼叫更新函式。

MemoListComponent.js

這個元件更簡單了,只需要將傳進來的 props 印到畫面上。

import React from 'react';

const MemoListComponent = (props)=>{

    return(
        <div className="ts card">
            <div className="content">
                <div className="meta">
                    <div>{props.memoDate}|{props.name}</div>
                </div>
            </div>
            <div className="content">
                <div className="description"><p>{props.memoContent}</p></div>
            </div>
        </div>
    )
}
export default MemoListComponent;

沒什麼好注意的,使用 props 記得加上「{ }」。

AboutComponent.js

這個也很簡單,可以自己發揮一下

import React from 'react';
import { Link } from 'react-router-dom';

const AboutComponent = (props)=>{
    return(
        <>
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/2.3.3/tocas.css"></link>
        <div className="ts very padded relaxed stackable container grid">
            <div className="sixteen wide column">
                <h1>我是誰</h1>
                <p>我是 onlymycast,讓你建立私人 podcast</p>
                <Link to="/">回上一頁</Link>
            </div>
        </div>
        </>
    )
}
export default AboutComponent;

但要記得重新引入 CSS 檔案,由於 AboutComponent 並不是 MemoComponent 的子元件。

成果

結論

首先,你已經是可以獨當一面的 React 開發者了,再來需要累積很多的實戰經驗來補足 React 的其他工具(如 Redux),本篇文章已經複習了很多先前介紹過的基本特性,包含:JSX、Component、Props、state、生命週期函數(本篇以 useEffect 實作)、useState、useEffect、useRef、表單處理以及 react-router-dom 。總之,如果你能自己完成這樣的小專案已經非常厲害,可以繼續練習其他專案了。

學習是無止無盡的

資訊技術的更迭速度飛快,一不小心就會被拋在後面,所以今天所學的雖然能用,但是未來可能會遇到版本的更新,就必須一直關注相關的社群。

技術也要一直追隨著相關科技或一些論壇,才能持續接收到新技術或是新的架構,思維也會隨著專案數量越來越多而有所成熟,我也還在學習的道路上,我們都不孤單。

感謝

最後還是感謝各位將本系列文章看到最後,未來 React 部分將會變成零星更新部分文章,不再是系列文。如果有幫助到你,我很高興!如果有什麼需要改進的地方,請不用不好意思,可以透過下方的表單跟我聯絡。

關於作者


長也

資管菸酒生,嘗試成為網頁全端工程師(laravel / React),技能樹成長中,閒暇之餘就寫一些筆記。 喔對了,也愛追一些劇,例如火神跟遺物整理師,推推。最愛的樂團應該是告五人吧(?)