mcpField
M
metabase-data-app-routing

metabase-data-app-routing

github.com/metabase开发49.9k800 次安装今天更新
代码仓库

关于 metabase-data-app-routing

为已有 Metabase Data App 添加路由的开发指南,介绍 SDK 提供的路由封装、链接和位置 API。它不依赖路由库,并说明默认页面、导航及路径匹配方式。

主要功能

  1. 01使用 SDK 路由原语
  2. 02读取路径并执行导航
  3. 03链接支持中键与新标签页
  4. 04自动处理 iframe URL 前缀
  5. 05为根路径设置默认页面

适用场景

  1. 01按 URL 路径显示不同页面
  2. 02在 Data App 中添加页面导航
  3. 03从按钮跳转到指定路径

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/metabase/metabase --skill metabase-data-app-routing

安装后在 Claude Code 中输入/metabase-data-app-routing确认已加载。

详细介绍

为 data-app 添加路由

Metabase data-app bundle 不会打包路由库 —— 它从 @metabase/embedding-sdk-react/data-app 导入三个小型原语:

API用途
<DataAppRouter>将应用包裹一次。跟踪当前子路径。自动检测 iframe 的 URL 前缀;bundle 作者无需编写 basename。
<DataAppLink to="/customers/42">内部导航链接。渲染真正的 <a href>,因此中键点击 / cmd + 点击会在新标签页中打开。
useDataAppLocation()返回 { pathname, navigate }。使用 pathname 进行等值匹配 / startsWith 渲染;使用 navigate(to) 进行程序化导航。

这就是全部接口。没有任何版本的 react-router,没有 <BrowserRouter>,没有 <HashRouter>。 该 API 有意与任何路由库解耦,以便未来的 Metabase 版本可以在不修改 bundle 代码的情况下替换底层实现。

何时使用此技能

  • 用户已有一个可用的 data-app 项目 —— 从 data-app 模板脚手架生成,因此已经存在一行式的 vite.config.ts(dataAppConfig())以及导出工厂的 src/index.tsx。开发预览没有项目内入口:SDK 的开发预设会提供它。
  • 用户希望 bundle 在不同 URL(/overview、/customers/:id)渲染不同内容。
  • 不要使用此技能从零脚手架生成项目 —— 它只修补现有的 data-app 项目。如果还没有项目,请停止并告知用户先通过新的 data-app 脚手架创建项目,再添加路由。

dataAppConfig() 已经将 @metabase/embedding-sdk-react/data-app 外部化。你无需编辑 vite.config.ts 来添加路由 —— 只需按下面的步骤编辑 src/App.tsx(并根据需要添加更多组件文件)。

第 1 步 —— 用 <DataAppRouter> 包裹 App.tsx

正常导入路由原语。<DataAppRouter> 不接受 basename prop —— 它在生产环境中自动检测 iframe 的 URL 前缀(/embed/apps/<name>),在 Vite 开发预览中解析为无前缀。

App.tsx 是纯内容 —— 这里没有 <MetabaseProvider>。SDK 的开发预览入口(DataAppDevProvider)和生产宿主(DataAppProvider)各自将树包裹在自己的 realm 中。

import { StaticQuestion } from "@metabase/embedding-sdk-react";
import {
  DataAppRouter,
  DataAppLink,
  useDataAppLocation,
} from "@metabase/embedding-sdk-react/data-app";

function Nav() {
  return (
    <nav style={{ padding: 16, borderBottom: "1px solid #e5e7eb" }}>
      <DataAppLink to="/" style={{ marginRight: 16 }}>Overview</DataAppLink>
      <DataAppLink to="/customers/42">Customer 42</DataAppLink>
    </nav>
  );
}

function Page() {
  const { pathname } = useDataAppLocation();

  if (pathname === "/") {
    return (
      <div style={{ padding: 24 }}>
        <h1>Overview</h1>
        <StaticQuestion questionId={1} height={360} />
      </div>
    );
  }

  const customerMatch = pathname.match(/^\/customers\/(\d+)$/);
  if (customerMatch) {
    const id = Number(customerMatch[1]);
    return (
      <div style={{ padding: 24 }}>
        <h1>Customer #{id}</h1>
        <StaticQuestion questionId={id} height={360} />
      </div>
    );
  }

  return <div style={{ padding: 24 }}>Not found: {pathname}</div>;
}

export default function App() {
  return (
    <DataAppRouter>
      <Nav />
      <Page />
    </DataAppRouter>
  );
}

运行 yarn dev,点击链接,观察 URL 栏变化。在 http://localhost:5174/customers/42 重新加载,开发预览会直接落在客户路由上。

加载时始终预选默认(最左侧)标签页

