Supabase Studio 页面开发指南:pages 目录的组织原则、withAuth 模板与布局体系
【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase
本文以 Supabase Studio 仓库中 pages 目录的编写规范 为核心,完整拆解 Studio(Supabase 控制台前端,基于 Next.js pages router 构建)中新建一个页面的标准流程:页面拆分原则、官方页面模板逐行解析、withAuth鉴权 HOC 的底层实现,以及布局组件体系的实际用法。读完后,你可以按照仓库既定的规范独立写出符合 Studio 工程约定的页面,并理解鉴权、布局、状态管理在各层中分别由谁负责。
页面组织原则:小构件组装,而非大文件堆砌
pages/README.md 给出了两条总纲,它们决定了 Studio 页面代码的基本形态:
- 把页面拆成小的构建块(building blocks):与某个页面强耦合的组件,应放在
components/interfaces/xxx/...目录下,而不是内联写在页面文件里。 - UI 逻辑一律用
useState等 React hooks 处理,不要为 UI 逻辑创建 MobX 局部 store。
这两条原则与 components/README.md 中定义的分层约定是配套的。Studio 将组件按"耦合范围"分为三层:
| 组件类型 | 存放位置 | 职责 |
|---|---|---|
| 布局组件 | components/layouts/xxx | 声明页面的整体结构与框架(导航、侧栏、滚动容器) |
| 界面耦合组件 | components/interfaces/xxx | 只服务于某个特定界面/页面的构件 |
| 可复用 UI 组件 | components/ui/xxx | 跨多个页面复用的通用组件 |
components README 还规定:如果组件自带常量、工具函数且与其强耦合,应和组件一起放进一个文件夹,以index.tsx作为入口;否则单独一个文件即可。组件模板要求使用命名导出(named export)而非默认导出:
// Declare the prop types of your component interface ComponentAProps { sampleProp: string } // use a named export, not a default export export const ComponentA = ({ sampleProp }: ComponentAProps) => { return <div>ComponentA: {sampleProp}</div> }这个"页面 = 布局 + 界面构件 + 复用 UI"的组合关系,在 pages README 给出的官方模板中体现得最为直接。
官方页面模板逐行解析
pages README 给出的标准模板如下(这是编写新页面的起点,本文后续各节逐一展开其中的关键部分):
import { NextPage } from 'next' import { withAuth } from 'hooks/misc/withAuth' // Import the corresponding layout based on the page import { Layout } from 'components/layouts' // Import the main building blocks of the page import { ... } from 'components/interfaces/xxx' // Import reusable UI components if needed import { ... } from 'components/ui/xxx' // Name your page accordingly const Page: NextPage = () => { return ( <Layout> <div>Page content</div> </Layout> ) } export default withAuth(Page)模板的要点可以归纳为四步:
- 页面函数以
NextPage类型标注(如const Page: NextPage = () => {...}),命名与页面职责对应; - 根据页面所处位置选择对应的 Layout 包裹内容——注释中明确写着 "Import the corresponding layout based on the page"。模板里的
Layout是示意名;在实际仓库中,页面都是直接导入具体布局组件,例如 organizations.tsx 中:import { AppLayout } from '@/components/layouts/AppLayout/AppLayout' import { DefaultLayout } from '@/components/layouts/DefaultLayout' import { PageLayout } from '@/components/layouts/PageLayout/PageLayout' import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold' - 从
components/interfaces/xxx导入页面主体构件,需要时再从components/ui/xxx导入复用 UI 组件; - 页面以
withAuth(Page)作为默认导出,把页面组件交给鉴权 HOC 包裹,而不是在页面内部手写登录判断。
值得注意的一个细节:withAuth并非只能包裹裸的NextPage。它同时支持带getLayout静态方法的NextPageWithLayout类型,并在包裹后透传getLayout(见 withAuth.tsx 中isNextPageWithLayout(WrappedComponent)的判断)。该类型的定义与类型守卫位于 types/next.ts:
export type NextPageWithLayout<P = { dehydratedState: any }, IP = P> = NextPage<P, IP> & { getLayout?: (page: React.ReactNode) => React.ReactNode } export function isNextPageWithLayout<T>( Component: ComponentType<T> | NextPageWithLayout<T, T> ): Component is NextPageWithLayout<T, T>因此在 Studio 中,一个页面既可以通过模板中的Layout包裹方式控制布局,也可以通过getLayout静态方法声明布局,二者都受 HOC 兼容。
withAuth:页面的统一鉴权入口
模板中export default withAuth(Page)是整个页面骨架里最关键的一行。hooks/misc/withAuth.tsx 的实现说明了它实际承担了哪些职责,远不止"没登录就跳转":
1. 自托管部署下直接短路。HOC 首先检查平台标识:
// ignore auth in self-hosted if (!IS_PLATFORM) { return WrappedComponent }也就是说,IS_PLATFORM为 false 的自托管(self-hosted)部署中,withAuth是一个 no-op,直接返回原组件。这解释了为什么同一套页面代码在平台版和自托管版都能运行。
2. MFA / 认证保证等级(AAL)检查。HOC 接收一个选项{ useHighestAAL: boolean },默认true:
const isAtHighestAAL = isSuccessAAL && aalData.currentLevel === aalData.nextLevel const isCorrectLevel = options.useHighestAAL ? isAtHighestAAL : true const needsMfaElevation = isLoggedIn && !isCorrectLevel其含义是:平台 API 会拒绝尚未完成 MFA 挑战的会话,所以大部分页面都要求最高认证等级(AAL2)。源码注释明确说明:只有在"不读取平台 API、且需要在用户完成登录前可达"的页面才应显式传useHighestAAL: false退出检查。当会话处于 AAL1 但需要提升时,HOC 不会注销用户,而是带着returnTo参数将其导向/sign-in-mfa完成 MFA 挑战(对应 pages/sign-in-mfa.tsx);若根本没有会话,则先signOut()再跳/sign-in?returnTo=...,并处理BASE_PATH子路径部署下location.pathname的剥离。
3. 会话加载超时保护。如果 session 或 AAL 查询在 10 秒(MAX_TIMEOUT = 10000)内未完成,页面会弹出SessionTimeoutModal(来自components/interfaces/SignIn/SessionTimeoutModal),并提供重试入口;对/project/路由还会附带projectRef、orgSlug作为支持上下文。
4. 权限查询错误提示。HOC 同时发起usePermissionsQuery(),若权限拉取失败且用户已处于最高 AAL,则通过toast.error提示刷新页面或提交 support ticket。
对页面开发者的实际含义是:页面本身不需要写任何登录守卫逻辑,只需按模板用withAuth导出页面;登录态、MFA 升级、超时兜底、错误提示全部由 HOC 在页面外完成。
布局体系:AppLayout、DefaultLayout、PageLayout 与 Scaffold
模板注释 "Import the corresponding layout based on the page" 指向的是一套层级化的布局组件,全部位于 components/layouts/ 目录。以 organizations.tsx 这一真实页面为例,可以看到 Studio 页面典型的布局嵌套方式(该页展示组织列表,带搜索框与空状态):
<ScaffoldContainer> <ScaffoldSection isFullWidth className="flex flex-col gap-y-4"> ... </ScaffoldSection> </ScaffoldContainer>其页面组件类型标注为NextPageWithLayout(而非模板中最简的NextPage),数据通过 React Query hook(useOrganizationsQuery)拉取,功能开关通过useIsFeatureEnabled('organizations:create')判断。从源码结构看,Studio 的布局按页面所处域划分:AppLayout(全局应用框架)、DefaultLayout、PageLayout(通用内容区)、Scaffold(栅格化容器/分区),以及面向特定域的专用布局如ProjectLayout、OrganizationLayout、WizardLayout、SQLEditorLayout等。页面开发者的选择规则即 README 模板所暗示的:依据页面所在导航域,导入"对应"的那个布局。
withAuth透传getLayout的能力(前文已述)保证了布局声明在鉴权包裹之后仍然生效,布局与鉴权两条链路互不干扰。
状态管理原则:UI 逻辑用 hooks,数据走 data 层
pages README 的第二条原则——"UI 相关逻辑只用useState,不要为此创建 MobX 局部 store"——需要与仓库的整体状态架构放在一起理解才能避免误用。根据 studio 的 AGENTS.md 中的分层说明,Studio 的状态管理按数据的性质分工:
- 全局状态:valtio(
state/目录); - URL 状态:nuqs;
- 表单:react-hook-form + zod;
- 平台 API 数据:统一走
data/fetchers.ts(openapi-fetch),按资源分目录存放,配 React Query hook; - 页面内纯 UI 状态(如 organizations.tsx 中的
const [search, setSearch] = useState('')搜索词):就地使用useState。
也就是说,README 禁止的"为 UI 逻辑建 MobX store",其正面替代正是这种分工:凡是会被网络、路由或全局共享的,进对应层;凡是只影响当前页面渲染的,useState就地解决即可。AGENTS.md 同时对页面级渲染风格有进一步约定(可作为编写页面 body 时的补充规范):fetch 状态用顶层 early return 或平铺的&&守卫块渲染,禁止嵌套三元;布尔变量以is/has/can/should命名并从现有状态推导,而不是用useEffect同步镜像值。
实战对照:按模板走一遍真实页面
把官方模板与 organizations.tsx 的实际写法对照,可以看到模板各占位符在真实代码中的落位:
| 模板占位 | 真实页面中的对应 |
|---|---|
import { NextPage } from 'next' | 使用更完整的NextPageWithLayout类型(@/types) |
import { withAuth } from 'hooks/misc/withAuth' | import { withAuth } from '@/hooks/misc/withAuth',页面末尾export default withAuth(OrganizationsPage) |
import { Layout } from 'components/layouts' | 按域导入具体布局:AppLayout、DefaultLayout、PageLayout、Scaffold |
import { ... } from 'components/interfaces/xxx' | 如OrganizationCard(components/interfaces/Organization/OrganizationCard)、空状态组件(components/interfaces/Home/ProjectList/EmptyStates) |
import { ... } from 'components/ui/xxx' | 如Button、Skeleton(ui)、Input(ui-patterns)、AlertError、NoSearchResults |
<Layout><div>Page content</div></Layout> | <ScaffoldContainer><ScaffoldSection>...</ScaffoldSection></ScaffoldContainer>内组织搜索、列表与错误/空状态分支 |
pages 目录 本身也印证了这套结构:根下是sign-in.tsx、organizations.tsx、maintenance.tsx等独立页面文件,以及org/、project/、support/等按域划分的子目录;每个域内的页面共享同一套布局与withAuth约定,页面专属构件则沉到components/interfaces/对应子目录中。
注意事项:pages 与 TanStack routes 的双轨并行
一个重要的适用前提:Studio 正处于从 Next.js pages router(pages/**)向 TanStack Start(routes/**)迁移的过程中,两套运行时并存,由环境变量STUDIO_FRAMEWORK选择pnpm dev/build使用的运行时(默认next)。依据 studio/AGENTS.md 的说明,迁移约束与本文模板的关系是:
- 不要删除任何
pages/**文件:多数routes/**文件只是薄封装,re-export 对应pages/**文件的默认导出,因此 Next 页面文件在最终清理前对两个运行时都是"承重"的; - 纯页面 body 的修改会自动传播到路由;但当改动涉及
getLayout/布局包裹、页面标题等staticData、withAuth或重定向路径时,需要手工同步到对应的routes/**文件; - 在
pages/**下新建页面时,需要同时在routes/**建立对应路由,并在 TANSTACK_MIGRATION.md 中登记; - 新代码使用 TanStack 原生 API,不引入
next/router或next/link。
因此,pages README 的模板仍然是当前编写页面的标准范式,但动手前应确认所写页面对应的路由侧约定。
小结:新页面开发检查清单
综合 pages README 模板与仓库实际实现,在 Studio 中新写一个页面时应依次确认:
- 布局选择:依据页面所在导航域,从 components/layouts 导入对应布局组件包裹内容;
- 构件拆分:页面专属组件放
components/interfaces/xxx/(与页面强耦合、以index.tsx收口的文件夹结构),通用组件放components/ui/xxx,命名导出; - 鉴权:页面默认导出统一为
withAuth(Page);仅当页面不读平台 API 且需登录前可达时传{ useHighestAAL: false }; - 状态:页面级 UI 状态用
useState;数据请求走data/下的 React Query hook,表单用 react-hook-form + zod,不自建 store; - 布局声明方式:模板式
<Layout>包裹或getLayout静态方法均可,NextPageWithLayout类型让 HOC 兼容两者(见 types/next.ts); - 路由同步:若处于迁移期,按 TanStack 迁移约定同步
routes/**文件并更新迁移清单。
以上规范与实现的对应关系,均可在当前仓库中直接查证:规范原文见 pages/README.md 与 components/README.md,关键实现见 hooks/misc/withAuth.tsx,可运行的参考示例见 pages/organizations.tsx。
【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考