Next.js、TypeScript和Firebase数据库介绍

487 阅读10分钟

Next.js、TypeScript和Firebase数据库介绍

Next.js是一个基于React的框架,使开发者能够轻松地创建可用于生产的Web应用程序。它具有各种预定义的功能,使开发者能够快速扩展应用程序。

Next.js是一个混合框架。这意味着它可以用于客户端和服务器端的渲染。它还可以利用静态生成功能,你可以在构建时用来获取数据和预渲染。这可以产生快速的网页。

前提条件

要跟上这篇文章,你需要。

  • 在你的电脑上安装[Node.js]。
  • 一些基本的HTML、CSS和JavaScript基础知识。
  • 一些关于[React]、[Next.js]和[TypeScript]的工作知识。

设置TypeScript

我们将使用TypeScript构建这个应用程序。Next.js与TypeScript捆绑在一起。

TypeScript带有额外的功能,使你的代码变得极简。这些功能包括静态类型、类型符号、类型检查等。

要使用TypeScript,你需要安装TypeScript JavaScript库。这将使TypeScript可以进入我们的项目。

使用Next.js,你只需要在create-next-app 命令中添加一个--ts 标志。例如,运行npx create-next-app@latest --ts next-js-firebase-app 将自动设置默认的TypeScript环境。

这将在我们的Next.js项目中添加typescript ,和@types/react 。注意,Next.js是一个React.js框架。

所以在使用TypeScript时,我们需要React类型定义。@types/ ,为React等第三方框架提供TypeScript功能。

创建一个Firebase项目

要设置Firebase,请使用以下步骤。

首先,进入Firebase控制台添加项目。输入你的项目的首选名称,即:next-js-todos-app 。然后点击继续。

然后Configure Google Analytics ,再点击继续。

创建一个项目,给它一些时间来完成这个过程。当项目准备好后,点击继续

添加一个Firebase应用

下一步是创建一个Firebase应用。我们可以用下面的代码实现这个功能。

在新创建的项目页面上,点击网络图标(</>)。

输入你喜欢的应用程序名称,即:next-js-todos-app 。然后点击注册应用程序继续进入控制台

在下一步,我们将设置Firestore

设置Firestore

要设置Firestore,请按照以下步骤进行。

在Firebase应用程序中,导航到左边的菜单,在build ,然后点击Firestore数据库,然后创建数据库

因为我们不是在建立一个生产应用,所以选择在测试模式下开始,然后转到下一步

从可用的选项列表中选择Cloud Firestore的位置,然后点击启用来设置所选位置。

在出现的页面中,我们将首先创建一个集合,从我们的Next.js应用程序中填充。

单击 "开始收集 "并添加收集id为todos,然后转到下一步

点击Auto-ID自动填充文档id字段,并添加一个标题字段作为字符串。

点击添加字段,添加一个描述字段为字符串,并给它一个值:Cook a delicious dinner

添加一个新的字段done,它是一个布尔值,并给它一个值false

你的表单应该类似于。

initial-collection-setup-form

现在该文件应该反映在集合中,如下图所示。

initial-collection

在下一步,我们将设置我们的Next.js应用程序。

设置Next.js应用程序

为了设置我们的Next.js应用程序,我们将使用create-next-app

为了设置它,我们将遵循以下步骤。

创建一个你希望项目所在的文件夹。

cd ./your-project-folder-path

运行以下命令,用TypeScript引导Next.js应用程序。

npx create-next-app@latest --ts next-js-firebase-app

上述命令将在next-js-firebase-app 文件夹内创建Next.js应用程序。

我们还添加了一个--ts 标志。这意味着生成的Next.js应用程序将是TypeScript友好的。所有文件都将被设置为.tsx ,而不是通常的.js

由于我们将与Firebase一起工作,下一步是安装Firebase包。

这将把Firebase SDK添加到Next.js项目中。改变目录,确保你的命令行指向next-js-firebase-app 文件夹。

然后运行这个命令,以获得Firebase JavaScript库的安装。

npm install firebase

安装好Firebase后,运行下面的命令来启动开发服务器。

npm run dev

在你的浏览器中,导航到http://localhost:3000 。你应该可以看到以下默认的Next.js登陆页面。

default-landing-page

这表明Next.js的模板正在工作。

让我们从整合Firebase和添加TypeScript开始。

初始化Firebase应用程序

下一步将是在Next.js应用程序中初始化Firebase数据库。

