使用 Refine Supabase Data Provider 构建实时 CRUD 后台:从示例到源码级实践指南
2026/9/12 2:20:56 网站建设 项目流程

使用 Refine Supabase Data Provider 构建实时 CRUD 后台:从示例到源码级实践指南

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

本文基于 Refine 仓库中的 Supabase 官方示例(示例文档、示例源码),完整讲解如何在 Refine v5 中接入 Supabase:包括 Data Provider 数据读写、Live Provider 实时订阅、Auth Provider 认证集成,以及 Storage 文件上传等典型后台功能。读完本文,你将掌握 Supabase 与 Refine 的完整集成方案,并能理解过滤器、排序、实时事件在底层的映射原理,直接复用到自己的项目中。

示例概览:一个开箱即用的 Supabase 后台

该示例覆盖了 Refine 与 Supabase 集成的三大核心能力(对应示例 frontmatter 中声明的example-tags: [data-provider, live-provider, auth-provider]):

  • Data Provider:通过@refinedev/supabase提供的dataProvider完成对 Supabase 数据库(PostgreSQL)的增删改查;
  • Live Provider:通过liveProvider让界面实时响应数据库变更;
  • Auth Provider:对接 Supabase Auth(邮箱密码登录 + Google OAuth),并配合Authenticated组件保护路由。

示例业务模型为blog_postscategories两张表(类型定义见 src/interfaces/index.d.ts),并实现了列表、创建、编辑、详情四个页面,同时演示了 MD 编辑器(@uiw/react-md-editor)、图片上传(Supabase Storage)与关联表展示等常见后台场景。

如何在本地运行该示例

在仓库根目录下,可通过 Refine CLI 一键创建独立示例项目:

npm create refine-app@latest -- --example>import { createClient } from "@refinedev/supabase"; const SUPABASE_URL = "https://iwdfzvfqbtokqetmbmbp.supabase.co"; const SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYzMDU2NzAxMCwiZXhwIjoxOTQ2MTQzMDEwfQ._gr6kXGkQBi9BM9dx5vKaNKYj_DJN1xlkarprGpM_fU"; export const supabaseClient = createClient(SUPABASE_URL, SUPABASE_KEY, { db: { schema: "public", }, auth: { persistSession: true, }, });

这里有两个值得注意的配置项:

  • db.schema:指定默认数据库 schema,示例使用public,可保证表与 RLS 策略在预期 schema 下生效;
  • auth.persistSession:启用会话持久化,刷新页面后登录状态不丢失。

在 packages/supabase/src/index.ts 中可以看到,createClient直接来自@supabase/supabase-js并原样导出,同时整个包的导出面包含typesutilsdataProviderliveProvider四个模块——这意味着你既可以使用 Refine 提供的封装客户端,也可以沿用自己已有的 supabase-js 客户端实例(它同样是dataProvider(supabaseClient)/liveProvider(supabaseClient)的参数)。

第二步:在 Refine 中装配 Data、Live 与 Auth Provider

在 src/App.tsx 中,三者被一次性注入<Refine>

<Refine dataProvider={dataProvider(supabaseClient)} liveProvider={liveProvider(supabaseClient)} routerProvider={routerProvider} authProvider={authProvider} resources={[ { name: "blog_posts", list: "/blog-posts", create: "/blog-posts/create", edit: "/blog-posts/edit/:id", show: "/blog-posts/show/:id", meta: { canDelete: true, }, }, ]} options={{ liveMode: "off", syncWithLocation: true, warnWhenUnsavedChanges: true, }} >

几点关键设计:

  • dataProvider(supabaseClient):将 supabase-js 客户端包装为 Refine 标准 Data Provider,之后useTableuseFormuseShow等 Hook 的所有 CRUD 调用都会翻译成对 Supabase REST/PostgREST 的请求;
  • liveProvider(supabaseClient):基于 Supabase Realtime 的频道订阅实现;结合liveMode的三种取值(auto/manual/off)控制页面是否自动更新。
  • options.liveMode: "off":这是示例特意给出的重要约束——注释中明确说明(见 src/App.tsx):当前@supabase/supabase-jsv2 客户端与@refinedev/supabase尚不支持同时建立多个实时订阅,因此开启全局liveMode会导致非预期行为;正确的做法是按页面手动设置liveMode: "auto"liveMode: "manual"(下文编辑页正是这样做的);
  • warnWhenUnsavedChanges:配合路由层的UnsavedChangesNotifier在离开页面时提示未保存的表单。

