如何用React、Material UI和Firebase建立一个Google Docs的克隆版

108 阅读17分钟

在这篇文章中,我们将使用React、Material UI和Firebase构建一个Google Docs克隆。

最终的应用程序将看起来像这样:

Screenshot-2022-05-08-145537

如果我们点击任何文档,它就会被打开,我们可以根据需要对其进行编辑:

Screenshot-2022-05-08-145643

而最令人惊奇的是,我们可以实时地编辑文档。这意味着,如果两个人在同一个文件上工作,他们的进展将反映在两个实例上。

但在我们开始之前,请确保你的系统中已经安装了Node。如果没有,请到nodejs.org/en/download…,下载并安装它。

基本项目设置

让我们首先使用下面的命令创建一个React应用程序。

npx create-react-app google-docs-clone

这将把所有的软件包和依赖项安装到本地文件夹中。

然后,简单地导航到项目文件夹,运行npm start来运行该应用。

Screenshot-2022-05-07-105724

我们将在这里看到所有这些我们需要删除的代码。我们将从一个空白的画布开始。

接下来创建一个名为组件的文件夹。在该文件夹内,让我们创建一个名为docs.js的文件。

Screenshot-2022-05-07-110011

让这个组件成为一个功能组件,像这样。

import React from 'react'

export default function Docs() {
  return (
    <div>
        <h1>docs</h1>
    </div>
  )
}

现在,把这个文件导入到主App.js文件中。

import './App.css';
import Docs from './components/docs';

function App() {
  return (
    <Docs />
  );
}

export default App;

然后我们会在屏幕上看到这样的输出。

Screenshot-2022-05-07-110713

Google docs的克隆,在左上角显示输出 "docs"。

现在,让我们让标题出现在中间。因此,在docs.js中,给maindiv一个 docs-main的className。

import React from 'react'

export default function Docs() {
  return (
    <div className='docs-main'>
        <h1>Docs Clone</h1>
    </div>
  )
}

并在App.css文件中,添加以下样式。

.docs-main{
    text-align: center;
}

现在我们的应用程序看起来像这样。

Screenshot-2022-05-07-113002

标题在中间的Google docs克隆

现在,我们需要一个按钮来添加我们的文档。所以,让我们用这个代码来创建它。

import React from 'react'

export default function Docs() {
  return (
    <div className='docs-main'>
        <h1>Docs Clone</h1>

        <button className='add-docs'>
            Add a Document
        </button>
    </div>
  )
}

而CSS看起来是这样的。

.add-docs{
    height: 40px;
    width: 200px;
    background-color: #ffc107;
    border: none;
    cursor: pointer;
}

让我们从Google Fonts导入一些字体。把这个放在CSS文件的顶部。

@import url('https://fonts.googleapis.com/css2?family=Poppins&family=Roboto&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Poppins&family=Roboto&display=swap');

.docs-main{
    text-align: center;
    font-family: 'Roboto', sans-serif;
}

.add-docs{
    height: 40px;
    width: 200px;
    background-color: #ffc107;
    border: none;
    cursor: pointer;
    font-family: 'Poppins', sans-serif;
}

要添加字体,只需在各自的classNames中这样做。

如何安装Material UI

要安装Material UI,只需输入下面的命令。如果你想阅读文档,请到mui.com/。

npm install @mui/material @emotion/react @emotion/styled

现在,让我们再为模态创建一个组件。我们将使用这个模态来向Firebase数据库添加文件。

import * as React from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import Modal from '@mui/material/Modal';

const style = {
    position: 'absolute',
    top: '50%',
    left: '50%',
    transform: 'translate(-50%, -50%)',
    width: 400,
    bgcolor: 'background.paper',
    border: '2px solid #000',
    boxShadow: 24,
    p: 4,
};

export default function Modal() {
    const [open, setOpen] = React.useState(false);
    const handleOpen = () => setOpen(true);
    const handleClose = () => setOpen(false);

    return (
        <div>
            <Modal
                open={open}
                onClose={handleClose}
                aria-labelledby="modal-modal-title"
                aria-describedby="modal-modal-description"
            >
                <Box sx={style}>
                    <Typography id="modal-modal-title" variant="h6" component="h2">
                        Text in a modal
                    </Typography>
                    <Typography id="modal-modal-description" sx={{ mt: 2 }}>
                        Duis mollis, est non commodo luctus, nisi erat porttitor ligula.
                    </Typography>
                </Box>
            </Modal>
        </div>
    );
}

