Supabase 封装与实践
1. 架构:Service + Hooks + React Query
为了实现最高效的封装,我们采用三层架构:
- Service 层:纯函数,调用 Supabase SDK,负责处理分页、连表等逻辑。
- Hooks 层:基于 React Query (TanStack Query) 封装 Service,管理 Loading、缓存和错误。
- UI 层:直接调用自定义 Hooks,极致简洁。
2. 第 0 步:初始化客户端 (Client)
在封装之前,先准备一个统一的客户端连接文件。
// lib/supabaseClient.ts (或 lib/supabase/index.ts)
import { createClient } from "@supabase/supabase-js";
import { Database } from "./types/database.types"; // 推荐生成类型
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey);
3. 第一步:封装通用 API 方法 (Service - TS 版)
利用 TypeScript 泛型确保存访数据从表名到字段名都有全自动补全。
// lib/supabase/services.ts
import { supabase } from "../supabaseClient";
import type { Database } from "../types/database.types";
type Tables = Database["public"]["Tables"];
/**
* 基础 CRUD 封装
*/
// 1. 获取单条基础数据 (默认 select *)
export const getOne = async <T extends keyof Tables>(
tableName: T,
id: string | number
) => {
const { data, error } = await supabase
.from(tableName)
.select("*")
.eq("id", id)
.single();
if (error) throw error;
return data as Tables[T]["Row"];
};
// 2. 获取详情 (支持自定义连表 select 字符串)
export const getDetail = async <T extends keyof Tables, R = any>(
tableName: T,
id: string | number,
select = "*"
) => {
const { data, error } = await supabase
.from(tableName)
.select(select)
.eq("id", id)
.single();
if (error) throw error;
return data as R;
};
// 2. 插入数据
export const create = async <T extends keyof Tables>(
tableName: T,
payload: Tables[T]["Insert"]
) => {
const { data, error } = await supabase
.from(tableName)
.insert(payload)
.select()
.single();
if (error) throw error;
return data as Tables[T]["Row"];
};
// 3. 更新数据
export const update = async <T extends keyof Tables>(
tableName: T,
id: string | number,
payload: Tables[T]["Update"]
) => {
const { data, error } = await supabase
.from(tableName)
.update(payload)
.eq("id", id)
.select()
.single();
if (error) throw error;
return data as Tables[T]["Row"];
};
// 4. 删除数据
export const remove = async <T extends keyof Tables>(
tableName: T,
id: string | number
) => {
const { error } = await supabase.from(tableName).delete().eq("id", id);
if (error) throw error;
return true;
};
/**
* 高级查询:分页与连表
*/
// 分页列表查询
export const getPagedData = async <T extends keyof Tables>(
tableName: T,
{ page = 1, pageSize = 10, select = "*", orderField = "created_at" }
) => {
const from = (page - 1) * pageSize;
const to = from + pageSize - 1;
const { data, count, error } = await supabase
.from(tableName)
.select(select, { count: "exact" })
.order(orderField as string, { ascending: false })
.range(from, to);
if (error) throw error;
return {
data: data as any[], // 连表查询结果较难用泛型完全覆盖,建议在调用处断言
total: count || 0,
};
};