Auth Provider:登录、注册与密码重置

示例中的authProvider全部基于supabaseClient.auth实现(src/App.tsx),典型的几个方法:

// 登录:支持 OAuth 与邮箱密码两种方式 login: async ({ email, password, providerName }) => { if (providerName) { const { data, error } = await supabaseClient.auth.signInWithOAuth({ provider: providerName, }); // ... } const { data, error } = await supabaseClient.auth.signInWithPassword({ email, password, }); // ... },
  • registerauth.signUp
  • forgotPasswordauth.resetPasswordForEmail(并携带redirectTo指向/update-password页面);
  • updatePasswordauth.updateUser
  • logoutauth.signOut
  • check→ 读取auth.getSession(),无会话则返回{ authenticated: false, redirectTo: "/login" }
  • getIdentity→ 返回用户信息,getPermissions→ 返回用户role

值得一提的错误处理:onError中对PGRST301401错误返回{ logout: true },即当会话过期/令牌失效时自动登出,配合 Supabase 的 RLS 策略形成完整的权限闭环。

路由层面使用<Authenticated>包裹受保护区域,未登录跳转/login;登录页AuthPage传入 Google OAuth 按钮并预设了演示账号(info@refine.dev/refine-supabase),见 src/App.tsx。

第三步:列表页——查询、排序、关联与过滤

列表页(src/pages/posts/list.tsx)演示了 Refine + Supabase 最常用的查询能力:

