Refine v5 集成 Airtable 数据提供者(@refinedev/airtable)完整实战指南
2026/9/11 23:15:49 网站建设 项目流程

Refine v5 集成 Airtable 数据提供者(@refinedev/airtable)完整实战指南

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

Airtable 是一款"电子表格与数据库混合体"(spreadsheet-database hybrid)服务,它同时具备表格的易用性与数据库的结构化查询能力,非常适合快速搭建内容管理、轻量业务后台等场景。本文基于 Refine 官方文档 Airtable 集成指南 以及仓库中 packages/airtable 包的源码与测试,系统讲解如何在 Refine v5 应用中使用@refinedev/airtable数据提供者完成 CRUD、排序、筛选、分页与认证配置。读完本文,你将能够从零接入 Airtable,并理解该数据提供者底层如何把 Refine 的查询参数翻译成 Airtable Formula 与 REST 调用,从而在实际项目中游刃有余地排查问题、定制行为。

为什么需要 Airtable 数据提供者

Refine 通过数据提供者(Data Provider)与各类后端通信。数据提供者是一个实现了DataProvider接口的函数,负责把 Refine 的useListuseOneuseUpdate等数据 Hook 的参数(资源名resource、记录id、分页pagination、排序sorters、筛选filters)翻译成对目标 API 的真实请求,并把响应规范化为{ data, total }等 Refine 约定结构。

Airtable 的 REST API 与常规 REST 服务差异较大:其记录以id + fields的形式返回,查询依赖filterByFormula(Airtable Formula 语法)、排序依赖sort数组、单次读取条数上限为 100 条。@refinedev/airtable正是为了抹平这些差异而存在,让开发者可以像使用其他数据提供者一样,用统一的 Hook 语法操作 Airtable 表格。关于 Refine 数据获取机制的通用介绍,可参考官方指南 Data Fetching。

安装

在你的 Refine 项目中安装数据提供者包:

npm install @refinedev/airtable # 或 pnpm add @refinedev/airtable

从仓库中 packages/airtable/package.json 可以看到,该包当前版本为5.0.1,以@refinedev/core^5.0.0作为 peer dependency,内部依赖airtable(Airtable 官方 JavaScript 客户端)、@qualifyze/airtable-formulator(用于把筛选条件编译为 Airtable Formula)等库,并声明运行环境要求 Node.js>= 20。也就是说,该数据提供者专为 Refine v5 设计,与@refinedev/corev4 不兼容。

快速开始:接入你的第一个 Airtable 数据源

1. 获取凭证

使用该集成前,需要先准备两个值:

  • API_TOKEN:Airtable 账号的 API Token。需要注意,官方文档明确说明:该集成目前不支持 Airtable 的个人访问令牌(Personal Access Token),请使用传统 API Key 格式的 Token
  • BASE_ID:目标 Base(工作区/数据库实例)的 ID,可以在 Airtable 的 API 文档页面或 Base URL 中获取(形如appXXXXXXXXXXXXXX)。

2. 在Refine组件中挂载数据提供者

@refinedev/airtable默认导出一个工厂函数dataProvider,接受API_TOKENBASE_ID两个必填参数,返回一个完整的数据提供者对象:

import Refine from "@refinedev/core"; import dataProvider from "@refinedev/airtable"; const App = () => ( <Refine dataProvider={dataProvider("<API_TOKEN>", "<BASE_ID>")} > {/* 应用路由、资源定义等 */} </Refine> ); export default App;

挂载之后,资源名(resource)即对应 Airtable 中的表名(Table 名)。仓库中的完整可运行示例位于 examples/data-provider-airtable/src/App.tsx,该示例定义了两个资源blog_postscategories,并演示了与 Ant Design(ThemedLayoutRefineThemes.Blue)和 React Router 的组合使用方式,适合作为接入时的参照模板。

数据提供者工厂函数:签名与返回值

深入 packages/airtable/src/dataProvider.ts 源码,可以看到工厂函数的完整签名:

