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。
你的表单应该类似于。

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

在下一步,我们将设置我们的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登陆页面。

这表明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,你现在应该能从主页上看到它。

添加一个文档到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;
我们正在设置一个基本的表单,有title 和description 字段。我们还有一个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 。
你的页面应该类似于。

填写表单字段,submit 。当表格成功提交后,你会得到一个成功信息,并有一个指向主页的链接,如下图所示。
如果你得到一个错误,重温一下上面的步骤,看看你可能错过了什么。

在下一步,我们将致力于更新一个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数据库实现的,它允许我们处理基本的后端请求。