将Airtable与Next.js集成
在这篇文章中,我们将建立一个杂货清单应用程序,在那里我们可以在去商店之前添加所有的杂货。我们将使用[Next.js],一个[React]框架来构建应用程序的前端。我们将使用[tailwindcss]来设计我们的应用程序。我们将使用的数据库解决方案是[Airtable]。
在过去的几年里,浏览器不断发展,变得更加强大。即使没有一个网络服务器,它们也能以完整的功能工作。一旦在构建灵活的网络应用程序时,可以利用浏览器的能力。
在构建闪电般的网络应用时,JAMstack是现代的方法。数据库技术变得更加方便用户使用。在本教程中,我们将学习如何将Airtable API与一个简单的Web应用集成。
我们要建立什么?
最终的应用会是这样的。

在深入学习本教程之前,读者应该有React的中级知识。如果读者能自如地使用React Hooks和Context API,会有帮助。
JAM栈和Airtable
JAMstack结合了[JavaScript]、[API]和[Markup],用于开发快速和可扩展的网络应用。JAMstack网站采用了第三方API来获取数据。
在与Airtable通信时,你将使用无服务器函数。Airtable是一个电子表格和数据库的混合体,你可以使用其优秀的API轻松地集成到你的应用程序中。
Airtable API有精彩的文档。示例代码包含了你所有的API密钥和基础名称。要在你的应用程序中使用它们,你可以复制和粘贴代码。
开始使用
打开你最喜欢的代码编辑器,运行命令npx create-next-app -e with-tailwindcss 。它生成一个安装了tailwindcss的Next.js应用程序。
使用该命令安装Airtable。npm install airtable.为了验证一切是否正常,尝试运行命令。npm run dev.如果你看到它正常渲染,你就可以开始了。
Next.js支持服务器端渲染,无需使用任何其他框架。它包括一个路由器,允许你将/pages 目录中的任何文件作为一个新的路由来访问。在/pages/api 目录中,你可以使用无服务器函数创建API端点。
Airtable的JavaScript设置
前往airtable.com,注册一个免费账户。登录成功后,从头开始创建一个新的base 。base 就是airtable所说的数据库。
你会有一个带有一些主字段的启动表被创建。你可以个性化整个基地;从基地的标题和表的名字开始。
你可以看到,用户界面是友好的,你可以用与电子表格相同的方式工作。通过右键点击表中的一个字段,你可以对其进行自定义。
你需要一个item ,用于杂货店名称,以及一个brought 的复选框字段。导航到[Airtable API],选择你想整合的基础。
让我们把Airtable连接到我们的应用程序,但首先,让我们定义一些你在代码中需要的变量。
API_KEY:Airtable的API密钥。BASE_ID: 你想整合的基地的ID。你可以在文档页面上找到它。TABLE_NAME:该基础中的表的名称(你可以为多个表使用一个基础)。
在你的应用程序的环境变量中添加所有这些秘密(.env 文件)。如果你使用版本控制,请确保你忽略它们。
/.env
AIRTABLE_API_KEY=
AIRTABLE_BASE_ID=
AIRTABLE_TABLE_NAME=
连接到Airtable
创建一个新的Airtable.js 文件。我更喜欢在root 目录下的一个新的utils 文件夹中创建它;你可以在你想的任何地方创建它。
添加以下代码。
const Airtable = require("airtable");
// Authenticate
Airtable.configure({
apiKey: process.env.AIRTABLE_API_KEY,
});
// Initialize a base
const base = Airtable.base(process.env.AIRTABLE_BASE_ID);
// Reference a table
const table = base(process.env.AIRTABLE_TABLE_NAME);
export { table };
上面的代码建立了一个与Airtable基地的连接。它首先使用你的API_KEY 来认证你。然后,你所要做的就是初始化一个基地并引用你需要的表。
使用Next.js建立一个API
Next.js允许你使用API路由创建你自己的API。Next.js将/pages/api 文件夹内的任何文件映射到/api/* ,一个API端点,而不是一个路由。
你可以使用无服务器函数来处理点击该端点的任何请求。它具有对请求和响应对象的读写权限。你可以使用一个条件块,用一个函数来处理不同类型的请求。
但在这个项目中,我们要在处理每个请求时创建一个单独的文件。
现在我们要创建一个API来对Airtable数据库进行[CRUD操作]。
获取表的记录
Airtable服务器在一个页面上每次最多返回100条记录。如果你知道你的表不超过100 ,你可以使用firstPage 方法。如果你有(或预期)超过100条记录,你应该使用eachPage 方法分页浏览。
在/pages/api 文件夹中创建一个新文件items.js 文件。
添加以下代码。
import { table } from "../../utils/Airtable";
export default async (_req, res) => {
try {
const records = await table.select({}).firstPage();
res.status(200).json(records);
} catch (error) {
console.error(err);
res.status(500).json({ msg: "Something went wrong! 😕" });
}
};
上面的代码检索所有在第一页的记录(100条记录)。你将得到一个看起来像这样的记录,其中有所有的额外数据。另外需要注意的是,如果false ,我们在brought 字段中什么都没有得到。所以我们必须手动添加。
[
{
"_table": {
"_base": { "_airtable": {}, "_id": "AIRTABLE_BASE_ID" },
"id": null,
"name": "AIRTABLE_BASE_NAME"
},
"id": "RECORD_ID",
"_rawJson": {
"id": "RECORD_ID",
"fields": {
"item": "item name",
"brought": false
},
"createdTime": "2021-08-08T13:28:29.000Z"
},
"fields": {
"item": "item name",
"brought": false
}
}
]
你应该映射所有的记录,只得到需要的信息。在/utils/Airtable.js 下声明这个函数,并在你需要时导入它。
// /utils/Airtable.js
// ...
// To get minified records array
const minifyItems = (records) =>
records.map((record) => getMinifiedItem(record));
// to make record meaningful.
const getMinifiedItem = (record) => {
if (!record.fields.brought) {
record.fields.brought = false;
}
return {
id: record.id,
fields: record.fields,
};
};
export { table, minifyItems, getMinifiedItem };
将minifyItems 导入到items.js 中,用于显示最小化的项目。
import { table, minifyItems } from "../../utils/Airtable";
export default async (_req, res) => {
try {
const records = await table.select({}).firstPage();
const minfiedItems = minifyItems(records);
res.status(200).json(minfiedItems);
} catch (error) {
console.error(err);
res.status(500).json({ msg: "Something went wrong! 😕" });
}
};
创建一个新的记录
要创建新记录,你可以使用create 方法。它需要一个最多有10个记录对象的数组。每个记录对象都应该有fields key与内容。如果调用成功,它返回一个创建的记录对象数组。
在/pages/api 文件夹中创建一个新文件,createItem.js ,并添加以下代码。
import { table, getMinifiedItem } from "../../utils/Airtable";
export default async (req, res) => {
const { item } = req.body;
try {
const newRecords = await table.create([{ fields: { item } }]);
res.status(200).json(getMinifiedItem(newRecords[0]));
} catch (error) {
console.log(error);
res.status(500).json({ msg: "Something went wrong! 😕" });
}
};
你可以使用Postman或类似的东西来发送一个请求并测试端点。

更新一条记录
要更新记录,你可以使用update 或replace 方法。如果你想更新一个记录的单个字段,请使用update 方法,如果你要用一个新的记录替换它,请使用replace 方法。
update 方法与create 方法非常相似。它接收一个由ids和fields ,直至10 记录组成的数组,并返回更新的记录数组。
在/pages/api 文件夹中创建一个新文件updateItem.js ,并添加以下代码。
import { table, getMinifiedItem } from "../../utils/Airtable";
export default async (req, res) => {
const { id, fields } = req.body;
try {
const updatedRecords = await table.update([{ id, fields }]);
res.status(200).json(getMinifiedItem(updatedRecords[0]));
} catch (error) {
console.log(error);
res.status(500).json({ msg: "Something went wrong! 😕" });
}
};
在这里,你正在检索对应于id的记录,并用新的值更新字段。你可以通过使用Postman向API发送一个请求来测试这个端点。

删除一条记录
你可以使用destroy 方法删除一条记录。它需要一个你想删除的记录的ids数组。你也可以把第一个参数设置为一个记录ID,以删除一条记录。它返回被删除的记录。
在/pages/api 文件夹中创建一个新文件deleteItem.js ,并添加以下代码。
import { table } from "../../utils/Airtable";
export default async (req, res) => {
const { id } = req.body;
try {
const deletedRecords = await table.destroy([id]);
res.status(200).json(deletedRecords);
} catch (error) {
console.log(error);
res.status(500).json({ msg: "Something went wrong! 😕" });
}
};

创建前端
现在我们已经为所有的CRUD操作准备好了我们的API。让我们创建界面,在我们的Next.js应用程序中显示这些数据。前往你的/pages 目录中的index.js 文件,删除其中的所有代码。
添加以下代码。
import Head from "next/head";
export default function Home() {
return (
<div className="container mx-auto my-6 max-w-xl">
<Head>
<title>@Grocery List</title>
</Head>
<main>
<p className="text-2xl font-bold text-grey-800 py-2">🛒 Grocery List</p>
</main>
</div>
);
}
上面的代码不过是一个React功能组件。Next.js有一个内置的Head 组件,它将充当你的HTML页面的head 标签。现在,如果你开始运行服务器,你会看到你的应用程序。Tailwindcss是一个基于类的框架。你必须根据你想要的样式来添加类,就像在bootstrap中一样。
Next.js有一个内置的函数getServerSideProps ,在页面内启用服务器端渲染时使用。Next.js每次在渲染页面之前都会执行该函数中的代码。
我们将从Airtable中获取所有的项目,然后将它们传递给Home 组件作为 props来使用这些数据。在你的index.js 页面上添加以下函数。
import { table, minifyItems } from "../utils/Airtable";
export default function Home({ initialItems }) {
console.log(initialItems);
// ...
}
export async function getServerSideProps(context) {
try {
const items = await table.select({}).firstPage();
return {
props: {
initialItems: minifyItems(items),
},
};
} catch (error) {
console.log(error);
return {
props: {
err: "Something went wrong 😕",
},
};
}
}
上述函数从Airtable中获取所有记录,并将它们传递给initialItems 。现在,只需通过控制台记录数据来看看它是否工作。
React上下文API来整合Airtable数据
Context提供了一种通过组件树传递数据的方式,在每一级都手动向下传递道具。
在大规模的项目中,我们必须在许多组件中使用这些数据。因此,使用React context而不是传递props是一个更好的主意。
创建一个新的context 文件夹并添加一个新的items.js 文件。在这里,我们将对Airtable数据进行所有操作,并将数据传递给前端。
import { createContext, useState } from "react";
const ItemsContext = createContext();
const ItemsProvider = ({ children }) => {
const [items, setItems] = useState();
// for creating an item
const addItem = async (item) => {
try {
// we will send a POST request with the data required to create an item
const res = await fetch("/api/createItem", {
method: "POST",
body: JSON.stringify({ item }),
headers: { "Content-Type": "application/json" },
});
const newItem = await res.json();
// then we will update the 'items' adding the newly added item to it
setItems((prevItems) => [newItem, ...prevItems]);
} catch (error) {
console.error(error);
}
};
// for updating an existing item
const updateItem = async (updatedItem) => {
try {
// we will send a PUT request with the updated information
const res = await fetch("/api/updateItem", {
method: "PUT",
body: JSON.stringify(updatedItem),
headers: { "Content-Type": "application/json" },
});
await res.json();
// then we will update the 'items' by replacing the fields of existing item.
setItems((prevItems) => {
const existingItems = [...prevItems];
const existingItem = existingItems.find(
(item) => item.id === updatedItem.id
);
existingItem.fields = updatedItem.fields;
return existingItems;
});
} catch (error) {
console.error(error);
}
};
// for deleting an item
const deleteItem = async (id) => {
try {
// we will send a DELETE request to the API with the id of item we want to delete
const res = await fetch("/api/deleteItem", {
method: "Delete",
body: JSON.stringify({ id }),
headers: { "Content-Type": "application/json" },
});
await res.json();
// them we will update the 'items' by deleting the item with specified id
setItems((prevItems) => prevItems.filter((item) => item.id !== id));
} catch (error) {
console.error(error);
}
};
return (
<ItemsContext.Provider
value={{
items,
setItems,
updateItem,
deleteItem,
addItem,
}}
>
{children}
</ItemsContext.Provider>
);
};
export { ItemsContext, ItemsProvider };
跟随代码的注释。每次操作时,我们都要发送一个HTTP 请求,并更新本地items 列表,以避免重新获取数据。
现在,打开/pages/_app.js 文件并导入ItemsProvider 。你只能在组件是ItemProvider 的孩子时使用ItemContext 。
// /pages/_app.js
import "tailwindcss/tailwind.css";
import { ItemsProvider } from "../context/items";
function MyApp({ Component, pageProps }) {
return (
<ItemsProvider>
<Component {...pageProps} />
</ItemsProvider>
);
}
export default MyApp;
你可以在index.js 文件中设置items 。将你之前获取的initialItems 传给setItems 函数。它会更新ItemsContext ,你可以在任何组件中使用这些项目。
// /pages/index.js
import React, { useContext, useEffect } from "react";
// ...
import { ItemsContext } from "../context/items";
export default function Home({ initialItems }) {
const { items, setItems } = useContext(ItemsContext);
useEffect(() => {
setItems(initialItems);
}, [initialItems, setItems]);
// ...
}
// ...
显示项目
在components 文件夹中创建一个新的Item.js 。它取一个道具item ,并在网页上显示它。
当添加更新和删除功能时,你可以从ItemsContext 中导入所需的功能。在这里,你可以通过勾选复选框将项目更新为brought 。如果你不需要这个项目,你可以通过点击删除按钮来删除它。
// /components/Item.js
import React, { useContext } from "react";
import { ItemsContext } from "../context/items";
const Item = ({ item }) => {
// for updating and deleting item
const { updateItem, deleteItem } = useContext(ItemsContext);
// Update the record when the checkbox is checked
const handleCompleted = () => {
const updatedFields = {
...item.fields,
brought: !item.fields.brought,
};
const updatedItem = { id: item.id, fields: updatedFields };
updateItem(updatedItem);
};
return (
<li className="bg-white flex items-center shadow-lg rounded-lg my-2 py-2 px-4">
<input
type="checkbox"
name="brought"
id="brought"
checked={item.fields.brought}
className="mr-2 form-chechbox h-5 w-5"
onChange={handleCompleted}
/>
<p
className={`flex-1 text-gray-800 ${
item.fields.brought ? "line-through" : ""
}`}
>
{item.fields.item}
</p>
{/* delete item when the delete button is clicked*/}
<button
type="button"
className="text-sm bg-red-500 hover:bg-red-700 text-white py-1 px-2 rounded"
onClick={() => deleteItem(item.id)}
>
Delete
</button>
</li>
);
};
export default Item;
现在,你可以导入Item 组件并通过项目数组映射,将每个项目传递给这个组件。
// /pages/index.js
// ...
import Item from "../components/Item";
export default function Home({ initialItems }) {
// ...
return (
// ...
<main>
<ul>
{items && items.map((item) => <Item key={item.id} item={item} />)}
</ul>
</main>
);
// ...
}
// ...
添加项目
你可以创建一个简单的表单来向Airtable添加项目。创建一个新的文件ItemForm.js 。当你提交表单时,HTML中的form 元素与React中的不同。
// /components/ItemForm.js
import React, { useState, useContext } from "react";
import { ItemsContext } from "../context/items";
const ItemForm = () => {
const [item, setItem] = useState("");
const { addItem } = useContext(ItemsContext);
const handleSubmit = (e) => {
e.preventDefault();
addItem(item);
setItem("");
};
return (
<form className="form my-6" onSubmit={handleSubmit}>
<div className="flex justify-between w-full">
<input
type="text"
name="item"
value={item}
onChange={(e) => setItem(e.target.value)}
placeholder="ex. Eggs"
className="flex-1 border border-gray-200 p-2 mr-2 rounded-lg appearance-none focus:outline-none focus:border-gray-500"
/>
<button
type="submit"
className="rounded bg-blue-500 hover:bg-blue-600 text-white py-2 px-4"
>
+ Add Item
</button>
</div>
</form>
);
};
export default ItemForm;
你应该在index.js 中渲染ItemForm 组件。
// /pages/index.js
// ...
import ItemForm from "../components/ItemForm";
export default function Home({ initialItems }) {
// ...
return (
// ...
<main>
<ItemForm />
<ul>{/* ... */}</ul>
</main>
);
// ...
}
// ...
部署
让我们使用[Vercel]来部署这个应用程序。我推荐使用Vercel进行部署。它不需要任何配置,对你的个人项目来说是免费的。
它支持Next.js的所有功能,性能最好。进入[vercel.com]并创建一个账户。把你的项目推送到Git仓库。然后把它导入到vercel进行部署。
接下来的步骤
现在你已经有了一个完整的JAMstack应用程序,试着扩展其功能。
- 在构建一个完美的API时,使用API的[最佳实践]。
- 尝试添加一个过滤选项。你应该分别显示所有带来的和待处理的。
- 尝试添加[认证]。如果你添加了一个项目,每个人都可以看到这个项目。你可以使用像Auth0这样的第三方服务来认证一个用户。