Metabase Data App 从零搭建实战:Scaffold、沙箱内开发验证与 Remote-Sync 发布全流程
2026/9/13 5:05:42 网站建设 项目流程

Metabase Data App 从零搭建实战:Scaffold、沙箱内开发验证与 Remote-Sync 发布全流程

【免费下载链接】metabaseThe easy-to-use open source Business Intelligence and Embedded Analytics tool that lets everyone work with data :bar_chart:项目地址: https://gitcode.com/GitHub_Trending/me/metabase

本文基于 Metabase 仓库中随 Agent 技能一起分发的>import { dataAppConfig } from "@metabase/embedding-sdk-react/data-app-dev/config"; export default dataAppConfig();

  • src/index.tsx 默认导出DataAppFactory,返回{ component, providerProps }
import type { DataAppFactory } from "@metabase/embedding-sdk-react/data-app"; import App from "./App"; import { sdkTheme } from "./theme"; const factory: DataAppFactory = () => ({ component: App, providerProps: { theme: sdkTheme }, }); export default factory;
  • package.json 中的脚本与依赖也说明了构建链路:dev固定跑在 5174 端口,build先执行embedding-sdk-react>APP_DIR="<repo>/data_apps/<slug>" # `<skill-dir>` = 本 SKILL.md 所在目录 # (如 `.claude/skills/metabase-data-app-setup`);模板是其 template/ 子目录。 cp -R "<skill-dir>/template/." "$APP_DIR/"

    data app 是 remote-sync 仓库的子目录,而不是自己的仓库——所以这是一次普通复制,绝不做嵌套的git clone/git init。此后所有命令都在$APP_DIR内执行。

    6. Step 4 — 定制脚手架

    模板就位后(以下都在<repo>/data_apps/<slug>/目录内运行):

    1. package.jsonname为 slug(模板默认是data-app-template,见 package.json)。

    2. @metabase/embedding-sdk-react固定到已发布的>npm install @metabase/embedding-sdk-react@64-alpha

      该 tag 解析为当前内部测试 SDK 构建,提供两个入口点:@metabase/embedding-sdk-react/data-app(应用 API)与@metabase/embedding-sdk-react/data-app-dev/configvite.config.ts使用的 dev/build 预设,负责提供沙箱入口)。不要使用latest64-stable——data apps 尚未正式发布。

    3. 确保仓库根.gitignore忽略.env.local——必须在创建任何凭据文件之前做,这样密钥永远不可能被提交。仓库没有.gitignore就先创建,缺条目就补:

      ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" if [ -z "$ROOT" ]; then echo "MISSING (run this from inside the connected git repo)" else GITIGNORE="$ROOT/.gitignore" # 不存在则创建,并确保 .env.local 被忽略 [ -f "$GITIGNORE" ] || : > "$GITIGNORE" grep -qxF ".env.local" "$GITIGNORE" || echo ".env.local" >> "$GITIGNORE" fi
    4. 在仓库根<repo>/.env.local,通常比应用目录高两级)而不是应用目录里配置 Metabase 凭据——仓库里一份.env.local服务所有 data app。若不存在从示例文件复制,然后不打印文件内容地校验两个变量(文件里可能还有别的密钥)——source之后只回显成败信号:

      # 先解析仓库根;不加保护的 $(git ...) 在仓库外会展开成 "/.env.local" 触碰系统级文件 ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" if [ -z "$ROOT" ]; then echo "MISSING (run this from inside the connected git repo)" else ENV_FILE="$ROOT/.env.local" [ -f "$ENV_FILE" ] || cp .env.local.example "$ENV_FILE" # 在子 shell 里 source,变量不会泄漏到当前环境 ( source "$ENV_FILE" 2>/dev/null [ -n "$DATA_APP_MB_URL" ] && [ "$DATA_APP_MB_URL" != "mb_replace_me" ] && [ -n "$DATA_APP_MB_API_KEY" ] && [ "$DATA_APP_MB_API_KEY" != "mb_replace_me" ] ) && echo "creds present" || echo "MISSING" fi

      若输出MISSING让用户自己<repo>/.env.localDATA_APP_MB_URL(正在运行的 Metabase 实例地址)和DATA_APP_MB_API_KEY(Admin → Authentication → API keys 生成)——事前一次性配好。

      安全红线:绝不要求用户把 API key 粘贴到对话里,也绝不cat/echo/ 打印.env.local或其变量。该文件被 git 忽略且可能含其他密钥——其内容和密钥本身永远不得进入对话或上下文。每个需要 key 的命令都source该文件让 shell 直接使用值,你只能看到creds present/MISSING信号。(creds present只表示两个变量已填且不是默认占位符mb_replace_me不代表URL 或 key 有效;错误的 key 会在后续请求失败时才暴露。)

    5. npm install(或用户偏好的包管理器——模板不随附 lockfile,npm/yarn/pnpm/bun都可以;clone 后若出现既有 lockfile 则沿用)。

    6. 修正应用的.gitignore,让 lockfile 和构建产物都被提交。remote-sync 仓库必须 track 两样东西:

      • lockfile——删掉忽略 lockfile 的整段(# Lockfiles —bun.lockb之间,覆盖package-lock.json/yarn.lock/pnpm-lock.yaml/bun.lock/bun.lockb),让项目提交 lockfile 以保证可复现安装;
      • 构建产物——Metabase 直接从提交的 Git 树中读取data_app.yamlpath声明的文件(模板构建到dist/index.js,即默认path)来服务它,所以该文件必须被提交。若模板的.gitignore忽略了dist/(或你的构建输出目录),删掉该行。

      git status验证——npm install+ 构建之后,生成的 lockfile 与构建产物(path指向的文件)都必须以可提交文件出现。任何一个没有,就说明对应的.gitignore行还在;删掉再查。不要跳过这一步——Agent 曾多次交付没有 lockfile 或 bundle 未同步的项目。

    7. npm run dev,确认 http://localhost:5174 的预览渲染出起步的 "Hello, data app" 消息。

    8. 若预览遇到 CORS,把http://localhost:5174加入 Admin → Embedding → Embedded analytics SDK → CORS。

    9. 编辑data_app.yaml(随模板附在应用目录,模板见 data_app.yaml)。这是 Metabase 在 sync 时读取的每应用配置——每个应用一个文件。为这个应用填好各字段:

      name: Sales App # admin UI 中显示的展示名 description: Pipeline health and quota attainment by region # 可选,见下 path: ./dist/index.js # bundle 路径,相对本应用目录——不改构建输出就保持原样 # allowed_hosts: # 可选——应用可 fetch/XHR 的外部源,见下 # - https://api.example.com # - https://*.internal.acme.com

      把它与构建产物(path指向的文件)一起提交。

      description——可选:一句话说明这个应用做什么,显示在 admin UI 中应用名下方,方便管理员一眼区分。Sync 会把连续空白折叠为单空格,超过 255 字符会被拒绝;admin 列表对剩余文字做换行而非截断,所以一句话效果好、段落会挤爆周围行。用一句关于应用的真实句子替换模板占位文案,或整行删除。

      allowed_hosts——仅当应用要用fetch/XHR直接调用外部API 时才需要。沙箱默认阻断所有网络出站;在此列出源(精确匹配或*.子域通配)会在npm run dev(dev-server CSP)和 Metabase(iframe CSP + 沙箱)两处同时放行。不要把 Metabase 实例列进去——Metabase 数据读走useMetabaseQuerydata hooks、写走useAction(SDK 处理鉴权),永远不要裸fetch。应用只与 Metabase 对话时,allowed_hosts整段省略。

      原生<form action="…">提交与<iframe src="…">/导航同样受该白名单约束。优先用客户端<form onSubmit>preventDefault后通过useAction/fetch写入):原生提交会把被沙箱化的 iframe 导航走。如果确实要用,目标 host 必须在allowed_hosts中(提交对应form-action,嵌入/导航对应frame-src);被导航或嵌入的 host 还必须允许被 frame(X-Frame-Options/frame-ancestors)——很多公开站点不允许。

    7. Step 5 — 验证起步应用

    到这一步,data app 已经存在。保持该工作流聚焦于"创建脚手架 + 证明起步 bundle 能跑":

    1. 运行npm run typecheck

    2. 运行npm run build

    3. 确认git status显示应用源码、lockfile、data_app.yaml与构建产物(默认dist/index.js)均为可提交文件;

    4. 如果用户要求活预览,运行npm run dev并确认起步 "Hello, data app" 界面经沙箱预览正常渲染;

    5. 预览打开时查一次诊断流——它是你(无浏览器者)看到运行时失败的唯一地方(见第 10 节):

      curl -s "http://localhost:5174/__data-app/diagnostics?startEventId=0"

      期望clients: 1且没有任何"alert": true的条目。clients: 0意味着没有预览标签页开着,空 feed 证明不了任何东西。

    用户只要求创建/搭建时,到此为止。如果下一个任务是构建或迭代真实 UI——尤其涉及既有 data app、Metabase 数据、生成的 schema 文件、已保存 question、table、metric、action、过滤器、语义层实体或 data hooks——那是另一个"编辑既有 data app"任务,走 Agent 常规技能发现流程,不要把数据层编写规则并入本 scaffold 技能。

    8. 构建契约:为什么vite.config.ts只有一行

    除非改动确有必要,不要修改src/index.tsxtsconfig.json整个构建/开发设置都藏在 SDK 的dataAppConfig()背后(连 dev HTML shell 也是它提供的——没有index.html可编辑),所以vite.config.ts就是:

    import { dataAppConfig } from "@metabase/embedding-sdk-react/data-app-dev/config"; export default dataAppConfig();

    dataAppConfig只暴露一个受控覆写集合(目前只有port)。整个契约——工厂形状、externals/globals、dev 沙箱入口、CSS 内联、SVG 作为组件支持——都是烤死的、不可覆写;这是有意为之,保证 data app 不会偏离 Metabase 实际加载的东西。没有本地构建配置可动(也没有index.html),而随意改动src/index.tsx有破坏工厂形状的风险,会静默搞坏 drill popup 与路由。

    没有逃生舱给额外的 Vite 插件、alias 或define——port是唯一的旋钮。如果以为需要更多,几乎肯定不需要;去src/里解决。

    每完成一轮有意义的编辑,运行npm run typecheck它跑tsc --noEmit(对应模板 package.json 中的"typecheck": "tsc --noEmit"),覆盖src/vite.config.ts——能抓到与 SDK 类型不符的 prop 形状、坏的重构、缺失导入。Vite dev server做类型检查(只转译),所以本会让生产 CI 失败的错误可能在一趟"看起来正常"的npm run dev会话里悄悄存在。宣告任务完成前先跑。

    交付前复查包卫生@metabase/embedding-sdk-react应使用目标环境预期的>// src/components/CustomerCard.tsx import { StaticQuestion } from "@metabase/embedding-sdk-react"; type Customer = { name: string; questionId: number }; export default function CustomerCard({ customer }: { customer: Customer }) { return ( <article> <h3>{customer.name}</h3> <StaticQuestion questionId={customer.questionId} height={300} width="100%" /> </article> ); }

    9.2 从第一天就分层

    起步应用扩展后的默认布局:

    src/ ├── index.tsx (模板——工厂,别动) ├── App.tsx (只做路由 + 组合) ├── theme.ts ├── pages/ (一屏一文件) │ ├── Overview.tsx │ └── CustomerDetail.tsx ├── components/ (共享 UI) │ └── Card.tsx ├── hooks/ (数据获取包装、自定义 hooks) │ └── useCustomers.ts ├── lib/ (纯工具/派生逻辑) │ └── format.ts └── types/ (共享 TS 类型) └── customer.ts

    Vite 会把从src/index.tsx可达的一切打进单个dist/index.jsIIFE——目录结构纯粹为自己可读性服务。

    多 tab 应用:最左/首个 tab 必须在初始加载时选中。应用绝不应启动在空白页、空壳或"等待点击"状态上。本地状态 tab 直接初始化为第一个:

    const [active, setActive] = useState(TABS[0].id); // 默认 = 最左 tab

    若 tab 由 URL 路由支撑,同样规则通过 router 实现(基础路径/需解析到默认 tab,参见 routing 技能)。无论哪种,都要重新加载应用验证:最左 tab 的内容立即可见且呈现选中态。

    构建产物是一个自包含的.js文件——别无其他。后端只服务单个 bundle,没有 sidecar 文件:CSS 内联进 JS,所有导入资产(图片、字体、以 URL 形式引入的 SVG)base64 内联为 data URI。import logo from "./logo.png"/import iconUrl from "./icon.svg"直接得到可用的>// ✅ 正确 import { StaticQuestion } from "@metabase/embedding-sdk-react"; import { DataAppRouter, DataAppLink, useMetabaseQuery, } from "@metabase/embedding-sdk-react/data-app"; // ❌ 错误——没有 globalThis 模式;那样读到的是空 const { MetabaseProvider, StaticQuestion } = globalThis;

    不要在App.tsx里渲染<MetabaseProvider>dev 入口(SDK dev 预设提供)与生产宿主各自在自己的 realm 里给你的树包上 provider——在 bundle 内再包一层,会把 SDK 的经监听器setState路径送进 Near Membrane 沙箱,静默搞坏 drill popup、插件初始化等。

    9.4 React 也按常规导入

    构建把reactreact-dom外部化,普通导入在两种模式下解析方式相同——生产宿主与 dev 沙箱都以沙箱 globals 形式赋予它们:

    import { useState, useEffect, useMemo } from "react";

    TSX 文件不需要import React from "react"——模板使用自动 JSX runtime(tsconfig.jsonjsx: "react-jsx"dataAppConfig()内置 React 插件)。编译器自动注入所需 JSX-runtime 导入(生产react/jsx-runtime、devreact/jsx-dev-runtime,二者均被外部化并由沙箱赋予)。写 JSX 加命名导入即可。

    React类型ComponentTypeReactNodeRefObject等)用命名类型导入,不用React.命名空间:

    import type { ComponentType, ReactNode } from "react";

    10. 读取诊断流(Reading the diagnostics feed)

    npm run dev把工具栏显示的一切以 JSON 提供——这是看到运行时失败的唯一途径,因为沙箱阻断、CSP 拒绝、失败的查询与未捕获错误都不会出现在终端,也不会被npm run typecheck抓到。任何无法通过读代码验证的改动之后都要查。

    循环:编辑前记下nextEventId→ 做改动(自动重建)→ 重读。startEventId含边界、且跨页面刷新存活。

    curl -s "http://localhost:5174/__data-app/diagnostics?startEventId=0"
    { "entries": [{ "eventId": 31, "kind": "blocked-network", "alert": true, "summary": "Blocked fetch to api.example.com (not in allowed_hosts)", "detail": null, // 有时的 stack frames "hint": "Add https://api.example.com to allowed_hosts in data_app.yaml …", "buildId": 7 // 报告该事件的 bundle 代次 }], "clients": 1, // 已连接的预览标签页——0 表示什么都没跑 "buildId": 7, // 当前运行中的代次 "staleEntries": 164, // 被扣住、由旧构建报告的条目,见下 "nextEventId": 32 // 下次作为 ?startEventId= 传回 }
    • clients: 0不代表健康——它意味着没有预览标签页开着,空entries证明不了什么。先打开http://localhost:5174
    • feed 回答的是"正在运行的那个 bundle",而非一切历史。每次保存都会重建并重挂载,多步编辑会经过编译不过或渲染不了的构建——那些错误描述的是预览已替换掉的代码,它们被扣住、计入staleEntries;编辑中途读只看到当前构建的失败而不是一堆旧账。没有任何丢失:重建会从头重跑应用,仍坏的东西会在新buildId下再报告一次;对比构建时才用?includeStale=true取回被扣住的条目。构建失败不会推进buildId——预览继续跑最后一个好 bundle,其条目保持最新。

    kind分诊(永远先读hint——它指明精确修法;汇报时不得弱化summary):

    kind修法
    blocked-network/csp-violation把源加入data_app.yamlallowed_hosts,然后重启npm run dev(白名单 + CSP 在启动时读取)
    blocked-api生产同样被阻断——改用 SDK API 或删掉调用,不要绕过
    sdk-call+alert: true一个 Metabase 请求失败;修查询——summary含端点与状态码
    error真 bug;detail里有 stack

    文本会被截断,缓冲区保留最近 200 个事件。curl -X DELETE .../__data-app/diagnostics为所有读者清空它。

    11. 主题规则(Theme rules)

    MetabaseProvidertheme(定义在src/theme.ts,模板见 src/theme.ts)是改变 SDK 组件外观的唯一途径——它不是给 bundle 自身 chrome 用的样式表。

    字段用途备注
    colors.brandSDK 控件强调色用应用主色
    colors.brand-hovercolors.brand-hover-lightSDK 控件 hover/强调背景用与 hover 文字对比的微表面色;不要用与brand同饱和度的颜色
    colors.charts图表调色板字符串数组。只用鲜艳的品牌色阶;绝不用浅色淡彩——调色板 index 1+ 可能渲染文字标签,白底上浅色不可见
    colors.positive/colors.negative语义指示色
    colors.backgroundcolors.background-secondarySDK 组件表面必须与 SDK 组件的直接父容器一致:图表在白色卡片里就用"white";在有底色页面上就用该底色
    colors.text-primarytext-secondarytext-tertiarySDK 表面上的文字必须与background对比:白底用#1f2937一类深色。SDK 默认text-primary解析为近白——白表面下不设就是白色隐形文字。覆写background时务必成对设置text-primary
    fontFamilySDK 控件字体

    hover 色是对比对:若设了colors.text-hover,在打开的菜单、图表类型选择器、可视化设置下拉中验证它与colors.brand-hover/colors.brand-hover-light的对比。蓝色brand#EAF4FF这类浅色 hover 表面,而非品牌蓝本身。

    主题只样式化 SDK 控件——绝不用于你自己 UI 的样式。页面背景、页头、卡片包装器用内联style={{ background: "#f5f5f7" }}或 CSS modules 处理自己的元素。

    不要指望按子树主题化。页面上多个<MetabaseProvider>会争夺唯一的 CSS 变量槽。外观需要随状态变化时,在 App 层重算sdkTheme再整树重渲染——全树重新主题化。

    12. 自定义错误 UI(可选)

    当某个内嵌 question/dashboard 加载不了(questionId被删、用户无权限、请求失败),宿主会在该组件位置渲染一个中性的内置错误态(弱化图标 + SDK 消息)。零配置;除非应用有强烈理由重样式化,否则保持默认。

    要匹配应用外观,就在工厂的providerProps里(与theme同一个对象)返回自己的errorComponent。它是普通的providerProps键,不是工厂形状变更——安全可加:

    // src/components/AppError.tsx import type { SdkErrorComponentProps } from "@metabase/embedding-sdk-react"; export default function AppError({ message }: SdkErrorComponentProps) { return ( <div style={{ padding: 16, textAlign: "center", color: "#6b7280" }}> {message} </div> ); }
    // src/index.tsx —— 与 `theme` 并排加一个键 import AppError from "./components/AppError"; const factory: DataAppFactory = () => ({ component: App, providerProps: { theme: sdkTheme, errorComponent: AppError }, });

    规则:

    • 它替换的是应用中所有SDK 组件的错误 UI,不只是 not-found——文案保持通用。渲染message(或你自己的措辞),不要预设是哪类错误;
    • messageReactNode——原样渲染,不要.toString()或下标访问;
    • 保持为小型展示型组件。它渲染在宿主的 provider 内部,但按叶子 UI 对待——不用 data hooks、不用useMetabaseQuery
    • 想保留中性默认就整个省略errorComponent

    13. SDK 能力面与被禁 API

    bundle 从"react"导入 hooks/JSX,从@metabase/embedding-sdk-react导入 SDK 组件,从@metabase/embedding-sdk-react/data-app导入><div style={{ height: 360 }}> <StaticQuestion questionId={1} height="100%" width="100%" withChartTypeSelector={false} /> </div>

    不要用 hover 时裁剪或位移的容器包裹InteractiveQuestion/StaticQuestion。避开overflow: hidden、hover transform 与 hover 驱动布局位移——popover、菜单与图表 tooltip 需要稳定几何与可见溢出。

    16. 同步到 Metabase:提交即发布

    Data apps 经 Git 交付而非上传——你提交应用目录,Metabase 在下次 remote-sync 导入时拉取。

    1. npm run build→ 产出data_app.yamlpath处的 bundle(模板构建到dist/index.js);

    2. 在仓库根提交应用目录——data_app.yaml、构建产物(path指向的文件)、源码与 lockfile——并push

      git add data_apps/<slug> git commit -m "Add <slug> data app" git push
    3. 应用在下次 remote-sync 导入后出现在 Metabase——手动Pull changes(Admin → Data apps / Remote sync)、自动导入轮询或重启——地址/apps/<slug>

    不要主动"部署"应用,也不要问 bundle 怎么到 staging 环境——没有独立部署步骤,这个问题只会让用户困惑:Metabase 在下次 sync 时从连接的仓库直接导入已提交的 bundle。变更一旦到达 Metabase 同步的分支(合并 PR 或直接 push,由用户决定),只需告诉他们拉取并在 Metabase 打开/apps/<slug>

    • 更新:提交新构建,再次拉取;
    • 删除:从仓库删除应用目录并 push——下次 sync 即移除。UI 上删不掉 repo 管理的应用;Data apps admin 页的Remove动作只在断开仓库连接后才出现,用于清理遗留应用。

    17. 常见坑速查表

    症状修法
    "Failed to fetch the user, the session might be invalid."API key 或 CORS 错误——用仓库根.env.local里的凭据请求$DATA_APP_MB_URL/api/user/current验证(source 文件、x-api-key头、只回显成败),并在 SDK CORS origins 加http://localhost:5174
    图表标签不可见主题里设text-primary(见第 11 节)
    图表与实例其他图表风格不一、无视主题、无 tooltip/格式化/drill-through它是 React 手搓的——改用StaticQuestion/InteractiveQuestion+visualization(见 13.2)
    图表溢出容器给 SDK 组件传height/width(见第 15 节)
    应用背景中途结束、短内容下方裸白根元素给minHeight: 100vh(见第 14 节)
    运行时 "Invalid hook call"两份 React。dataAppConfig()外部化react——确认react/react-dom已安装且没有第二份或版本不匹配
    bundle 达数 MBReact/SDK 本应被契约插件外部化——确认vite.config.ts仍用dataAppConfig()且装了固定 tag 的 contenteditable="false">【免费下载链接】metabaseThe easy-to-use open source Business Intelligence and Embedded Analytics tool that lets everyone work with data :bar_chart:项目地址: https://gitcode.com/GitHub_Trending/me/metabase

    创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

    需要专业的网站建设服务?

    联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

    立即咨询