const { tableProps, sorters, filters } = useTable<IPost>({ meta: { select: "*, categories(title)", // PostgREST 嵌入关联表 }, sorters: { initial: [{ field: "id", order: "asc" }], }, });
  • 关联查询meta.select直接透传给 PostgREST 的select参数,*, categories(title)会一并返回每条帖子关联的分类标题,随后Table.Column dataIndex={["categories", "title"]}即可直接渲染;
  • 服务端排序getDefaultSortOrder("id", sorters)与列上的sorter组合,实现点击表头即走后端排序;
  • 服务端过滤FilterDropdown+getDefaultFilter("categoryId", filters, "in")实现多选分类过滤,useSelectpagination: { mode: "server" }让下拉选项分页加载;
  • server 端分页:Refine 的 Data Provider 会把页码与页大小转换为 PostgREST 的range参数。

第四步:创建/编辑页——表单、Markdown 与 Storage 上传

创建页(src/pages/posts/create.tsx)展示了完整的表单与文件上传流程。核心是Upload.DraggercustomRequest

customRequest={async ({ file, onError, onSuccess }) => { const rcFile = file as RcFile; await supabaseClient.storage .from("refine") .upload(`public/${rcFile.name}`, file, { cacheControl: "3600", upsert: true, }); const { data } = await supabaseClient.storage .from("refine") .getPublicUrl(`public/${rcFile.name}`); onSuccess?.({ url: data?.publicUrl }, new XMLHttpRequest()); }}

关键点:

  • 上传到名为refine的 bucket 的public/前缀下(需在 Supabase 控制台开启该 bucket 的 Public 访问并配置存储 RLS 策略);
  • cacheControl: "3600"设置 CDN 缓存时长,upsert: true允许同名文件覆盖;
  • 上传成功后通过getPublicUrl取回公开 URL,作为onSuccess的结果写回表单;
  • 表单字段images使用normalizeFile(src/utility/normalize.ts)将 antd Upload 的fileList归一化为{ uid, name, url, type, size, percent, status }结构后入库,对应的IPost.images类型为IFile[]

编辑页的实时体验:liveMode 手动模式

编辑页(src/pages/posts/edit.tsx)演示了liveMode: "manual"的正确用法——不强制刷新表单,而是提醒用户手动刷新:

const { formProps, saveButtonProps, query: queryResult } = useForm<IPost>({ liveMode: "manual", onLiveEvent: () => { setIsDeprecated(true); // 弹出"数据已变更"警告条 }, }); const handleRefresh = () => { queryResult?.refetch(); setIsDeprecated(false); };

当其他客户端修改了该记录,Supabase Realtime 推送事件后,页面顶部出现“This post is changed. Reload to see it's latest version.”警告条,并提供RefreshButton手动拉取最新数据,从而避免在用户编辑中途强行覆盖其输入。详情页(src/pages/posts/show.tsx)采用同样的策略,并配合useOne按需加载分类标题、MarkdownField渲染 Markdown、ImageField展示上传的图片。

第五步:源码级原理——过滤器如何映射到 PostgREST

@refinedev/supabase的 Data Provider 之所以能透明地把 Refine 的CrudFilter变成数据库查询,核心在于两个工具函数。

generateFilter.ts 将 Refine 的条件操作符翻译为 supabase-js 查询链,例如:

Refine 操作符supabase-js 查询语义
eq/nequery.eq / query.neq等于 / 不等于
in/inaquery.in / query.contains属于数组 / 数组包含(cs
ninaquery.not(field, "cs", "{...}")数组不包含
gt/gte/lt/ltequery.gt / gte / lt / lte比较运算
betweenquery.gte(...).lte(...)区间(长度必须为 2,否则抛出明确错误)
contains/containssquery.ilike("%v%") / like("%v%")模糊匹配(大小写不敏感/敏感)
nullquery.is(field, null)为空
startswith/endswithilike("v%") / ilike("%v")前后缀匹配
orquery.or(orSyntax)拼接 PostgREST 的 or 语法
and抛出Operator 'and' is not supported显式拒绝

其中or分支会将子条件序列化为 PostgREST 的 Horrock 语法(如categoryId.eq.1,title.ilike.%foo%),并在ina/ninacontainsstartswithendswith等场景自动补充{}%等必要包裹符号。

而 mapOperator.ts 负责把 Refine 的CrudOperators映射为 PostgREST 运算符(例如ne → neqcontains → ilikencontains → not.ilikennull → not.is等),并明确抛出between/nbetween不支持的错误——这正是generateFilterbetween单独用gte + lte实现的原因。handleError.tsgenerateFilter.ts一起,构成了 Data Provider 错误归一化与查询构建的底层支撑。

另外,@refinedev/supabase的 Data Provider 在写操作时还会自动处理created_atupdated_at等时间戳字段的序列化,并支持通过meta.select控制返回字段;Live Provider 则通过supabaseClient.channel()监听 Postgres 变更事件,再分发给订阅了liveMode的 Hook。这些行为与示例页面中的表现完全对应,读者可结合 packages/supabase/src/dataProvider/index.ts 与 packages/supabase/src/liveProvider/index.ts 继续深挖。

运行注意事项与常见陷阱

  1. Realtime 在 StackBlitz 中不可用:官方文档明确提示(supabase.md)“StackBlitz environment does not allow Realtime features to work”,因此在沙箱中调试实时功能可能无效果,实时演示请在本机运行。
  2. 不要开启全局liveMode: "auto":由于 supabase-js v2 客户端与@refinedev/supabase的订阅实现限制,同一时间支持单个订阅,建议像示例那样保持全局liveMode: "off",在需要实时能力的页面单独设置liveMode: "manual"(或"auto")。
  3. RLS 与匿名 key:示例使用的是 Supabase 公开演示项目与 anon key;在自有项目中,务必为blog_postscategoriesstorage.objects配置符合业务的安全策略(Row Level Security),否则接口会返回PGRST权限错误(示例的onError已对PGRST301做了登出处理)。
  4. Storage bucket 与 CORS:上传前需在 Supabase 控制台创建 bucket(示例为refine)、开放 public 读权限并配置存储策略;开发环境下若跨域访问资源,需确认 Supabase 项目的 CORS 白名单包含你的本地域名。
  5. 运行环境:示例要求node >= 20(见 package.json 的engines字段),推荐使用仓库采用的 pnpm 工作区方式安装依赖。

小结

Supabase 为后台应用提供了数据库、认证、实时订阅与对象存储的一站式能力,而 Refine 的@refinedev/supabase包将这些能力统一抽象为 Data Provider / Live Provider / Auth Provider,让 CRUD 页面可以用声明式 Hook 直接驱动。通过本文示例,你可以快速获得一个带登录鉴权、关联查询、服务端排序过滤、文件上传与实时提醒的完整后台骨架;如需进一步验证行为,仓库中还提供了对应的端到端测试(cypress/e2e/data-provider-supabase)可供参考。之后只需替换为自有项目的 URL、key 与表结构,即可无缝迁移到生产环境。

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

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

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

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

立即咨询