简单来说,初始化Firebase应用程序意味着连接Firebase数据库实例/SDK,这样我们就可以工作并扩展Next.js应用程序。

这只是涉及到收集我们的Firebase应用所特有的Firebase凭证。

为了初始化它,我们将使用以下步骤。

在你的项目根目录下创建一个env.local 文件。这将承载环境变量。

在你的Firebase仪表板上,导航到项目设置。向下滚动到你的应用程序部分,然后到SDK设置和配置

在应用程序设置中,我们将采取firebaseConfig 对象。将其内容提取到.env.local 文件中,如下所示。

NEXT_PUBLIC_FIREBASE_API_KEY = "your_api_key"
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN = "your_auth_domain"
NEXT_PUBLIC_FIREBASE_PROJECT_ID = "your_project_id"
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET = "your_storage_bucket"
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID = "your_messaging_sender_id"
NEXT_PUBLIC_FIREBASE_APP_ID = "your_app_id"
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID = "your_measurement_id"

firebaseConfig 对象中列出的凭证替换每个环境。

接下来,在项目根文件夹内创建一个新的firebase 目录。

firebase 文件夹中,创建一个文件clientApp.ts

我们将在clientApp.ts 文件中配置Firebase实例,如下图所示。

首先从Firebase包中导入initializeApp

import {initializeApp} from "firebase/app";

调用initializeApp 函数并传入你在env.local 文件中列出的凭证。

initializeApp( {
   apiKey:process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
   authDomain:process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
   projectId:process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
   storageBucket:process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
   messagingSenderId:process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
   appId:process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
   measurementId:process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID
});

从Firebase导入getFirestore

import {getFirestore} from "firebase/firestore";

创建一个Firestore 实例。

const firestore = getFirestore();

导出firestore ,这样它就可以被我们在这个项目中以后创建的文件所访问。

export {firestore};

由于我们有环境变量,我们将不得不重新启动开发服务器。

ctrl + c 来关闭它,然后按npm run dev 来启动它。

从Firestore查询文件

为了从Firestore查询文档,我们将在pages/index.tsx

import type { NextPage } from 'next'
import Head from 'next/head'
import styles from '../styles/Home.module.css'
const Home: NextPage = () => {
return (
   <div className={styles.container}>
   <Head>
     <title>Todos app</title>
     <meta name="description" content="Next.js firebase todos app" />
     <link rel="icon" href="/favicon.ico" />
   </Head>
   <main className={styles.main}>
     <h1 className={styles.title}>
     Todos app
     </h1>
   </main>
   <footer className={styles.footer}>
     <a
     href="#"
     rel="noopener noreferrer"
     >
     Todos app
     </a>
   </footer>
   </div>
)
}
export default Home

以上只是一个骨架,我们将在这个todos应用程序上开始工作。

我们已经改变了pages/index.tsx ,这意味着现有的链接CSS代码不会在新添加的代码上工作。

编辑styles/Home.module.css 文件,如下所示。

.container {
  min-height: 100vh;
  padding: 0 0.5rem;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}
.main {
  padding: 1rem 0;
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
}
.footer {
  width: 100%;
  height: 100px;
  border-top: 1px solid #eaeaea;
  display: flex;
  justify-content: center;
  align-items: center;
}
.footer a {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-grow: 1;
}
.title a {
  color: #0070f3;
  text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
  text-decoration: underline;
}
.title {
  margin: 0;
  line-height: 1.15;
  font-size: 1.5rem;
}
.title,
.description {
  text-align: center;
}
.description {
  line-height: 1.5;
  font-size: 1.5rem;
}
.grid {
  display: flex;
  flex-wrap: wrap;
  width: 80%;
  margin-top: 1rem;
}
.card {
  margin: 1rem auto;
  padding: 0.5rem;
  text-align: left;
  color: inherit;
  text-decoration: none;
  border: 1px solid #eaeaea;
  border-radius: 10px;
  transition: color 0.15s ease, border-color 0.15s ease;
  width: 60%;
}
.card h2 {
  margin: 0 0 1rem 0;
  font-size: 1.5rem;
}
.card p {
  margin: 0;
  font-size: 1.25rem;
  line-height: 1.5;
}
.cardActions {
  width: 100%;
  display: flex;
  justify-content: space-between;
  margin-top: 7px;
}
.form {
  width: 50%;
  margin: 1rem auto;
  padding: 10px;
  box-shadow: 0 1px 3px 0 #d4d4d5, 0 0 0 1px #d4d4d5;
}
.formGroup {
  width: 100%;
  margin: 1rem 0px;
}
.formGroup label {
  display: block;
  margin-bottom: 0.5rem;
}
.formGroup input[type="text"] {
  width: 100%;
  padding: 10px;
}
.formGroup textarea {
  width: 100%;
  padding: 10px;
}
.error {
  color: red;
  text-align: center;
}
.success {
  color: green;
  text-align: center;
}
.success a {
  color: blue;
  text-decoration: underline;
}
@media (max-width: 600px) {
  .grid {
   width: 100%;
   flex-direction: column;
  }
}