这是一个来自Material UI的简单模态组件。现在我们要把这个组件导入我们的Docs.js组件中。

而且我们需要把一些东西从Modal.js移到Docs.js中。

const [open, setOpen] = React.useState(false);
const handleOpen = () => setOpen(true);

如果我们点击添加文档按钮,模态将使用这些函数打开。

import React, { useState } from 'react';
import Modal from './Modal';

export default function Docs() {
    const [open, setOpen] = React.useState(false);
    const handleOpen = () => setOpen(true);
    return (
        <div className='docs-main'>
            <h1>Docs Clone</h1>

            <button
                className='add-docs'
                onClick={handleOpen}
            >
                Add a Document
            </button>

            <Modal
                open={open}
                setOpen={setOpen}
            />
        </div>
    )
}

所以,将这些函数和状态作为道具传入模态组件,并接收它们。

import * as React from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import Modal from '@mui/material/Modal';

const style = {
    position: 'absolute',
    top: '50%',
    left: '50%',
    transform: 'translate(-50%, -50%)',
    width: 400,
    bgcolor: 'background.paper',
    border: '2px solid #000',
    boxShadow: 24,
    p: 4,
};

export default function ModalComponent({
    open,
    setOpen,
}) {
    const handleClose = () => setOpen(false);

    return (
        <div>
            <Modal
                open={open}
                onClose={handleClose}
                aria-labelledby="modal-modal-title"
                aria-describedby="modal-modal-description"
            >
                <Box sx={style}>
                    <Typography id="modal-modal-title" variant="h6" component="h2">
                        Text in a modal
                    </Typography>
                    <Typography id="modal-modal-description" sx={{ mt: 2 }}>
                        Duis mollis, est non commodo luctus, nisi erat porttitor ligula.
                    </Typography>
                </Box>
            </Modal>
        </div>
    );
}

现在,这就是我们的页面与模态的外观。

Screenshot-2022-05-07-115100

显示模型的Google docs克隆页面

让我们在模态中添加一个输入,用于输入文件名。

<Modal
                open={open}
                onClose={handleClose}
                aria-labelledby="modal-modal-title"
                aria-describedby="modal-modal-description"
            >
                <Box sx={style}>
                    <input
                        placeholder='Add the Title'
                        className='add-input'
                    />
                </Box>
            </Modal>

让我们用下面的方法给它一些样式。

.add-input{
    width: 95%;
    height: 40px;
    outline: none;
    border: 1px solid #676767;
    border-radius: 0px;
    padding: 10px;
    font-family: 'Poppins', sans-serif;
}

现在,这就是我们的模版的样子。

Screenshot-2022-05-07-115756

添加了样式的Modal

让我们也添加一个按钮。我们可以复制Add a Document Button。

import * as React from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import Modal from '@mui/material/Modal';

const style = {
    position: 'absolute',
    top: '50%',
    left: '50%',
    transform: 'translate(-50%, -50%)',
    width: 500,
    height: 150,
    bgcolor: 'background.paper',
    boxShadow: 24,
    p: 5,
};

export default function ModalComponent({
    open,
    setOpen,
}) {
    const handleClose = () => setOpen(false);

    return (
        <div>
            <Modal
                open={open}
                onClose={handleClose}
                aria-labelledby="modal-modal-title"
                aria-describedby="modal-modal-description"
            >
                <Box sx={style}>
                    <input
                        placeholder='Add the Title'
                        className='add-input'
                    />
                    <div className='button-container'>
                        <button
                            className='add-docs'
                        >
                            Add
                        </button>
                    </div>
                </Box>
            </Modal>
        </div>
    );
}

而CSS看起来像这样。

.button-container{
    text-align: center;
    margin: 30px;
}

这就是它现在的样子。

Screenshot-2022-05-07-120129

添加了样式和按钮的模态

如何在我们的应用程序中添加Firebase

现在,让我们为数据库安装Firebase。使用下面的命令简单地安装Firebase。

npm install firebase

前往firebase.google.com/,点击右上方的Go to console。

Screenshot-2022-05-07-120526

然后,点击添加项目。

Screenshot-2022-05-07-120625

创建项目后,点击代码按钮,在Firebase中创建一个Web应用。给它起个名字,我们就可以开始了。

