TanStack Start 集成 Supabase 实战:基于 React 的全栈认证与数据获取示例解析
TanStack Start 集成 Supabase 实战基于 React 的全栈认证与数据获取示例解析【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router本文以仓库 examples/react/start-supabase-basic 为例系统讲解如何在 TanStack Start客户端优先、全栈类型安全的 React 框架项目中集成 Supabase实现邮箱密码认证、受保护路由、服务端数据获取与登出跳转等完整链路。读完本文你将掌握supabase/ssr服务端客户端封装、createServerFn认证函数、路由级鉴权守卫的写法并能直接基于该示例搭建自己的 Supabase 全栈应用。示例定位一个可运行的 Supabase 全栈认证样板start-supabase-basic是一个围绕 Supabase Auth 与数据库能力构建的最小完整示例它演示了四类核心集成点使用 Supabase Auth 完成邮箱 密码的注册、登录与登出通过 TanStack Start 的服务端能力访问 Supabase 客户端在路由加载阶段进行鉴权保护需要登录才能访问的页面结合 Server Functions 实现服务端数据获取示例中以 JSONPlaceholder 模拟数据库查询。整个示例的目录结构如下examples/react/start-supabase-basic/ ├── src/ │ ├── components/ # Auth / Login / NotFound / DefaultCatchBoundary │ ├── hooks/ │ │ └── useMutation.ts # 轻量级 mutation 状态管理 hook │ ├── routes/ │ │ ├── __root.tsx # 根路由全局用户上下文 SEO 布局 │ │ ├── _authed.tsx # 受保护布局路由登录鉴权守卫 │ │ ├── index.tsx / login.tsx / signup.tsx / logout.tsx │ │ └── _authed/ │ │ ├── posts.tsx / posts.index.tsx / posts.$postId.tsx │ ├── utils/ │ │ ├── supabase.ts # 服务端 Supabase 客户端工厂 │ │ └── posts.ts # 服务端数据获取函数 │ ├── router.tsx # Router 实例与类型注册 │ └── routeTree.gen.ts # 由路由生成器自动生成 ├── vite.config.ts ├── package.json └── README.md基于示例创建新项目README 中提供了两种快速上手方式。最直接的方式是使用gitpick从本仓库拉取该示例作为新项目基底npx gitpick TanStack/router/tree/main/examples/react/start-supabase-basic start-supabase-basic复制完成后进入目录安装依赖并启动开发服务器pnpm install pnpm devpnpm dev以开发模式启动应用监听文件变化并自动重建资源。生产构建与本地预览命令如下pnpm build pnpm preview从 package.json 的 scripts 可以看到完整的命令矩阵dev对应vite devbuild对应vite build tsc --noEmit构建后还会执行 TypeScript 全量类型检查保证类型安全start则通过srvx以生产模式运行服务端产物。环境准备Supabase 项目与 .env 配置示例依赖 Supabase 云服务运行时需要两个环境变量示例的.env文件模板如下SUPABASE_URLyour-project-url SUPABASE_ANON_KEYyour-anon-key准备步骤在 Supabase 控制台创建一个项目在项目设置中获取 Project URL 与 anon public key将两个值填入.env注意.env不应提交到版本库。代码侧通过process.env.SUPABASE_URL与process.env.SUPABASE_ANON_KEY读取这两个变量见 src/utils/supabase.ts因此服务端构建时必须保证这两个环境变量可用。核心封装服务端 Supabase 客户端与 Cookie 会话整个集成最关键的代码在 src/utils/supabase.ts它利用supabase/ssr的createServerClient创建了一个服务端专用的 Supabase 客户端并通过 TanStack Start 提供的getCookies/setCookie与 SSR 请求的 Cookie 读写打通import { getCookies, setCookie } from tanstack/react-start/server import { createServerClient } from supabase/ssr export function getSupabaseServerClient() { return createServerClient( process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!, { cookies: { getAll() { return Object.entries(getCookies()).map(([name, value]) ({ name, value, })) }, setAll(cookies) { cookies.forEach((cookie) { setCookie(cookie.name, cookie.value) }) }, }, }, ) }这段封装解决了 SSR 场景下 Supabase 会话管理的经典难题getAll()将请求携带的 Cookie 转换为 Supabase 客户端可读的数组结构使服务端能还原登录态setAll()将 Supabase 需要写入的会话 Cookie如sb-*-auth-token回写到响应中实现登录后浏览器持久化会话。由于所有认证操作getUser、signInWithPassword、signUp、signOut都通过这个工厂创建的服务端客户端执行会话 Cookie 的读写被统一收口客户端无需自行管理 token。这也是supabase/ssr的核心价值——兼容 Next.js、TanStack Start 等 SSR 框架的 Cookie 抽象。认证链路登录、注册与登出登录Server Function 调用 Supabase Auth登录逻辑位于 _authed.tsx以createServerFn({ method: POST })定义服务端函数loginFn先通过.validator()对入参邮箱与密码做类型校验再调用 Supabase 的signInWithPasswordexport const loginFn createServerFn({ method: POST }) .validator((d: { email: string; password: string }) d) .handler(async ({ data }) { const supabase getSupabaseServerClient() const { error } await supabase.auth.signInWithPassword({ email: data.email, password: data.password, }) if (error) { return { error: true, message: error.message } } })值得注意的错误处理约定函数不抛异常而是返回{ error, message }结构由前端根据返回值决定是否跳转避免把 Supabase 的错误信息直接暴露为未处理异常。注册支持重定向回跳注册函数定义在 signup.tsx同样使用createServerFn({ method: POST })validator 额外接受可选的redirectUrl成功后通过throw redirect({ href: data.redirectUrl || / })跳转回来源页面或首页export const signupFn createServerFn({ method: POST }) .validator( (d: { email: string; password: string; redirectUrl?: string }) d, ) .handler(async ({ data }) { const supabase getSupabaseServerClient() const { error } await supabase.auth.signUp({ email: data.email, password: data.password, }) if (error) { return { error: true, message: error.message } } throw redirect({ href: data.redirectUrl || / }) })throw redirect(...)是 TanStack Start / TanStack Router 中中断当前处理流程并触发导航的标准做法服务端抛出的 redirect 会被框架捕获并转换为响应跳转。登出loader 中执行并跳回首页登出路由 logout.tsx 展示了导航即动作的写法Route关闭预加载preload: false在loader中直接执行logoutFnconst logoutFn createServerFn().handler(async () { const supabase getSupabaseServerClient() const { error } await supabase.auth.signOut() if (error) { return { error: true, message: error.message } } throw redirect({ href: / }) }) export const Route createFileRoute(/logout)({ preload: false, loader: () logoutFn(), })用户点击导航栏的 Logout 链接后路由进入/logoutloader 调用服务端登出、清理会话 Cookie并以 redirect 回到首页。路由级鉴权_authed 布局守卫示例通过路由前缀布局_authed下划线前缀表示该路径段不会出现在 URL 中实现整组页面的登录保护。核心逻辑在 _authed.tsxexport const Route createFileRoute(/_authed)({ beforeLoad: ({ context }) { if (!context.user) { throw new Error(Not authenticated) } }, errorComponent: ({ error }) { if (error instanceof Error error.message Not authenticated) { return Login / } throw error }, })beforeLoad读取根路由注入的context.user未登录时抛出自定义的Not authenticated错误errorComponent捕获该错误并渲染Login /实现未登录访问受保护页面时原地展示登录表单其他错误则继续向上抛出交给外层错误边界处理。凡是位于src/routes/_authed/下的子路由posts.tsx、posts.index.tsx、posts.$postId.tsx都自动继承这一守卫无需在每个页面重复鉴权代码。全局用户状态根路由 beforeLoad 注入context.user从哪来答案在根路由 __root.tsx根路由的beforeLoad通过服务端函数fetchUser调用supabase.auth.getUser()获取当前用户并将结果放进路由上下文const fetchUser createServerFn({ method: GET }).handler(async () { const supabase getSupabaseServerClient() const { data, error: _error } await supabase.auth.getUser() if (!data.user?.email) { return null } return { email: data.user.email } }) export const Route createRootRoute({ beforeLoad: async () { const user await fetchUser() return { user } }, ... })页面组件通过Route.useRouteContext()读取user见 __root.tsx导航栏据此切换显示邮箱 Logout或显示 Login两种状态{user ? ( span classNamemr-2{user.email}/span Link to/logoutLogout/Link / ) : ( Link to/loginLogin/Link )}由于根路由的beforeLoad在每次导航时都会执行登录/登出后用户状态会自动刷新配合 router.tsx 中开启的scrollRestoration: true体验接近原生应用。根路由同时承担了全局 SEOseo()工具函数生成 title/description、HeadContent/Scripts挂载与TanStackRouterDevtools注入等职责。客户端交互useMutation 轻量封装示例没有引入完整的数据请求库而是自研了一个约 40 行的 useMutation hook管理提交状态机与结果const [status, setStatus] React.useState idle | pending | success | error (idle) const mutate React.useCallback( async (variables: TVariables): PromiseTData | undefined { setStatus(pending) setSubmittedAt(Date.now()) setVariables(variables) try { const data await opts.fn(variables) await opts.onSuccess?.({ data }) setStatus(success) setError(undefined) setData(data) return data } catch (err) { setStatus(error) setError(err as TError) } }, [opts.fn], )它对外暴露statusidle/pending/success/error、variables、submittedAt、mutate、error、data六个值。登录表单 Login.tsx 的使用方式onSubmit中通过new FormData(e.target)取表单字段调用loginMutation.mutate({ data: { email, password } })onSuccess中若返回无错误则router.invalidate()刷新全部加载数据并router.navigate({ to: / })跳转首页登录失败时afterSubmit区域展示错误信息并在遇到Invalid login credentials时给出Sign up instead?按钮一键将同一表单数据转为注册请求signupMutation.mutate(...)。共享的表单 UI 抽在 Auth.tsx 中通过actionText、status、onSubmit、afterSubmit四个 props 同时服务登录与注册两个页面按钮在pending状态自动禁用并显示占位符。数据获取Server Functions 与模拟数据库_authed下的文章列表演示了服务端取数 → loader 注入 → 组件消费的标准流程。数据函数定义在 src/utils/posts.ts以createServerFn封装示例用 JSONPlaceholder 充当远程数据源export const fetchPost createServerFn({ method: GET }) .validator((d: string) d) .handler(async ({ data: postId }) { console.info(Fetching post with id ${postId}...) const post await axios .getPostType(https://jsonplaceholder.typicode.com/posts/${postId}) .then((r) r.data) .catch((err) { console.error(err) if (err.status 404) { throw notFound() } throw err }) return post }) export const fetchPosts createServerFn({ method: GET }).handler(async () { console.info(Fetching posts...) await new Promise((r) setTimeout(r, 1000)) return axios .getArrayPostType(https://jsonplaceholder.typicode.com/posts) .then((r) r.data.slice(0, 10)) })两个细节值得学习fetchPost在 404 时throw notFound()由路由层的notFoundComponent渲染专属的Post not found页面见 posts.$postId.tsxfetchPosts人为延迟 1 秒便于观察 loader 与 Suspense 行为。路由侧在loader中直接调用这些函数数据在服务端完成拉取后再序列化到客户端// posts.tsx export const Route createFileRoute(/_authed/posts)({ loader: () fetchPosts(), component: PostsComponent, }) // posts.$postId.tsx export const Route createFileRoute(/_authed/posts/$postId)({ loader: ({ params: { postId } }) fetchPost({ data: postId }), errorComponent: PostErrorComponent, component: PostComponent, notFoundComponent: () NotFoundPost not found/NotFound, })组件侧分别用Route.useLoaderData()读取数据列表页还额外追加了一个i-do-not-exist的占位条目点击它即可直观验证 404 分支与notFoundComponent的表现见 posts.tsx。构建配置一览vite.config.ts 揭示了示例的技术栈组合export default defineConfig({ server: { port: 3000 }, resolve: { tsconfigPaths: true }, plugins: [tailwindcss(), tanstackStart(), viteReact()], })tanstackStart()是 TanStack Start 的 Vite 插件负责文件路由扫描、Server Functions 编译与 SSR 管线tailwindcss()提供原子化样式示例 UI 全部使用 Tailwind 类名viteReact()提供 React Fast Refresh。依赖侧package.json以supabase/ssrsupabase/supabase-js提供认证与数据库客户端以tanstack/react-start、tanstack/react-router、react-router-devtools构成框架底座redaxios作为轻量 HTTP 客户端。开发端口固定为 3000可通过server.port调整。小结从start-supabase-basic示例可以提炼出一套可复用的 TanStack Start Supabase 集成模式会话收口用supabase/ssr的createServerClient封装服务端客户端通过getCookies/setCookie打通 SSR Cookie动作即函数登录、注册、登出全部写成createServerFn用.validator保障入参类型用return { error }约定而非异常传递业务错误用throw redirect完成服务端跳转守卫即路由在布局路由的beforeLoad中校验根路由注入的context.user用errorComponent就地渲染登录表单保护整组子路由数据在服务端把取数逻辑放进 Server Functionsloader 直接消费404 通过notFound()精细化处理。若需将示例中的模拟数据替换为真实 Supabase 数据只需在posts.ts的 Server Function 内改用getSupabaseServerClient().from(posts).select(*)这类数据库查询即可其余架构无需改动。深入理解本示例后建议继续阅读仓库中的 React Start 框架文档 与 React Router 路由 API 文档掌握 Server Functions 的更多形态与路由守卫的进阶用法。【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考