Skip to content

Frontend Pages

worable edited this page Oct 20, 2026 · 2 revisions

页面路由

页面路由(pages)是前端包的核心能力,声明主题或插件提供的用户可访问页面。

页面定义

interface FrontendPageDefinition {
  path: string; // 路由路径
  component: string; // 组件名
  data?: FrontendPageDataRequirement[]; // 可选,Finder 数据绑定
  layout?: string; // 可选,Layout 名
}

interface FrontendPageDataRequirement {
  finder: string; // Finder 名称
  as: string; // 注入到组件 props 的键名
  input?: Record<string, unknown>; // 传给 Finder 的额外参数
  permission?: string; // Finder 执行前检查的权限
}

组件契约

页面组件从 props 接收平台注入的数据。完整形状见 FrontendPageProps(packages/sdk/src/frontend.ts):

interface FrontendPageProps<P extends Record<string, unknown> = Record<string, unknown>> {
  data: P;                          // data 绑定注入的数据,键为 as
  params: Record<string, string>;   // 路径参数,如 :slug
  settings: FrontendSettings;       // 前端设置(见设置 Schema)
  actions: FrontendActionExecutors; // 声明式操作,见 admin-extensions
  assetsBaseUrl?: string;           // 解析主题/插件资源 URL 的同源 BFF 基址
}

渲染来源(theme/plugin/builtin)由平台内部的 PageResolution.source 决定,不会作为 prop 传给组件。

组件以 pageComponents[component] 的名称为键导出:

export function BlogPostPage({ data, params }: FrontendPageProps) {
  const post = data.post as Post;
  return <article><h1>{post.title}</h1></article>;
}

别忘了 export default frontend。 缺少 default 导出会让整份前端包解析失败,所有页面静默 not-found。详见前端包介绍:三条硬约束。

路径格式

静态路径

{ "path": "/", "component": "HomePage" }
{ "path": "/about", "component": "AboutPage" }

动态参数

使用 :param 声明路径参数:

{ "path": "/blog/:slug", "component": "BlogPostPage" }

参数通过 captureFrontendPathParams 注入 Finder,可在组件中通过 params.slug 访问。

通配

通配符 * 和 ** 只能作为路径的最后一段。有两种用法:

全局 fallback(* 或 /**):

{ "path": "*", "component": "NotFoundPage" }

* 和 /** 匹配所有路径,优先级最低(specificity = -1)。在跨包覆盖时,显式匹配(非 fallback)优先于 fallback 匹配。

fallback 模板不会被当作正常页面直接返回:FrontendRoute 命中 fallback 时调用 notFound(),由 (user)/not-found.tsx 边界渲染主题声明的 * 模板(无则回退平台默认 404 面板),HTTP 状态码保持真实 404。

路径内通配(如 /docs/*):

{ "path": "/docs/*", "component": "DocPage" }

匹配 /docs/ 下的所有路径。路径内 * 的 specificity 为 0(低于字面段和参数段)。

路径匹配优先级

包内优先级

selectFrontendPageDefinition 按 specificity 评分选择最佳匹配:

段类型 评分 示例
字面段 2 /about 中的 about
参数段 :param 1 /:slug 中的 :slug
通配段 */**(路径内) 0 /docs/* 中的 *
全局 fallback *//** -1 *

评分总和最高的路由胜出。例如 /about(2 分)优先于 /:slug(1 分)优先于 *(-1 分)。

跨包覆盖优先级

resolveFrontendPageSource 按以下顺序选择页面来源:

  1. 主题显式匹配(非 fallback 路径)
  2. 插件显式匹配(非 fallback 路径)
  3. 主题 fallback(* 或 /**)
  4. 插件 fallback(* 或 /**)
  5. 内置

数据绑定

页面可以通过 data 字段绑定一个或多个 Finder,每个绑定声明 Finder 名称、注入键名和可选参数:

{
  "path": "/blog/:slug",
  "component": "BlogPostPage",
  "data": [
    { "finder": "blogPost", "as": "post" },
    { "finder": "comments", "as": "comments", "input": { "limit": 10 } }
  ]
}

渲染时,平台并行调用每个 Finder(Promise.all),传入路径参数、页面设置和 input。返回值以 as 声明的键名注入组件 props(如 post、comments)。如果声明了 permission,Finder 执行前会检查当前会话权限。详见 Finder 数据源。

Layout 绑定

页面可以通过 layout 字段使用命名 Layout:

{
  "path": "/blog/:slug",
  "component": "BlogPostPage",
  "layout": "DefaultLayout"
}

Layout 必须在前端包的 layouts 列表中声明。渲染时,平台用 Layout 组件包装页面组件。详见 命名 Layout。

完整示例

{
  "pages": [
    { "path": "/", "component": "HomePage", "layout": "DefaultLayout" },
    { "path": "/about", "component": "AboutPage", "layout": "DefaultLayout" },
    {
      "path": "/blog",
      "component": "BlogListPage",
      "data": [{ "finder": "blogList", "as": "posts" }],
      "layout": "DefaultLayout"
    },
    {
      "path": "/blog/:slug",
      "component": "BlogPostPage",
      "data": [{ "finder": "blogPost", "as": "post" }],
      "layout": "DefaultLayout"
    },
    { "path": "*", "component": "NotFoundPage" }
  ],
  "layouts": ["DefaultLayout"],
  "finders": ["blogList", "blogPost"]
}

冲突检测

启用/安装插件时,assertNoConflictingFrontendPages 检查该插件的页面路径不与已激活插件的页面路径重叠(主题页面不参与此检测,通过覆盖优先级共存)。路径模式匹配包括 :param 和 *。

如果两个插件声明了重叠的路径(如 /blog/:slug 和 /blog/:id),启用后者时会返回 409 错误。

渲染流程

用户请求 /blog/hello-world
  → (user)/[...slug] catch-all
  → resolveFrontendPageSource(主题 > 插件 > 内置)
  → selectFrontendPageDefinition(包内具体 > 参数 > 通配)
  → captureFrontendPathParams(提取 :slug = hello-world)
  → 执行 Finder(如有 data 字段)
  → 用 Layout 包装(如有 layout 字段)
  → 渲染组件

Clone this wiki locally