Screenshot-2022-05-07-120803

现在,我们将添加所有这些我们必须存储在React应用中的配置数据。所以,创建一个名为firebaseConfig.js的文件,并添加它们。

Screenshot-2022-05-07-120857

我们将需要数据库,所以让我们把它激活。同时,像这样导出const应用程序和数据库。

import { initializeApp } from "firebase/app";
import { getFirestore } from 'firebase/firestore';

const firebaseConfig = {
  //Your Firebase Data
};

export const app = initializeApp(firebaseConfig);
export const database = getFirestore(app)

将应用程序和数据库导入App.js文件。并将数据库作为道具传递给Docs组件。我们以后会用它来向Firebase Firestore添加数据。

import './App.css';
import Docs from './components/docs';
import { app, database } from './firebaseConfig';

function App() {
  return (
    <Docs database={database}/>
  );
}

export default App;

而在Docs组件中。同时,让我们从props中接收数据库的导出。

import React, { useState } from 'react';
import Modal from './Modal';

export default function Docs({
    database
}) {
    const [open, setOpen] = React.useState(false);
    const handleOpen = () => setOpen(true);
    return (
        <div className='docs-main'>
            <h1>Docs Clone</h1>

            <button
                className='add-docs'
                onClick={handleOpen}
            >
                Add a Document
            </button>

            <Modal
                open={open}
                setOpen={setOpen}
            />
        </div>
    )
}

现在,让我们来配置我们的Firestore数据库。

从左边的侧边栏转到Firestore数据库,然后点击创建数据库。

Screenshot-2022-05-07-121804

我们将在生产模式下启动我们的数据库。所以,点击下一步,然后启用。

Screenshot-2022-05-07-121900

我们必须使安全规则公开,只是现在。所以,点击顶部标签的规则,编辑以下规则。这意味着任何人都可以写入数据或读取它们,即使没有认证。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write;
    }
  }
}

如何向Firestore数据库添加文档数据

现在,让我们实际添加我们的数据。但在这之前,我们需要从输入字段中获取数据。

所以在Docs组件中,创建一个状态来保存这些数据。

const [title, setTitle] = useState('')

把标题和setTitle传给模态组件。

<Modal
                open={open}
                setOpen={setOpen}
                title={title}
                setTitle={setTitle}
            />

把它们都作为道具接收,并在输入框中设置它们。

import * as React from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import Modal from '@mui/material/Modal';

const style = {
    position: 'absolute',
    top: '50%',
    left: '50%',
    transform: 'translate(-50%, -50%)',
    width: 500,
    height: 150,
    bgcolor: 'background.paper',
    boxShadow: 24,
    p: 5,
};

export default function ModalComponent({
    open,
    setOpen,
    title, 
    setTitle
}) {
    const handleClose = () => setOpen(false);

    return (
        <div>
            <Modal
                open={open}
                onClose={handleClose}
                aria-labelledby="modal-modal-title"
                aria-describedby="modal-modal-description"
            >
                <Box sx={style}>
                    <input
                        placeholder='Add the Title'
                        className='add-input'
                        onChange={(event) => setTitle(event.target.value)}
                        value={title}
                    />
                    <div className='button-container'>
                        <button
                            className='add-docs'
                        >
                            Add
                        </button>
                    </div>
                </Box>
            </Modal>
        </div>
    );
}

现在,如果我们在输入框中输入一些东西,它将被保存在标题状态中。

接下来,我们需要一个函数来触发添加数据的函数,所以让我们来创建它。

在Docs.js中,创建一个函数并把它传递给模态组件。

const addData = () => {
        
}

在模态组件中接收它,并像这样简单地将它绑定到添加按钮上。

<div className='button-container'>
                        <button
                            className='add-docs'
                            onClick={addData}
                        >
                            Add
                        </button>
                    </div>

现在,addData函数将在我们点击添加按钮时运行。

现在,为了将数据从React动态地发送到Firebase,让我们从Firebase Firestore导入一些东西。

import { addDoc, collection } from 'firebase/firestore';

在这里,我们将使用collection ,在Firebase中创建一个数据集合,addDoc将向该集合添加数据。

让我们首先创建一个集合引用。它将接受我们从firebaseConfig.js中得到的数据库和我们想使用的集合的名称。

const collectionRef = collection(database, 'docsData')