如果应用呈现多个标签页(或任何顶层页面切换器),基础路径 / 必须渲染默认 —— 最左侧 / 第一个 —— 标签页的内容,绝不能是空白页面、"Not found" 或空外壳。 这是最常见的单一错误:应用在 / 启动,没有分支匹配,用户什么都看不到,直到点击某个标签页。不要依赖用户(或后续导航)来选择第一个标签页 —— 默认标签页就是初始状态。

有两种等效的方式保证这一点,取决于标签页是否由路由支持:

  • 由路由支持的标签页 —— 为 / 提供一个显式分支来渲染第一个标签页,这样未匹配 / 基础路径会解析到它:

    const TABS = [
      { path: "/", label: "Overview", render: () => <Overview /> },
      { path: "/customers", label: "Customers", render: () => <Customers /> },
      { path: "/reports", label: "Reports", render: () => <Reports /> },
    ];
    
    function Page() {
      const { pathname } = useDataAppLocation();
      // Exact-or-prefix match; fall back to the FIRST tab so `/` (and any
      // unknown sub-path) always shows the default tab, never a blank page.
      const active =
        TABS.find((t) => t.path !== "/" && pathname.startsWith(t.path)) ?? TABS[0];
      return active.render();
    }
    
  • 本地状态标签页(无路由) —— 将活动标签页状态初始化为第一个标签页,这样首次渲染就会显示它:

    const [active, setActive] = useState(TABS[0].id); // default = leftmost tab
    

通过在基础路径(/)全新加载应用来验证:最左侧标签页的内容必须立即可见,并且该标签页在标签栏中必须显示为选中状态。

导航如何转换为父级 URL

你无需为此做任何事。以下为背景信息:

  • Bundle 调用 <DataAppLink to="/customers/42"> → 宿主的 navigate 会以 /embed/apps/<name>/customers/42 执行 pushState。
  • 父级的 AppView 会观察 iframe URL 的变化,并将其镜像到父级的地址栏,显示为 /apps/<name>/customers/42 —— /embed 前缀会被去除,因为父级的 React Router 路由是 /apps/:name/*,而不是 /embed/...。
  • 刷新可以正常工作,因为 BE 会为每个 /embed/apps/:name/* 子路径提供相同的 data-app.html;iframe 启动后,<DataAppRouter> 会读取 window.location.pathname,自动检测前缀,并从正确的子路径开始。

不要做什么

  • 不要向项目中添加 react-router-dom(或任何路由库)。Bundle 不需要它。
  • 不要在 bundle 内使用 <BrowserRouter> 或 <HashRouter>。它们会运行自己的 setState 流程,从而触发 Near Membrane 批处理 bug。
  • 不要从 bundle 内直接调用 window.history.pushState。请使用 <DataAppLink> 或来自 useDataAppLocation 的 navigate。
  • 不要尝试读取或写入父级的 URL。 iframe 的 sandbox 属性在设计上会阻止这一点。

常见模式

基于路径的条件渲染
const { pathname } = useDataAppLocation();
if (pathname === "/") return <Home />;
if (pathname.startsWith("/customers/")) return <Customer />;
return <NotFound />;
编程式导航(例如从 <button> 触发)
function OpenCustomerButton({ id }: { id: number }) {
  const { navigate } = useDataAppLocation();
  return <button onClick={() => navigate(`/customers/${id}`)}>Open #{id}</button>;
}
手动解析路径参数

没有内置的 useParams。请自行匹配:

const { pathname } = useDataAppLocation();
const match = pathname.match(/^\/customers\/(\d+)$/);
if (match) {
  const id = Number(match[1]);
  // ...
}

如果你发现自己经常这样做,可以将其提取到 bundle 内你自己的辅助函数中:

function useCustomerIdFromPath(): number | null {
  const { pathname } = useDataAppLocation();
  const m = pathname.match(/^\/customers\/(\d+)$/);
  return m ? Number(m[1]) : null;
}

故障排查

症状修复
<DataAppLink> 在初始加载时渲染为纯文本(没有可点击的链接)。Bundle 尚未加载完成,显示的是回退路径。提供者包裹会自动提供(SDK 开发预览的 DataAppDevProvider,或宿主的 DataAppProvider)—— 不要在应用内添加 <MetabaseProvider>。一旦 bundle 启动,回退内容会解析为真实链接。
URL 改变了但 UI 没有变化。树中仍然存在 <BrowserRouter>/<HashRouter>。请移除该路由库,改用 <DataAppRouter> / <DataAppLink> —— Near Membrane 与 React-18 批处理的交互会破坏每一个在 bundle 内运行自身 setState 的路由器。
在 <DataAppLink> 上中键点击没有任何反应。该链接使用 event.button !== 0 来跳过非左键点击;请检查你是否将其包裹在另一个会吞掉该事件的组件中。

评论

支持 Markdown