关于 metabase-data-app-routing
为已有 Metabase Data App 添加路由的开发指南,介绍 SDK 提供的路由封装、链接和位置 API。它不依赖路由库,并说明默认页面、导航及路径匹配方式。
主要功能
- 01使用 SDK 路由原语
- 02读取路径并执行导航
- 03链接支持中键与新标签页
- 04自动处理 iframe URL 前缀
- 05为根路径设置默认页面
适用场景
- 01按 URL 路径显示不同页面
- 02在 Data App 中添加页面导航
- 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 来跳过非左键点击;请检查你是否将其包裹在另一个会吞掉该事件的组件中。 |