现在,在addData函数中,让我们使用addDoc。这个addDoc 函数将接受集合的引用,以及数据本身。

const addData = () => {
        addDoc(collectionRef, {
            title: title
        })
        .then(() => {
            alert('Data Added')
        })
        .catch(() => {
            alert('Cannot add data')
        })
    }

现在,在文本输入中添加一些东西,然后点击添加。它将被添加到Firebase Firestore中,并有一个提示说数据已经被添加。但是如果失败了,我们会得到 "无法添加数据"。

Screenshot-2022-05-07-123810

如果我们刷新数据库,我们会看到这个新条目。

Screenshot-2022-05-07-123848

这就是我们添加数据的方法。让我们也在添加数据后关闭模态。

创建一个函数handleClose,并在addData 函数的then 块之后直接调用这个函数。

const addData = () => {
        addDoc(collectionRef, {
            title: title
        })
        .then(() => {
            alert('Data Added');
            handleClose()
        })
        .catch(() => {
            alert('Cannot add data')
        })
    }

如何从Firebase读取数据

现在,让我们来读取我们添加到Firebase的数据。我们将需要onSnapshot 函数来实现。onSnapshot 函数实时地获取数据。

首先,像这样从Firebase导入它。

import { addDoc, collection, onSnapshot } from 'firebase/firestore';

然后,创建一个函数getData ,它将在我们的页面加载时被触发。因此,我们将把这个onSnapshot 放入ReactuseEffect Hook中。

const getData = () => {
        onSnapshot(collectionRef, (data) => {
            console.log(data.docs.map((doc) => {
                return {...doc.data(), id: doc.id}
            }))
        })
    }

然后,在useEffect Hook里面调用这个函数。

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

Screenshot-2022-05-08-120757

但正如你所看到的,我们得到了两次数据。这是因为我们使用的是React 18版本,其中包括并发渲染。这就是为什么useEffect钩子会运行两次。

为了解决这个问题,我们需要创建一个useRef 引用。

const isMounted = useRef()

然后在useEffect Hook中,我们必须检查isMounted.current是否为真。所以,如果它是真的,我们将不返回任何东西。然后,我们将设置isMounted.current为真,然后我们将调用我们的getData函数。

useEffect(() => {
        if(isMounted.current){
            return 
        }

        isMounted.current = true;
        getData()
    }, [])

如果我们现在刷新页面,我们将只得到一次数据。

Screenshot-2022-05-08-121500

现在,我们必须将这些数据纳入一个数组状态。所以,让我们来做这件事。

创建一个docsData的状态。

 const [docsData, setDocsData] = useState([]);

并使用setDocsData在这个状态中设置传入的数据。

const getData = () => {
        onSnapshot(collectionRef, (data) => {
            setDocsData(data.docs.map((doc) => {
                return {...doc.data(), id: doc.id}
            }))
        })
    }

现在,让我们映射我们的数组,让数据显示在用户界面上。

<div>
                {docsData.map((doc) => {
                    return (
                        <div>
                            <p>{doc.title}</p>
                        </div>
                    )
                })}
            </div>

这将在我们的React应用程序中显示所有的数据。

Screenshot-2022-05-08-121859

我们将在我们的页面上看到两个文档。但让我们让它们出现在一个网格中。给这些div容器取一个grid-maingrid-child的类名。

<div className='grid-main'>
                {docsData.map((doc) => {
                    return (
                        <div className='grid-child'>
                            <p>{doc.title}</p>
                        </div>
                    )
                })}
            </div>

并在CSS中,添加以下类。

.grid-main{
    display: grid;
    grid-template-columns: auto auto auto auto;
    color: whitesmoke;
    margin-top: 20px;
    gap: 20px;
    justify-content: center;
}

.grid-child{
    padding: 20px;
    background-color: rgb(98, 98, 98);
    width: 300px;
    cursor: pointer;
}

现在,我们的应用程序将看起来像这样。

Screenshot-2022-05-08-122658

如何获得ID并重定向到编辑文档页面

现在,上面这些项目中的每一个都有一个ID。我们将使用这些ID重定向到另一个页面,在那里我们可以编辑这些项目并编写我们的主要内容。

为此,我们需要两个包。一个是React-Router,用于重定向,另一个是React-Quill,用于我们的编辑器。像这样安装它们。

npm i react-quill react-router-dom@6