请自由编辑这些样式,使之成为你喜欢的外观。

pages/index.tsx ,从clientApp.ts 文件中导入Firestore,并在todos Collection中创建一个指针。

然后使用useState 来承载我们的todos的状态,如下图所示。

import { firestore } from '../firebase/clientApp';

import {collection,QueryDocumentSnapshot,DocumentData,query,where,limit,getDocs} from "@firebase/firestore";

const todosCollection = collection(firestore,'todos');

import { useState } from 'react';
const [todos,setTodos] = useState<QueryDocumentSnapshot<DocumentData>[]>([]);
const [loading,setLoading] = useState<boolean>(true);

一个todo 对象将有一个QueryDocumentSnapshot<DocumentData> 的类型。我们将初始化加载到true ,以避免在它们没有完全加载时访问todos

接下来,创建一个函数来获取这些todos,并构建一个useEffect 钩子来调用getTodos 方法。

const getTodos = async () => {
   // construct a query to get up to 10 undone todos 
   const todosQuery = query(todosCollection,where('done','==',false),limit(10));
   // get the todos
   const querySnapshot = await getDocs(todosQuery);
   
   // map through todos adding them to an array
   const result: QueryDocumentSnapshot<DocumentData>[] = [];
   querySnapshot.forEach((snapshot) => {
   result.push(snapshot);
   });
   // set it to state
   setTodos(result);
};

useEffect( () => {
   // get the todos
   getTodos();
   // reset loading
   setTimeout( () => {
     setLoading(false);
   },2000)
},[]);

从上面来看,我们正在获取todos 对象,并在每两秒后重置它们。

我们需要在浏览器中显示这些todos 。让我们创建一个Next.js视图来显示获取的todos。

index.tsx title ,即<h1className={styles.title}> Todos app</h1> ,下面添加以下代码。

<div className={styles.grid}>
{
  loading ? (
   <div className={styles.card}>
    <h2>Loading</h2>
   </div>
  ): 
  todos.length === 0 ? (
   <div className={styles.card}>
    <h2>No undone todos</h2>
    <p>Consider adding a todo from <a href="/add-todo">here</a></p>
   </div>
  ) : (
   todos.map((todo) => {
    return (
     <div className={styles.card}>
      <h2>{todo.data.arguments['title']}</h2>
      <p>{todo.data.arguments['description']}</p>
      <div className={styles.cardActions}>
      <button type="button">Mark as done</button>
      <button type="button">Delete</button>
      </div>
     </div>
    )
   })
  )
}
</div> 

上面我们显示一个loading ,检查我们是否有todos。如果我们没有,我们将显示一条信息;否则,现有的todos将被映射和显示。

在这种情况下,由于我们在设置Firestore数据库时添加了一个todos,你现在应该能从主页上看到它。

querying-todos

添加一个文档到Firestore

为了向Firestore添加一个文档,我们需要创建一个表单来输入一个新的todotitle ,和description

pages 文件夹中,创建一个文件add-todo.tsx setup,并添加以下代码。

import type { NextPage } from 'next'
import Head from "next/head";
import { useState } from 'react';
import styles from '../styles/Home.module.css'
const AddTodo:NextPage = () => {
   const [title,setTitle] = useState<string>(""); // title
   const [description,setDescription] = useState<string>("");// description
   const [error,setError] = useState<string>("");// error
   const [message,setMessage] = useState<string>("");// message
   const handleSubmit = (e: { preventDefault: () => void; }) => {
     e.preventDefault(); // avoid default behaviour
     
     if(!title || !description){ // check for any null value
       return setError("All fields are required");
     }
   }
   return (
     <div className={styles.container}>
       <Head>
         <title>Add todo</title>
         <meta name="description" content="Next.js firebase todos app" />
         <link rel="icon" href="/favicon.ico" />
       </Head>
       <div className={styles.main}>
         <h1 className={styles.title}>
           Add todo
         </h1>
         <form onSubmit={handleSubmit} className={styles.form}>
           {
             error ? (
               <div className={styles.formGroup}>
                 <p className={styles.error}>{error}</p>
               </div>
             ) : null
           }
           {
             message ? (
               <div className={styles.formGroup}>
                 <p className={styles.success}>
                   {message}. Proceed to <a href="/">Home</a>
                 </p>
               </div>
             ) : null
           }
           <div className={styles.formGroup}>
             <label>Title</label>
             <input type="text" 
             placeholder="Todo title" 
             onChange={e => setTitle(e.target.value)} />
           </div>
           <div className={styles.formGroup}>
             <label>Description</label>
             <textarea 
             placeholder="Todo description"  
             onChange={e => setDescription(e.target.value)}
             />
           </div>
           <div className={styles.formGroup}>
             <button type="submit">Submit</button>
           </div>
         </form>
       </div>
     </div>
   )
}
export default AddTodo;

