metabase-data-app-routing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAdd routing to a data-app
为 data-app 添加路由
A Metabase data-app bundle doesn't bundle a router library — it imports three small primitives from :
@metabase/embedding-sdk-react/data-app| API | Purpose |
|---|---|
| Wrap the app once. Tracks the current sub-path. Auto-detects the iframe's URL prefix; bundle author writes no basename. |
| Internal navigation link. Renders a real |
| Returns |
That's the entire surface. No of any version, no , no . The API is deliberately decoupled from any router library so a future Metabase version can swap the underlying implementation without touching bundle code.
react-router<BrowserRouter><HashRouter>Metabase data-app 包不内置路由库——它从 导入三个轻量原语:
@metabase/embedding-sdk-react/data-app| API | 用途 |
|---|---|
| 在应用外层包裹一次。跟踪当前子路径。自动检测 iframe 的 URL 前缀;包开发者无需编写 basename。 |
| 内部导航链接。渲染为真实的 |
| 返回 |
以上就是全部 API 表面。没有任何版本的 ,没有 ,也没有 。 该 API 特意与所有路由库解耦,以便未来 Metabase 版本可以替换底层实现,而无需修改包的代码。
react-router<BrowserRouter><HashRouter>When to use this skill
何时使用此技能
- The user has a working data-app project — scaffolded from the repo, so a one-liner
data-app-template(vite.config.ts) and andataAppConfig()that exports a factory already exist. The dev preview has no in-project entry: the SDK's dev preset serves it.src/index.tsx - The user wants the bundle to render different content at different URLs (,
/overview)./customers/:id - Do not use this skill to scaffold a project from scratch — it only patches an existing data-app project. If there is no project yet, stop and tell the user to start with a new data-app scaffold before adding routing.
The template already externalizes in . You do NOT need to edit to add routing — just edit (and add more component files as needed) per the step below.
@metabase/embedding-sdk-react/data-appvite.config.tsvite.config.tssrc/App.tsx- 用户已有一个可运行的 data-app 项目——基于 仓库搭建,因此已存在一行式的
data-app-template(vite.config.ts)和导出工厂函数的dataAppConfig()。开发预览没有项目内的入口:由 SDK 的开发预设提供服务。src/index.tsx - 用户希望包在不同 URL(、
/overview)下渲染不同内容。/customers/:id - 不要使用此技能 从零搭建项目——它仅用于修补现有 data-app 项目。如果还没有项目,请停止并告知用户先使用新的 data-app 脚手架创建项目,再添加路由。
模板已经在 中将 设为外部依赖。你无需编辑 来添加路由——只需按照以下步骤编辑 (并根据需要添加更多组件文件)即可。
vite.config.ts@metabase/embedding-sdk-react/data-appvite.config.tssrc/App.tsxStep 1 — Wrap App.tsx
with <DataAppRouter>
App.tsx<DataAppRouter>步骤 1 —— 用 <DataAppRouter>
包裹 App.tsx
<DataAppRouter>App.tsxImport the routing primitives normally. does NOT take a prop — it auto-detects the iframe's URL prefix () in production and resolves to no prefix in the Vite dev preview.
<DataAppRouter>basename/embed/apps/<name>App.tsx<MetabaseProvider>DataAppDevProviderDataAppProvidertsx
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>
);
}Run , click the links, watch the URL bar change. Reload at and the dev preview lands on the customer route directly.
yarn devhttp://localhost:5174/customers/42正常导入路由原语。 不需要 传入 属性——它在生产环境会自动检测 iframe 的 URL 前缀(),在 Vite 开发预览中则解析为无前缀。
<DataAppRouter>basename/embed/apps/<name>App.tsx<MetabaseProvider>DataAppDevProviderDataAppProvidertsx
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 devhttp://localhost:5174/customers/42Always preselect the default (leftmost) tab on load
加载时始终预选默认(最左侧)标签页
If the app presents multiple tabs (or any top-level page switcher), the base path MUST render the default — leftmost / first — tab's content, never a blank page, a "Not found", or an empty shell. This is the single most common mistake: the app boots at , no branch matches, and the user sees nothing until they click a tab. Don't rely on the user (or a later navigation) to select the first tab — the default tab is the initial state.
//Two equivalent ways to guarantee it, depending on whether tabs are route-backed:
-
Route-backed tabs — givean explicit branch that renders the first tab, so an unmatched/base path resolves to it:
/tsxconst 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(); } -
Local-state tabs (no routing) — initialize the active-tab state to the first tab, so the very first render shows it:tsx
const [active, setActive] = useState(TABS[0].id); // default = leftmost tab
Verify by reloading the app at its base path () with a fresh load: the leftmost tab's content must be visible immediately, and that tab must read as selected in the tab bar.
/如果应用呈现多个标签页(或任何顶层页面切换器),基础路径 必须渲染默认的——最左侧/第一个——标签页的内容,绝不能是空白页、“未找到”或空壳。 这是最常见的错误:应用在 路径启动时,没有分支匹配,用户什么都看不到,直到点击某个标签页。不要依赖用户(或后续导航)来选择第一个标签页——默认标签页就是初始状态。
//有两种等效的保证方式,取决于标签页是否基于路由:
-
基于路由的标签页 —— 给添加一个显式分支来渲染第一个标签页,这样未匹配的路径/基础路径都会回退到它:
/tsxconst 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(); } -
本地状态标签页(无路由) —— 将活动标签页状态初始化为第一个标签页,这样首次渲染就会显示它:tsx
const [active, setActive] = useState(TABS[0].id); // default = leftmost tab
通过在基础路径()重新加载应用来验证:最左侧标签页的内容必须立即可见,并且该标签页在标签栏中应显示为选中状态。
/How navigation translates to the parent URL
导航如何映射到父级 URL
You don't need to do anything for this. For context:
- Bundle calls → host's
<DataAppLink to="/customers/42">runsnavigatewithpushState./embed/apps/<name>/customers/42 - The parent's observes the iframe URL change and mirrors it to the parent's URL bar as
AppView— the/apps/<name>/customers/42prefix is stripped because the parent's React Router route is/embed, not/apps/:name/*./embed/... - Reload works because the BE serves the same for every
data-app.htmlsub-path; the iframe boots,/embed/apps/:name/*reads<DataAppRouter>, auto-detects the prefix, and starts at the right sub-path.window.location.pathname
你不需要为此做任何操作。供参考:
- 包调用 → 宿主的
<DataAppLink to="/customers/42">会执行navigate,路径为pushState。/embed/apps/<name>/customers/42 - 父级的 监听 iframe 的 URL 变化,并将其同步到父级的地址栏,显示为
AppView——/apps/<name>/customers/42前缀被移除,因为父级的 React Router 路由是/embed,而非/apps/:name/*。/embed/... - 刷新功能之所以有效,是因为后端会为所有 子路径提供相同的
/embed/apps/:name/*;iframe 启动后,data-app.html读取<DataAppRouter>,自动检测前缀,并从正确的子路径开始。window.location.pathname
What NOT to do
禁止操作
- Don't add (or any router library) to the project. The bundle doesn't need it.
react-router-dom - Don't use or
<BrowserRouter>inside the bundle. They run their own<HashRouter>flow that hits the Near Membrane batching bug.setState - Don't call directly from inside the bundle. Use
window.history.pushStateor<DataAppLink>fromnavigate.useDataAppLocation - Don't try to read or write the parent's URL. The iframe's sandbox attribute blocks it by design.
- 不要添加 (或任何路由库)到项目中。包不需要它。
react-router-dom - 不要在包内使用 或
<BrowserRouter>。它们自身的<HashRouter>流程会触发 Near Membrane 批处理 bug。setState - 不要在包内直接调用 。请使用
window.history.pushState或<DataAppLink>返回的useDataAppLocation。navigate - 不要尝试读取或写入父级的 URL。iframe 的 sandbox 属性会按设计阻止此操作。
Common patterns
常见模式
Conditional render based on path
基于路径的条件渲染
tsx
const { pathname } = useDataAppLocation();
if (pathname === "/") return <Home />;
if (pathname.startsWith("/customers/")) return <Customer />;
return <NotFound />;tsx
const { pathname } = useDataAppLocation();
if (pathname === "/") return <Home />;
if (pathname.startsWith("/customers/")) return <Customer />;
return <NotFound />;Programmatic navigation (e.g. from a <button>
)
<button>编程式导航(例如从 <button>
触发)
<button>tsx
function OpenCustomerButton({ id }: { id: number }) {
const { navigate } = useDataAppLocation();
return <button onClick={() => navigate(`/customers/${id}`)}>Open #{id}</button>;
}tsx
function OpenCustomerButton({ id }: { id: number }) {
const { navigate } = useDataAppLocation();
return <button onClick={() => navigate(`/customers/${id}`)}>Open #{id}</button>;
}Parsing path params manually
手动解析路径参数
There's no built-in . Match yourself:
useParamstsx
const { pathname } = useDataAppLocation();
const match = pathname.match(/^\/customers\/(\d+)$/);
if (match) {
const id = Number(match[1]);
// ...
}If you find yourself doing this often, factor it into your own helper inside the bundle:
tsx
function useCustomerIdFromPath(): number | null {
const { pathname } = useDataAppLocation();
const m = pathname.match(/^\/customers\/(\d+)$/);
return m ? Number(m[1]) : null;
}没有内置的 。请自行匹配:
useParamstsx
const { pathname } = useDataAppLocation();
const match = pathname.match(/^\/customers\/(\d+)$/);
if (match) {
const id = Number(match[1]);
// ...
}如果你经常需要这样做,可以在包内封装成自己的辅助函数:
tsx
function useCustomerIdFromPath(): number | null {
const { pathname } = useDataAppLocation();
const m = pathname.match(/^\/customers\/(\d+)$/);
return m ? Number(m[1]) : null;
}Troubleshooting
故障排查
| Symptom | Fix |
|---|---|
| The bundle hasn't finished loading and the fallback path is showing. The provider wrap is supplied automatically (the SDK dev preview's |
| URL changes in dev preview but the production iframe shows the bundle re-render itself on every navigation (or routes don't work in prod at all). | |
| URL changes but UI doesn't. | A |
Reload at a deep URL in dev ( | Vite's dev server is serving the route as a 404 instead of falling back to |
Middle-click on a | The link uses |
| 症状 | 修复方法 |
|---|---|
| 包尚未完成加载,当前显示的是回退路径。Provider 包裹是自动提供的(SDK 开发预览的 |
| 开发预览中 URL 会变化,但生产环境 iframe 中每次导航都会导致包重新渲染(或路由在生产环境完全不工作)。 | |
| URL 变化但 UI 没有更新。 | 组件树中仍有 |
在开发环境的深层 URL( | Vite 的开发服务器将该路由视为 404,而没有回退到 |
中键点击 | 该链接使用 |