现在,让我们来配置路由到另一个页面。但我们首先需要另一个页面。所以,让我们来创建它。

创建一个叫做EditDocs 的组件**。** 让它成为一个功能性组件。

import React from 'react'

export default function EditDocs() {
  return (
    <div>EditDocs</div>
  )
}

为了配置路由,来到index.js,即应用程序的入口点。将包裹在BrowserRouter里面。

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from "react-router-dom";

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

现在,我们可以在任何地方使用路由,因为我们在基本层面上声明了BrowserRouter。

现在,来看看App.js文件。从React-Router导入Routes和Route 。我们还在editDocs路径中追加ID,这样我们就可以在地址栏中看到ID。

import { Routes, Route } from "react-router-dom";
import './App.css';
import Docs from './components/docs';
import EditDocs from './components/EditDocs';
import { Routes, Route } from "react-router-dom";
import { app, database } from './firebaseConfig';

function App() {
  return (
    <Routes>
      <Route path="/" element={<Docs database={database} />} />
      <Route path="/editDocs/:id" element={<EditDocs database={database}/>} />
    </Routes>
  );
}

export default App;

并添加以下路由。如果我们去 '/editDocs/:id',我们将看到我们的editDocs页面。

Screenshot-2022-05-08-124659

现在,我们需要从文档中获取特定的ID,并将其发送到editDocs 页面。

创建一个函数getID,并将该函数分配给文档。

const getID = () => {

}
<div className='grid-main'>
                {docsData.map((doc) => {
                    return (
                        <div className='grid-child' onClick={() => getID(doc.id)}>
                            <p>{doc.title}</p>
                        </div>
                    )
                })}
            </div>

现在,如果我们点击文档,如果我们在控制台中记录它,我们将得到它的ID。

const getID = (id) => {
        console.log(id)
    }

Screenshot-2022-05-08-124956

现在,让我们使用useNavigate将这个ID发送到editDocs 页面。

首先,从 react-router 中导入 useNavigate 。

import { useNavigate } from 'react-router-dom';

然后,像这样创建一个useNavigate的实例。

let navigate = useNavigate();

然后,要传递ID,只需这样做。我们将把自己和 ID 一起发送到 editDocs 页面。

const getID = (id) => {
        navigate(`/editDocs/${id}`)
}

现在,让我们在另一端接收我们的ID。在editDocs 组件中,我们需要使用 react-router 的Params

所以,导入它并创建一个实例。

import { useParams } from 'react-router-dom';

let params = useParams();

同样,如果我们控制台,我们将看到ID。

import { useParams } from 'react-router-dom';

let params = useParams();
console.log(params)

Screenshot-2022-05-08-125849

我们可以看到,我们在地址栏以及控制台中得到了ID。

现在,让我们把React Quill添加到我们的editDocs 页面。

import React from 'react';
import { useParams } from 'react-router-dom';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
export default function EditDocs() {
    let params = useParams();
    return (
        <div>
            <h1>EditDocs</h1>

            <ReactQuill />
        </div>
    )
}

我们必须导入React-Quill和CSS。

Screenshot-2022-05-08-131423

但是我们可以看到我们在这里有两个工具条。要解决这个问题,只要从index.js中删除React.StrictMode

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from "react-router-dom";

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

我们就会没事了。

Screenshot-2022-05-08-131534

现在,我们需要为这个React Quill数据建立一个状态。所以,让我们来创建它。同时,我们将创建一个函数,在我们输入时触发。

const [docsDesc, setDocsDesc] = useState('');
    const getQuillData = () => {
        
    }

现在,让我们把这个函数和状态绑定到React Quill。

<ReactQuill
   value={docsDesc}
   onChange={getQuillData}
/>

getQuillData 函数中,让我们使用setDocsDesc 函数,将值绑定到docsDesc 状态中。

const getQuillData = (value) => {
        setDocsDesc(value)
    }

我们就在这里完成了。你可以用控制台来检查这个docsDesc状态。

现在我们有了ID,以及我们可以用来更新文档的数据。所以,让我们来做这件事。

如何更新文档

我们需要两样东西,updateDoc集合 函数。我们将使用一个Debounce函数来调用updateDoc函数。这意味着,当我们打完字,5或10秒后,我们的updateDoc函数将运行。

所以,让我们创建一个函数。

const updateDocsData = () => {

}