export const dataProvider = ( apiKey: string, baseId: string, airtableClient?: AirtableBase, ): Required<DataProvider> => { const base = airtableClient || new Airtable({ apiKey: apiKey }).base(baseId); // ... };

三个参数的作用分别是:

参数类型说明
apiKeystringAirtable API Token,用于创建官方客户端实例
baseIdstring目标 Base 的 ID
airtableClientAirtableBase(可选)自定义 Airtable 客户端实例,传入后优先使用,可用于注入自定义认证或 Mock 客户端

返回值类型为Required<DataProvider>,即完整实现了DataProvider接口的 12 个方法:getListgetOnegetManycreatecreateManyupdateupdateManydeleteOnedeleteManygetApiUrlcustom。这里有两个值得一提的"例外":

  • getApiUrlcustom在源码中直接throw Error("Not implemented on refine-airtable data provider."),即当前版本未实现。这意味着依赖custom方法做自由请求、或依赖getApiUrl读取 API 地址的用法在该数据提供者上不可用;
  • 所有读写方法返回的记录都遵循 Airtable 的数据模型,被规范化为{ id, ...fields }结构,即记录 ID 放在id字段,其余列值平铺为顶层字段(见下文的 CRUD 逐方法讲解)。

CRUD 方法的底层实现与调用约定

以下内容均以 dataProvider.ts 源码为准,并辅以 packages/airtable/test 下的测试用例佐证。

getList:列表查询、排序与分页

getList: async ({ resource, pagination, sorters, filters }) => { const { currentPage = 1, pageSize = 10, mode = "server" } = pagination ?? {}; const generatedSort = generateSort(sorters) || []; const queryFilters = generateFilter(filters); const { all } = base(resource).select({ pageSize: 100, sort: generatedSort, ...(queryFilters ? { filterByFormula: queryFilters } : {}), }); const data = await all(); const isServerPaginationEnabled = mode === "server"; return { data: data .slice( isServerPaginationEnabled ? (currentPage - 1) * pageSize : undefined, isServerPaginationEnabled ? currentPage * pageSize : undefined, ) .map((p) => ({ id: p.id, ...p.fields })), total: data.length, }; }

从源码可以确认以下行为:

  • 排序:通过sorters传入,由generateSort转换为 Airtable 的{ field, direction }数组(详见下文排序章节);
  • 筛选:通过filters传入,由generateFilter编译为filterByFormula(详见下文筛选章节);
  • 分页:每次向 Airtable 请求时固定使用pageSize: 100拉取(这是 Airtable API 单次返回的最大条数),随后在内存中执行切片。分页参数默认值为currentPage = 1pageSize = 10mode = "server"
    • mode === "server"(默认)时,按(currentPage - 1) * pageSizecurrentPage * pageSize切片;
    • mode"client"等非 server 值时,不做切片,返回全量数据,由前端侧(如useTable的 client 模式)自行分页;
  • total:返回的是all()拉取到的全部记录数(即未分页前满足筛选条件的记录总数),而不是当前页条数,这保证了分页组件的总页数计算是准确的。

需要留意的是,由于 Airtable 单次最多返回 100 条,getList实际可触及的数据规模受此限制;若表数据量超过 100 条,当前实现并不会自动翻页拉取全部数据。这一点在 test/getList/index.spec.ts 的用例中也能看到:测试用posts表仅返回 2 条记录,total2

getOne / getMany:单条与批量读取

getOne: async ({ resource, id }) => { const { fields } = await base(resource).find(id.toString()); return { data: { id, ...fields } }; }, getMany: async ({ resource, ids }) => { const { all } = base(resource).select({ pageSize: 100 }); const data = await all(); return { data: data.filter((p) => ids.includes(p.id)).map((p) => ({ id: p.id, ...p.fields })), }; },
  • getOne直接调用 Airtable 客户端的find(id)按记录 ID 精确读取,效率最高;
  • getMany由于 Airtable 没有原生的按 ID 批量读取接口,实现上是拉取整张表(每页 100 条)后在内存中按ids过滤。因此当表数据量很大且频繁调用getMany时,会带来额外的请求开销,这是该实现的取舍,值得在业务设计时留意。

create / createMany:新增记录

create: async ({ resource, variables }) => { const { id, fields } = await base(resource).create(variables); return { data: { id, ...fields } }; }, createMany: async ({ resource, variables }) => { const data = await base(resource).create(variables); return { data: data.map((p) => ({ id: p.id, ...p.fields })) }; },
  • create一次创建一条记录,variables中的键值对即 Airtable 表格的字段名与值;
  • createMany一次批量创建多条记录(variables为记录数组),返回值是包含新记录id与完整fields的数组;
  • Airtable 会自动为每条新记录分配rec开头的记录 ID,写入结果中的id字段即取自该 ID。

update / updateMany:更新记录

update: async ({ resource, id, variables }) => { const { fields } = await base(resource).update(id.toString(), variables); return { data: { id, ...fields } }; }, updateMany: async ({ resource, ids, variables }) => { const requestParams = ids.map((id) => ({ id: id.toString(), fields: { ...variables } })); const data = await base(resource).update(requestParams); return { data: data.map((p) => ({ id: p.id, ...p.fields })) }; },
  • update更新单条记录,注意id会被显式转为字符串后传给 Airtable;
  • updateMany会把同一个variables应用到所有目标 ID,构造出[{ id, fields }]形式的批量更新参数,一次调用完成多条更新。

deleteOne / deleteMany:删除记录

deleteOne: async ({ resource, id }) => { const { fields } = await base(resource).destroy(id.toString()); return { data: { id, ...fields } }; }, deleteMany: async ({ resource, ids }) => { const data = await base(resource).destroy(ids.map(String)); return { data: data.map((p) => ({ id: p.id, ...p.fields })) }; },

删除操作直接调用 Airtable 客户端的destroy方法,返回被删除记录的最后状态。deleteMany通过ids.map(String)统一转字符串后批量销毁。

排序:从 CrudSorting 到 Airtable sort 参数

排序逻辑位于 packages/airtable/src/utils/generateSort.ts:

export const generateSort = (sorters?: CrudSorting) => { return sorters?.map((item) => ({ field: item.field, direction: item.order, })); };

Refine 的CrudSorting结构({ field, order },其中order"asc""desc")被原样映射为 Airtableselect方法接受的sort: [{ field, direction }]数组。也就是说,你可以在useListuseTable中直接传入:

useTable({ sorters: { initial: [ { field: "title", order: "asc" }, { field: "created_at", order: "desc" }, ], }, });

多个排序字段会按数组顺序生效。对应测试见 packages/airtable/test/utils/generateSort.spec.ts,以及 test/getList/index.spec.ts 中对title降序排序返回结果的验证。

筛选:Refine 过滤器到 Airtable Formula 的编译管线

筛选是@refinedev/airtable最有技术含量的一部分。Refine 的filters需要被翻译成 Airtable 的filterByFormula字符串,这一管线由 packages/airtable/src/utils 目录下的多个模块协作完成,并最终借助@qualifyze/airtable-formulator把中间表示编译为 Formula 字符串。

编译流程

调用链如下:

  1. generateFilter.ts:入口函数。若传入了filters,则以["AND", ...generateFilterFormula(filters)]为根节点调用compile()输出最终 Formula;由于 Refine 的CrudFilters顶层数组语义就是"各条件之间取 AND",因此这里显式包了一层AND。若未传入筛选条件,返回undefinedgetList就不会携带filterByFormula
  2. generateFilterFormula.ts:遍历条件数组,遇到operator === "or"时递归生成["OR", ...]子表达式,其余条件交给generateLogicalFilterFormula
  3. generateLogicalFilterFormula.ts:将单个逻辑条件转换为 Airtable Formula 的数组中间表示(如["=", { field }, value])。

操作符支持矩阵

下表整理自 isSimpleOperator.ts 与 generateLogicalFilterFormula.ts:

Refine 操作符语义生成的 Airtable Formula说明
eq等于{field} = value简单比较,直接映射
ne不等于{field} != value简单比较
lt小于{field} < value简单比较
lte小于等于{field} <= value简单比较
gt大于{field} > value简单比较
gte大于等于{field} >= value简单比较
containss包含(区分大小写)FIND(value, {field}) != 0借助FIND定位子串,结果非 0 即包含
ncontainss不包含(区分大小写)FIND(value, {field}) = 0同上取反
contains包含(不区分大小写)FIND(LOWER(value), LOWER({field})) != 0双方先LOWERFIND
ncontains不包含(不区分大小写)FIND(LOWER(value), LOWER({field})) = 0同上取反
null为空{field} = BLANK()匹配空值
nnull非空{field} != BLANK()匹配非空值
or逻辑或OR(...)generateFilterFormula中递归展开
其他操作符抛出Error("Operator ${operator} is not supported for the Airtable data provider")inbetween等不支持

其中简单比较操作符的映射关系定义在 isSimpleOperator.ts:

export const simpleOperatorMapping: Record<SimpleOperators, OperatorSymbol> = { eq: "=", ne: "!=", lt: "<", lte: "<=", gt: ">", gte: ">=", } as const;

值得注意的细节是containscontainss的差异:contains系列会对字段与值同时做LOWER()转换实现大小写不敏感的模糊匹配,而containss系列保持大小写敏感。对应操作符判定逻辑见 isContainsOperator.ts,单元测试见 test/utils 下的generateFilterFormula.spec.tsgenerateLogicalFilterFormula.spec.tsisContainsOperator.spec.tsisSimpleOperator.spec.ts

组合条件的实际效果

由于顶层数组隐式取 AND,or显式取 OR,你可以组合出常见的业务查询。例如下面的筛选条件:

filters: [ { field: "status", operator: "eq", value: "published" }, { operator: "or", value: [ { field: "author", operator: "contains", value: "john" }, { field: "author", operator: "contains", value: "jane" }, ], }, ]

会被编译为类似AND({status}="published", OR(FIND(LOWER("john"), LOWER({author})) != 0, FIND(LOWER("jane"), LOWER({author})) != 0))的 Formula 交给 Airtable 执行。

认证机制与第三方客户端注入

@refinedev/airtable底层使用 Airtable 官方 JavaScript 客户端(airtable.js)发起请求,认证方式为:

new Airtable({ apiKey: apiKey }).base(baseId)

即通过API Token(传统 API Key)完成认证。官方文档特别提示:Airtable 的 Personal Access Token(个人访问令牌)目前不被支持,请勿混用。

同时,工厂函数暴露了可选的第三个参数airtableClient,允许调用方注入一个自定义的AirtableBase实例:

import Airtable from "airtable"; const customBase = new Airtable({ apiKey: API_TOKEN, endpointUrl: "https://..." }).base(BASE_ID); dataProvider(API_TOKEN, BASE_ID, customBase)

传入后dataProvider会优先使用该实例,这在接入代理、Mock 服务或自定义网络配置的场景下非常实用(仓库内的测试也正是通过 nock 拦截请求、配合真实 airtable 客户端完成的)。

已知限制与注意事项

基于文档与源码,使用该数据提供者时需要了解以下边界:

  1. getApiUrlcustom未实现:调用会抛出"Not implemented on refine-airtable data provider."错误(见 dataProvider.ts 末尾),依赖这两者的功能(如custom自由请求)不可用;
  2. 操作符支持有限:仅支持上表列出的操作符,inbetweenstartswithendswith等 Refine 内置操作符会直接抛错;
  3. 分页在内存中进行getList每次向 Airtable 拉取 100 条后切片,数据量超过 100 条时无法访问到第 100 条之后的记录;getMany同样依赖拉全表后内存过滤;
  4. 记录结构被扁平化:Airtable 记录的列值统一放在fields中,数据提供者将其平铺为{ id, ...fields },关联表、附件等复杂字段类型会以其原始对象/数组形式暴露;
  5. 版本配套:包版本5.0.1需要@refinedev/core^5.0.0与 Node.js>= 20,接入前请确认项目版本;
  6. 认证:仅支持 API Token,不支持 Personal Access Token。

可运行示例与延伸阅读

仓库中提供了完整可运行的示例工程 examples/data-provider-airtable,其中 App.tsx 展示了数据提供者与路由、资源、Ant Design 主题布局的完整集成方式(包含blog_postscategories两个资源的 list/create/edit/show 页面组织),适合作为脚手架参考。

若想进一步理解 Refine 的数据获取机制(DataProvider接口约定、useList/useOne/useUpdate等数据 Hook、基于 TanStack Query 的缓存与失效策略、多数据提供者混用等),请阅读官方指南 Data Fetching。本文涉及的源码与测试均可直接在仓库的 packages/airtable 目录中继续研读。

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询