我们正在设置一个基本的表单,有titledescription 字段。我们还有一个handleSubmit 函数,当表单被提交时被调用。

现在,它只是检查null 的值。现在让我们来处理数据到我们的Collection

首先导入必要的东西。

import { doc } from '@firebase/firestore'; // for creating a pointer to our Document
import { setDoc } from 'firebase/firestore'; // for adding the Document to Collection
import { firestore } from '../firebase/clientApp'; // firestore instance

创建一个addTodo() 函数,向todos 集合添加一个新的Document

const addTodo = async () => {
   // get the current timestamp
   const timestamp: string = Date.now().toString();
   // create a pointer to our Document
   const _todo = doc(firestore, `todos/${timestamp}`);
   // structure the todo data
   const todoData = {
     title,
     description,
     done: false
   };
   try {
     //add the Document
     await setDoc(_todo, todoData);
     //show a success message
     setMessage("Todo added successfully");
     //reset fields
     setTitle("");
     setDescription("");
   } catch (error) {
     //show an error message
     setError("An error occurred while adding todo");
   }
};

在上面的代码示例中,我们正在获取一个时间戳作为Document id 。我们正在将数据保存到集合。

如果有一个错误,我们将抓住它。否则,我们将设置该信息。从你的浏览器,打开http://localhost:3000/add-todo

你的页面应该类似于。

add-todo-form

填写表单字段,submit 。当表格成功提交后,你会得到一个成功信息,并有一个指向主页的链接,如下图所示。

如果你得到一个错误,重温一下上面的步骤,看看你可能错过了什么。

successful-add-todo-form

在下一步,我们将致力于更新一个document

在Firestore中更新一个文档

在我们的方案中,更新一个文档将涉及设置一个todo 对象。

要做到这一点,请浏览pages/index.tsx 并导入updateDoc

接下来,创建一个updateTodo() 函数,如下图所示。

import {updateDoc} from "@firebase/firestore";

const updateTodo = async (documentId: string) => {   
   // create a pointer to the Document id
   const _todo = doc(firestore,`todos/${documentId}`);
   // update the doc by setting done to true
   await updateDoc(_todo,{
   "done":true
   });
   // retrieve todos
   getTodos();
}

在映射一个todo的同时,在Mark as done 按钮上添加一个onClick 函数,并按如下方式调用该函数。

<button type="button" onClick={() => updateTodo(todo.data().id)}>Mark as done</button>

对于任何获取的todo,点击Mark as done 按钮。那个todo 对象将消失,因为它将被更新为一个done

然后,未完成的todos 项目将根据getTodos() 方法中设置的查询来获取。

在下一步,我们将致力于删除一个todo。

在Firestore中删除一个文档

要删除一个文档,请浏览pages/index.tsx ,导入deleteDoc 函数,并创建一个方法来处理删除(deleteDoc)功能,如下图所示。

import {deleteDoc} from "@firebase/firestore";

const deleteTodo = async (documentId:string) => {
   // create a pointer to the Document id
   const _todo = doc(firestore,`todos/${documentId}`);
   // delete the doc
   await deleteDoc(_todo);
   // retrieve todos
   getTodos();
}

添加一个delete 按钮,使用onClick 事件链接到上述函数。

<button type="button" onClick={() => deleteTodo(todo.id)}>Delete</button>

当你在任何获取的todo 项目上点击delete 按钮时,该对象将从Collection 中被删除。

结语

在本教程中,我们已经学会了如何在Next.js应用程序中处理CRUD操作。

这个功能是通过Firebase数据库实现的,它允许我们处理基本的后端请求。