我们还需要指定集合。为此,我们需要App.js中的数据库 **。**所以,让我们使用props来获得它。

<Route path="/editDocs/:id" element={<EditDocs database={database}/>} />

现在,让我们创建一个集合引用。

const collectionRef = collection(database, 'docsData')

现在,为了去伪存真,我们需要在useEffect钩中的updateDocsData

useEffect(() => {
        const updateDocsData = () => {

        }
 }, [])

现在,让我们添加一个有间隔的setTimeout函数。这意味着该函数将在指定的时间间隔后运行。把时间间隔定为1000毫秒,或1秒

useEffect(() => {
    const updateDocsData = setTimeout(() => {
      
    }, 1000)  
    return () => clearTimeout(updateDocsData)
  }, [])

现在,在setTimeOut里面,让我们添加updateDoc函数。所以在文档变量里面,我们要从参数中传递collectionRefID 。然后,updateDoc 将变量document作为第一个参数。

const updateDocsData = setTimeout(() => {
            const document = doc(collectionRef, params.id)
            updateDoc(document, {

            })
        }, 1000)

让我们也导入doc 函数。它指定使用ID作为主键来更新哪个文档。

import {
    updateDoc,
    collection,
    doc
} from 'firebase/firestore';

现在让我们在 updateDoc 函数的第二个参数中传递数据。

useEffect(() => {
        const updateDocsData = setTimeout(() => {
            const document = doc(collectionRef, params.id)
            updateDoc(document, {
                docsDesc: docsDesc
            })
        }, 1000)
        return () => clearTimeout(updateDocsData)
    }, [])

在依赖数组中,添加docsDesc 的状态**。** 所以在我们输入一些东西后**,** updateDoc函数将在1秒后运行。

useEffect(() => {
        const updateDocsData = setTimeout(() => {
            const document = doc(collectionRef, params.id)
            updateDoc(document, {
                docsDesc: docsDesc
            })
            .then(() => {
                alert('Saved')
            })
            .catch(() => {
                alert('Cannot Save')
            })
        }, 1000)
        return () => clearTimeout(updateDocsData)
    }, [docsDesc])

所以,在编辑器中输入一些东西,它将被保存在数据库里面。

Screenshot-2022-05-08-135046

而这里的数据。

Screenshot-2022-05-08-135105

如果我们进一步添加一些东西,我们将附加上之前的数据。

Screenshot-2022-05-08-135224

Screenshot-2022-05-08-135242

如何将数据从数据库中取回到编辑器中

现在,如果我们回去点击任何一个文件,数据就会变成空的,或者被删除。所以,我们必须从数据库中获取数据并将其设置到编辑器中。

我们将使用onSnapshot 函数来做到这一点。

import {
    updateDoc,
    collection,
    doc,
    onSnapshot
} from 'firebase/firestore';
const getData = () => {
        
    }

    useEffect(() => {
        if(isMounted.current){
            return 
        }

        isMounted.current = true;
        getData()
    }, [])

所以,这就像我们在Docs 组件中所做的那样。我们需要使用ID参数指定要获取的数据。然后我们把这个文档传递给onSnapshot函数来获取我们需要的数据。

const getData = () => {
        const document = doc(collectionRef, params.id)
        onSnapshot(document, (docs) => {
            console.log(docs.data().docsDesc)
        })
    }

Screenshot-2022-05-08-140423

让我们使用setDocsDesc将这个docs.data().docsDesc 设置在docsDesc状态。 所以,如果文档加载,它将被设置在那里。

添加一些数据,然后再回去。如果你回到同一个组件,文档描述就会出现在那里。

Screenshot-2022-05-08-140723

现在在我们看到所有数据的主页上,我们也需要添加描述,如果它存在的话。

 <div dangerouslySetInnerHTML={{__html: doc.docsDesc}} />

我们使用危险的SetInnerHTML ,因为在React Quill中,数据是以标签的形式添加的。这使得渲染格式化更容易。

Screenshot-2022-05-08-141304

看,我已经添加了一些格式,如粗体斜体 文本。

现在,我们需要做一些轻微的修改。在App.js文件中(我们正在添加文档标题),让我们也添加描述,它最初将是空的。

const addData = () => {
        addDoc(collectionRef, {
            title: title,
            docsDesc: ''
        })
        .then(() => {
            alert('Data Added');
            handleClose()
        })
        .catch(() => {
            alert('Cannot add data')
        })
    }

