数据层
使用类型化 Supabase 客户端、TanStack Query 和持久化模式。
Expo 模板的数据层相当于原生模板中的 Repository pattern:类型化 Supabase 调用放在小型 feature module 中,TanStack Query 负责 loading、cache、retry、pagination、offline awareness 和 mutation 后的刷新。
技术栈
| 层 | 文件 | 作用 |
|---|---|---|
| Supabase client | lib/supabase.ts | 使用 types/database.ts 类型的 createClient<Database>() |
| Query provider | lib/query.tsx | 全局 QueryClient、offline-first 默认值、focus/online managers |
| Query keys | lib/query.tsx | profile、todos、uploads、subscription、version config 的稳定 key |
| Feature modules | lib/profile.ts、lib/sample-uploads.ts、lib/referrals.ts | 把 query/mutation 函数从 screen 组件中抽离 |
| Screen queries | app/todos.tsx、app/upload-sample.tsx | 把 feature 调用组合成 UI 状态、缓存更新和 toast |
schema 修改后运行类型生成:
pnpm gen:types之后尽量从 types/database.ts 导入 table types,不要手写 row shape。
Query provider
QueryProvider 使用 PersistQueryClientProvider 包住应用,默认值如下:
| 设置 | 值 | 效果 |
|---|---|---|
networkMode | offlineFirst | queries 和 mutations 能容忍临时离线状态 |
| Query retry | 最多 2 次 transient failure | 只重试网络类错误;validation/auth 错误不重试 |
staleTime | 1 分钟 | 最近成功数据避免马上 refetch |
gcTime | 24 小时 | 内存 cache 可以跨普通导航保留 |
| 持久化 | AsyncStorage key SOAR_QUERY_CACHE | 成功 query 最多可跨启动恢复 7 天 |
NetInfo 驱动 TanStack Query online manager。在 native 上,AppState 驱动 focus manager,让应用回到前台时可以刷新 stale data。
Feature modules
可复用数据调用放在 lib/,screen 只负责呈现状态。
lib/profile.ts读取和 upsertprofiles,用expo-image-manipulator压缩头像,上传到avatarsbucket,并解析 signed URL。lib/sample-uploads.ts读取最近的sample_uploadsrows,并通过带进度的 XMLHttpRequest 上传文件到 Supabase Storage。lib/referrals.ts基于 deep-link helper 生成分享链接。lib/network.ts把 transient failure 映射为用户友好文案,并提供retryAsync。lib/cache.ts把小型 JSON payload 存进 AsyncStorage 或 localStorage。
profile.ts 引用了 avatars bucket,但随附迁移只创建 sample-uploads。如果保留头像上传,请为项目添加 avatars bucket 和对应策略。
Todos 参考功能
app/todos.tsx 是最完整的 CRUD 参考:
useInfiniteQuery每次分页读取 20 条todos。- 查询按
user_id过滤,同时 RLS 仍在服务端强制所有权。 - 新增、切换、编辑、删除、清理已完成 mutation 成功后,用
queryClient.setQueryData(...)更新 query cache。 - 重排会向 Supabase 写入
sort_order。 - 如果 refetch 失败但已有缓存 rows,UI 会显示上次保存的 todos,并弹出 reconnect toast。
这里的“乐观”是指 mutation 成功后立即把结果合并到 cache;它不会假装失败的写入已经成功。
离线行为
应用有三层离线行为:
- TanStack Query 会跨启动持久化成功 query 结果。
- NetInfo 标记 QueryClient 的 online/offline 状态,并可驱动
OfflineBanner。 toNetworkFriendlyMessage把 transient errors 转成统一文案。
components/offline-banner.tsx 已实现,但 root layout 当前没有全局挂载
NetworkStatusProvider 和 OfflineBanner。如果产品需要全局离线横幅,把它们加在 navigator 外层即可。
RLS 预期
客户端会按已登录用户过滤,但真正的信任边界来自 Postgres RLS:
| 表/资源 | 客户端访问 |
|---|---|
profiles | 用户可 select、insert、update 自己的 profile row |
todos | 用户可 select、insert、update、delete 自己的 todos |
sample_uploads | 用户可 select、insert、delete 自己的 upload rows |
sample-uploads 的 storage.objects | 用户可读写删除第一个路径段等于自己 user ID 的对象 |
subscriptions / payments | 用户只能 select 自己的 rows;service-role Edge Functions 负责写入 |
添加功能
添加迁移
在 supabase/migrations/ 中创建表、索引、触发器和按 owner 隔离的 RLS policy。
创建 lib/ module
把 Supabase from(...)、storage.from(...) 和数据映射放在小模块里,不要散落在 screen 中。
添加 query keys 和 hooks
在 queryKeys 中添加稳定 key,然后从 useQuery、useInfiniteQuery 或 useMutation 调用你的 module。
渲染 screen
用 SignInGate 处理未登录状态,用 toNetworkFriendlyMessage 映射错误,并在 mutation 后更新或 invalidate cache。
