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 的useList、useOne、useUpdate等数据 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_TOKEN与BASE_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_posts与categories,并演示了与 Ant Design(ThemedLayout、RefineThemes.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); // ... };三个参数的作用分别是:
| 参数 | 类型 | 说明 |
|---|---|---|
apiKey | string | Airtable API Token,用于创建官方客户端实例 |
baseId | string | 目标 Base 的 ID |
airtableClient | AirtableBase(可选) | 自定义 Airtable 客户端实例,传入后优先使用,可用于注入自定义认证或 Mock 客户端 |
返回值类型为Required<DataProvider>,即完整实现了DataProvider接口的 12 个方法:getList、getOne、getMany、create、createMany、update、updateMany、deleteOne、deleteMany、getApiUrl、custom。这里有两个值得一提的"例外":
getApiUrl与custom在源码中直接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 = 1、pageSize = 10、mode = "server";- 当
mode === "server"(默认)时,按(currentPage - 1) * pageSize到currentPage * pageSize切片; - 当
mode为"client"等非 server 值时,不做切片,返回全量数据,由前端侧(如useTable的 client 模式)自行分页;
- 当
total:返回的是all()拉取到的全部记录数(即未分页前满足筛选条件的记录总数),而不是当前页条数,这保证了分页组件的总页数计算是准确的。
需要留意的是,由于 Airtable 单次最多返回 100 条,getList实际可触及的数据规模受此限制;若表数据量超过 100 条,当前实现并不会自动翻页拉取全部数据。这一点在 test/getList/index.spec.ts 的用例中也能看到:测试用posts表仅返回 2 条记录,total为2。
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 }]数组。也就是说,你可以在useList或useTable中直接传入:
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 字符串。
编译流程
调用链如下:
- generateFilter.ts:入口函数。若传入了
filters,则以["AND", ...generateFilterFormula(filters)]为根节点调用compile()输出最终 Formula;由于 Refine 的CrudFilters顶层数组语义就是"各条件之间取 AND",因此这里显式包了一层AND。若未传入筛选条件,返回undefined,getList就不会携带filterByFormula; - generateFilterFormula.ts:遍历条件数组,遇到
operator === "or"时递归生成["OR", ...]子表达式,其余条件交给generateLogicalFilterFormula; - 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 | 双方先LOWER再FIND |
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") | 如in、between等不支持 |
其中简单比较操作符的映射关系定义在 isSimpleOperator.ts:
export const simpleOperatorMapping: Record<SimpleOperators, OperatorSymbol> = { eq: "=", ne: "!=", lt: "<", lte: "<=", gt: ">", gte: ">=", } as const;值得注意的细节是contains与containss的差异:contains系列会对字段与值同时做LOWER()转换实现大小写不敏感的模糊匹配,而containss系列保持大小写敏感。对应操作符判定逻辑见 isContainsOperator.ts,单元测试见 test/utils 下的generateFilterFormula.spec.ts、generateLogicalFilterFormula.spec.ts、isContainsOperator.spec.ts、isSimpleOperator.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 客户端完成的)。
已知限制与注意事项
基于文档与源码,使用该数据提供者时需要了解以下边界:
getApiUrl与custom未实现:调用会抛出"Not implemented on refine-airtable data provider."错误(见 dataProvider.ts 末尾),依赖这两者的功能(如custom自由请求)不可用;- 操作符支持有限:仅支持上表列出的操作符,
in、between、startswith、endswith等 Refine 内置操作符会直接抛错; - 分页在内存中进行:
getList每次向 Airtable 拉取 100 条后切片,数据量超过 100 条时无法访问到第 100 条之后的记录;getMany同样依赖拉全表后内存过滤; - 记录结构被扁平化:Airtable 记录的列值统一放在
fields中,数据提供者将其平铺为{ id, ...fields },关联表、附件等复杂字段类型会以其原始对象/数组形式暴露; - 版本配套:包版本
5.0.1需要@refinedev/core^5.0.0与 Node.js>= 20,接入前请确认项目版本; - 认证:仅支持 API Token,不支持 Personal Access Token。
可运行示例与延伸阅读
仓库中提供了完整可运行的示例工程 examples/data-provider-airtable,其中 App.tsx 展示了数据提供者与路由、资源、Ant Design 主题布局的完整集成方式(包含blog_posts、categories两个资源的 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),仅供参考