因此,如果我们创建一个文档,我们将在Firestore文档中拥有docsDesc。这将防止我们的应用程序在进入EditDocs页面时崩溃。

现在,在EditDocs页面,让我们添加文档的标题,以便它显示在顶部。创建一个叫做 documentTitle 的状态并设置它。

const [documentTitle, setDocumentTitle] = useState('')

const getData = () => {
        const document = doc(collectionRef, params.id)
        onSnapshot(document, (docs) => {
            setDocumentTitle(docs.data().title)
            setDocsDesc(docs.data().docsDesc);
        })
    }

然后在顶部显示这个状态。

<h1>{documentTitle}</h1>

下面是到目前为止EditDocs 页面的全部代码。

import React, { useEffect, useState, useRef } from 'react';
import { useParams } from 'react-router-dom';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
import {
    updateDoc,
    collection,
    doc,
    onSnapshot
} from 'firebase/firestore';
export default function EditDocs({
    database
}) {
    const isMounted = useRef()
    const collectionRef = collection(database, 'docsData')
    let params = useParams();
    const [documentTitle, setDocumentTitle] = useState('')
    const [docsDesc, setDocsDesc] = useState('');
    const getQuillData = (value) => {
        setDocsDesc(value)
    }
    useEffect(() => {
        const updateDocsData = setTimeout(() => {
            const document = doc(collectionRef, params.id)
            updateDoc(document, {
                docsDesc: docsDesc
            })
                .then(() => {
                    alert('Saved')
                })
                .catch(() => {
                    alert('Cannot Save')
                })
        }, 1000)
        return () => clearTimeout(updateDocsData)
    }, [docsDesc])

    const getData = () => {
        const document = doc(collectionRef, params.id)
        onSnapshot(document, (docs) => {
            setDocumentTitle(docs.data().title)
            setDocsDesc(docs.data().docsDesc);
        })
    }

    useEffect(() => {
        if (isMounted.current) {
            return
        }

        isMounted.current = true;
        getData()
    }, [])
    return (
        <div>
            <h1>{documentTitle}</h1>

            <ReactQuill
                value={docsDesc}
                onChange={getQuillData}
            />
        </div>
    )
}

如何添加一些样式

现在让我们在这个EditDocs页面中添加一些样式。

<div className='editDocs-main'>
            <h1>{documentTitle}</h1>
            <div className='editDocs-inner'>
                <ReactQuill
                    className='react-quill'
                    value={docsDesc}
                    onChange={getQuillData}
                />
            </div>
        </div>

在CSS中,添加以下样式。


.editDocs-main {
    font-family: 'Poppins', sans-serif;
    padding: 20px;
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
}

.editDocs-inner {
    width: 800px;
    box-shadow: 0px -2px 5px 2px rgba(181, 181, 181, 0.75);
    -webkit-box-shadow: 0px -2px 5px 2px rgba(181, 181, 181, 0.75);
    -moz-box-shadow: 0px -2px 5px 2px rgba(181, 181, 181, 0.75);
    padding: 20px;
    height: 750px;
}

.ql-container.ql-snow {
    border: none !important;
}

我们正在添加一个盒状阴影,我们正在删除React Quill的边框,并且我们正在将所有东西居中。

这就是我们的编辑文档页面现在的样子。

Screenshot-2022-05-08-144341

现在是我们的最后一件事:让我们用吐司信息取代我们的警报。我们还需要一个叫React Toastify的包。所以,让我们安装它。

npm i react-toastify

然后我们需要导入这两个东西。

import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';

然后,****组件。

现在,对于吐司信息,只需这样做。

useEffect(() => {
        const updateDocsData = setTimeout(() => {
            const document = doc(collectionRef, params.id)
            updateDoc(document, {
                docsDesc: docsDesc
            })
                .then(() => {
                    toast.success('Document Saved', {
                        autoClose: 2000
                    })
                })
                .catch(() => {
                    toast.error('Cannot Save Document', {
                        autoClose: 2000
                    })
                })
        }, 1000)
        return () => clearTimeout(updateDocsData)
    }, [docsDesc])

我们有toast.success用于成功警报,toast.error用于错误警报。

Screenshot-2022-05-08-145209

结论

就这样,你已经建立了一个Google Docs的克隆。你可以自由地进行实验,使其变得更好。