如何将Airtable与Next.js结合起来

423 阅读11分钟

将Airtable与Next.js集成

在这篇文章中,我们将建立一个杂货清单应用程序,在那里我们可以在去商店之前添加所有的杂货。我们将使用[Next.js],一个[React]框架来构建应用程序的前端。我们将使用[tailwindcss]来设计我们的应用程序。我们将使用的数据库解决方案是[Airtable]。

在过去的几年里,浏览器不断发展,变得更加强大。即使没有一个网络服务器,它们也能以完整的功能工作。一旦在构建灵活的网络应用程序时,可以利用浏览器的能力。

在构建闪电般的网络应用时,JAMstack是现代的方法。数据库技术变得更加方便用户使用。在本教程中,我们将学习如何将Airtable API与一个简单的Web应用集成。

我们要建立什么?

最终的应用会是这样的。

final-grocery-list

在深入学习本教程之前,读者应该有React的中级知识。如果读者能自如地使用React HooksContext 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,注册一个免费账户。登录成功后,从头开始创建一个新的basebase 就是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或类似的东西来发送一个请求并测试端点。

Creating a new item in airtable by sending raw data to createItem endpoint

更新一条记录

要更新记录,你可以使用updatereplace 方法。如果你想更新一个记录的单个字段,请使用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发送一个请求来测试这个端点。

Updating an item in airtable by sending data to updateItem endpoint

删除一条记录

你可以使用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! 😕" });
  }
};

Deleting an existing item in airtable by sending a delete request to deleteItem endpoint

创建前端

现在我们已经为所有的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这样的第三方服务来认证一个用户。