关于 jsapi-filters
jsapi-filters 是用于为嵌入式 Domo 内容添加客户端筛选的技能,面向开发宿主页面交互的开发者,可即时更新筛选而无需重新加载嵌入内容。
主要功能
- 01通过 MessagePort 筛选嵌入内容
- 02广播筛选条件或指定 iframe
- 03订阅钻取和筛选事件
- 04向 App Studio 发送上下文数据
- 05处理应用就绪和框架尺寸事件
适用场景
- 01将下拉菜单或日期选择器连接到 Domo 嵌入内容
- 02同步多个嵌入内容的筛选条件
- 03将钻取筛选条件应用到另一个嵌入内容
- 04向 App Studio 传递用户或客户上下文
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ stahura/ domo-ai-vibe-rules --skill jsapi-filters安装后在 Claude Code 中输入/jsapi-filters确认已加载。
附带文件references/rpc-methods.md
详细介绍
Domo 客户端筛选器(JS API)
在嵌入内容加载后,从宿主页面动态筛选已嵌入的 Domo 内容。与服务端程序化筛选器(固化在嵌入令牌中)不同,客户端筛选器可即时更新,无需重新获取令牌。
适用场景:UI 驱动的筛选(下拉框、日期选择器)、下钻响应、在多个嵌入内容之间同步筛选器、向 App Studio 应用传递上下文。服务端筛选器请参阅 programmatic-filters。
工作原理
Domo iframe 通过 MessagePort(JSON-RPC 2.0)与宿主页面通信:
- Domo iframe 发送一个带有
MessagePort和referenceId的message事件 - 宿主页面捕获该端口并监听消息
- 宿主通过该端口发送 RPC 调用(筛选器);Domo 则回传事件(下钻、筛选器变更)
设置
第 1 步:初始化 JS API 监听器
设置一个全局监听器,以捕获每个 iframe 的 MessagePort 握手。
// lib/jsapi.ts
const ports: Record<string, MessagePort> = {};
export interface DomoRpcMessage extends MessageEvent {
data: {
referenceId?: string;
method?: string;
params?: any;
result?: any;
error?: any;
id?: string;
jsonrpc?: string;
};
}
export const initializeJsApi = () => {
if (typeof window === 'undefined') return;
const handleMessage = (e: MessageEvent) => {
if (!e.ports || !e.ports[0]) return;
const referenceId = e.data?.referenceId;
if (!referenceId) return;
const port = e.ports[0];
port.start();
// Store the port for sending commands later
ports[referenceId] = port;
// Listen for events from this iframe
port.onmessage = (event: MessageEvent) => {
const data = event.data;
if (!data) return;
if (data.method) {
handleRpcMethod(referenceId, data.method, data.params);
}
if (data.error) {
console.error(`[JsApi] RPC error (ref: ${referenceId}):`, data.error);
}
};
};
window.addEventListener('message', handleMessage);
return () => {
window.removeEventListener('message', handleMessage);
};
};
第 2 步:尽早挂载初始化器
在任何 Domo iframe 加载之前挂载(应用根节点)。React/Next.js:
// components/DomoJsApiInitializer.tsx
'use client'
import { useEffect } from 'react'
import { initializeJsApi } from '@/lib/jsapi'
export default function DomoJsApiInitializer() {
useEffect(() => {
const cleanup = initializeJsApi()
return cleanup
}, [])
return null
}
然后将其包含在根布局中:
// app/layout.tsx
import DomoJsApiInitializer from './components/DomoJsApiInitializer'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<DomoJsApiInitializer />
{children}
</body>
</html>
)
}
第 3 步:配置嵌入授权域名
在 Domo 中将你的宿主域名添加到 Admin > Network Security > Embed Authorized Domains。若不这样做,iframe 将不会发送握手 message 事件。对于自定义 Domo 应用,还需添加到 Custom Apps Authorized Domains。
第 4 步:使用嵌入令牌(而非公开 URL)
JS API 仅适用于经过身份验证的嵌入(基于令牌的 POST 流程)。公开嵌入 URL 不会发起 MessagePort 握手。令牌流程请参阅 programmatic-filters。
应用筛选器
通过已存储的 MessagePort 发送 /v1/filters/apply。
筛选器结构
客户端筛选器使用 operand(不是服务端筛选器所用的 operator —— 这是最常见的 bug):
interface ClientFilter {
column: string;
operand: string; // 'IN', 'NOT_IN', 'EQUALS', 'BETWEEN', 'CONTAINS', etc.
values: any[];
dataType?: string; // 'STRING' | 'NUMBER' | 'DATE' — disambiguates values
dataSourceId?: string; // target a specific dataset in multi-dataset embeds
}
applyFilters 函数
export const applyFilters = (
filters: Array<{ column: string; operand: string; values: any[] }> = []
) => {
Object.values(ports).forEach(port => {
port.postMessage({
id: `filter-${Date.now()}`,
jsonrpc: '2.0',
method: '/v1/filters/apply',
params: {
filters
}
});
});
};
这会将筛选器广播到所有已连接的 Domo iframe。若要针对特定的 iframe,请使用 referenceId:
export const applyFiltersToEmbed = (
referenceId: string,
filters: Array<{ column: string; operand: string; values: any[] }> = []
) => {
const port = ports[referenceId];
if (!port) {
console.warn(`[JsApi] No port found for referenceId: ${referenceId}`);
return;
}
port.postMessage({
id: `filter-${Date.now()}`,
jsonrpc: '2.0',
method: '/v1/filters/apply',
params: { filters }
});
};
示例
按单个值筛选:
applyFilters([
{ column: 'Region', operand: 'IN', values: ['West'] }
]);
多个筛选器(以 AND 组合):
applyFilters([
{ column: 'Region', operand: 'IN', values: ['West', 'East'] },
{ column: 'Year', operand: 'EQUALS', values: [2025] }
]);
清除所有筛选器:
applyFilters([]);
React 示例 —— 筛选器下拉框
'use client'
import { useState } from 'react'
import { applyFilters } from '@/lib/jsapi'
const regions = ['All', 'West', 'East', 'North', 'South']
export default function RegionFilter() {
const [selected, setSelected] = useState('All')
const handleChange = (region: string) => {
setSelected(region)
if (region === 'All') {
applyFilters([])
} else {
applyFilters([
{ column: 'Region', operand: 'IN', values: [region] }
])
}
}
return (
<select value={selected} onChange={(e) => handleChange(e.target.value)}>
{regions.map(r => (
<option key={r} value={r}>{r}</option>
))}
</select>
)
}
处理来自 Domo 的事件
Domo 通过 MessagePort 回传事件。使用订阅/取消订阅模式来清晰地管理监听器:
// Event subscription pattern
type EventCallback = (referenceId: string, params: any) => void;
const listeners: Record<string, Set<EventCallback>> = {};
export function on(event: string, callback: EventCallback): () => void {
if (!listeners[event]) listeners[event] = new Set();
listeners[event].add(callback);
return () => { listeners[event].delete(callback); };
}
然后在你的 handleRpcMethod 中分派到已注册的监听器。用一个集中的 switch 来处理它们:
function handleRpcMethod(referenceId: string, method: string, params: any) {
switch (method) {
case '/v1/onDrill':
handleDrill(referenceId, params);
break;
case '/v1/onFiltersChange':
handleFiltersChange(referenceId, params);
break;
case '/v1/onAppReady':
handleAppReady(referenceId, params);
break;
case '/v1/onAppData':
handleAppData(referenceId, params);
break;
case '/v1/onFrameSizeChange':
handleFrameResize(referenceId, params);
break;
default:
break;
}
}
/v1/onDrill —— 下钻事件
当用户点击可下钻元素时触发。包含表示下钻路径的筛选器:
function handleDrill(referenceId: string, params: any) {
const drillFilters = params?.filters;
// drillFilters: [{ column: 'Region', operand: 'IN', values: ['West'] }]
// Example: apply the drill filters to another embed on the page
applyFiltersToEmbed('other-embed-ref', drillFilters);
// Example: navigate to a detail page
router.push(`/details?region=${drillFilters[0]?.values[0]}`);
}
/v1/onFiltersChange —— 筛选器变更事件
当嵌入内容内部的筛选器发生变化时触发(用户交互、卡片到卡片,或程序化变更)。用于同步宿主 UI:
function handleFiltersChange(referenceId: string, params: any) {
const currentFilters = params?.filters;
// currentFilters: [{ column: 'Region', operand: 'IN', values: ['West'] }]
// Example: update your app's filter UI to reflect what's selected in Domo
setActiveFilters(currentFilters);
// Example: sync filters to another embed
applyFiltersToEmbed('second-dashboard-ref', currentFilters);
}
/v1/onAppReady —— App Studio 就绪
当 App Studio 应用完成加载时触发。在使用 overrideFilters=true 时至关重要 —— 只有在此事件之后才能发送筛选器:
function handleAppReady(referenceId: string, params: any) {
// Now safe to send filters and appData
applyFiltersToEmbed(referenceId, initialFilters);
sendAppData(referenceId, { customerId: '12345' });
}
/v1/onAppData —— 来自 Domo 的应用数据
当 App Studio 应用将数据回传给宿主时触发:
function handleAppData(referenceId: string, params: any) {
const appData = params?.appData;
// Handle data sent from the App Studio app
console.log('App data received:', appData);
}
/v1/onFrameSizeChange —— 框架尺寸变化
当嵌入内容调整大小时触发。用于调整 iframe 容器:
function handleFrameResize(referenceId: string, params: any) {
const { width, height } = params || {};
const iframe = document.querySelector(`iframe[name="${referenceId}"]`);
if (iframe && height) {
(iframe as HTMLIFrameElement).style.height = `${height}px`;
}
}
向 App Studio 应用发送数据
通过 /v1/appData/apply 向 App Studio 应用传递任意上下文:
export const sendAppData = (referenceId: string, appData: any) => {
const port = ports[referenceId];
if (!port) return;
port.postMessage({
id: `appData-${Date.now()}`,
jsonrpc: '2.0',
method: '/v1/appData/apply',
params: { appData }
});
};
嵌入的应用通过 domo.onAppData() 接收此数据。用于传递用户 ID、客户上下文或偏好设置。
PFilter URL 参数
一种无 JS 的替代方案:将筛选器作为 URL 查询参数附加。取舍:会触发 iframe 完全刷新,且筛选器在 URL 中可见/可修改。
结构
?pfilters=[{"column":"ColumnName","operand":"OPERATOR","values":["val1","val2"]}]
示例
https://example.domo.com/embed/pages/private/ABCDE?pfilters=[{"column":"Region","operand":"IN","values":["West","East"]},{"column":"Year","operand":"EQUALS","values":["2025"],"dataType":"NUMBER"}]
可选字段
dataSourceId—— 定位到特定的数据集dataType——STRING、NUMBER、DATE
何时使用 PFilters 与 JS API
| PFilters | JS API | |
|---|---|---|
| 所需设置 | 无 | MessagePort 初始化 |
| 速度 | 较慢(iframe 刷新) | 即时(无需重新加载) |
| 安全性 | 在 URL 中可见,用户可修改 | 在 DOM 中可见,用户可修改 |
| 多次更新 | 每次更改都会刷新 iframe | 就地更新 |
| 最适用于 | 简单的一次性筛选、链接分享 | 交互式筛选、动态 UI |
安全警告: PFilters 在 URL 中可见,最终用户可对其进行修改。它们不提供任何安全性——请使用服务端程序化筛选器来实现数据隔离。
用于 App Studio 的 overrideFilters
overrideFilters=true URL 参数会告知 App Studio 应用忽略已保存的默认值,并等待宿主提供的筛选器:
https://example.domo.com/embed/pages/private/ABCDE?overrideFilters=true
行为:
overrideFilters=true— 应用在无筛选器的情况下加载,等待宿主发送筛选器overrideFilters=false或省略 — 应用正常加载其已保存的默认值
重要: 你必须至少发送一次 /v1/filters/apply(即使是空的 []),否则应用会一直处于等待状态。请先监听 /v1/onAppReady。
function handleAppReady(referenceId: string, params: any) {
// App is ready — send the initial filter state
applyFiltersToEmbed(referenceId, userFilters.length > 0 ? userFilters : []);
}
用于嵌入内容的其他有用 URL 参数:
| 参数 | 用途 |
|---|---|
transparentBackground=true | 使仪表板背景透明 |
appData=value | 向 App Studio 应用传递初始上下文(空格用 +) |
viewId=abc123 | 指定要显示哪个 App Studio 页面视图 |
已知 RPC 方法
宿主 → Domo(命令)
| 方法 | 描述 |
|---|---|
/v1/filters/apply | 应用或清除运行时筛选器 |
/v1/appData/apply | 向 App Studio 应用发送上下文数据 |
Domo → 宿主(事件)
| 方法 | 描述 |
|---|---|
/v1/onDrill | 用户深入钻取到某个数据点 |
/v1/onFiltersChange | 嵌入内容内部的筛选器发生变化 |
/v1/onAppReady | App Studio 应用加载完成,已准备好接收输入 |
/v1/onAppData | App Studio 应用将数据发送回宿主 |
/v1/onFrameSizeChange | 嵌入内容更改了尺寸 |
与服务端筛选器结合使用
- 服务端: 安全边界。由 Domo 强制执行,用户无法绕过。用于数据隔离。
- 客户端: UX 便利。用户可更改。用于交互式筛选。
客户端筛选器会在服务端允许的数据范围内进一步缩小范围——它们无法覆盖服务端的限制。
注意事项与最佳实践
端口可用性时机
在握手完成之前调用 applyFilters 会静默失败。有两种方法:
仪表板/卡片: 在连接时排队并刷新:
const pendingFilters: Record<string, Array<{ column: string; operand: string; values: any[] }>> = {};
// When storing the port after handshake, flush pending filters
ports[referenceId] = port;
if (pendingFilters[referenceId]) {
applyFiltersToEmbed(referenceId, pendingFilters[referenceId]);
delete pendingFilters[referenceId];
}
App Studio 应用: 先监听 /v1/onAppReady,然后再发送筛选器。在 overrideFilters=true 时这是必需的。
operand 与 operator
客户端使用 operand,服务端使用 operator。将两者混淆会导致静默失败。
referenceId 映射
referenceId 由 Domo iframe 分配——你无法控制它。对于多个嵌入内容,请将握手时机与渲染顺序关联起来。对于单个嵌入内容,请使用广播式 applyFilters()。
Domo API 中的运算符拼写错误
GREATER_THAN_EQUALS_TO 有时会显示为 GREAT_THAN_EQUALS_TO(缺少 ER)。如果比较运算符失败,请尝试两种拼写。
多个 Iframe
每个 iframe 都有自己的端口。applyFilters 会向所有端口广播;如需独立筛选,请使用带有特定 referenceId 的 applyFiltersToEmbed。
公开嵌入不支持 JS API
只有经过身份验证(基于令牌)的嵌入才会发起 MessagePort 握手。公开嵌入 URL 无法使用。
支持的 operand
IN、NOT_IN、EQUALS、NOT_EQUALS、GREATER_THAN、GREATER_THAN_EQUALS_TO、LESS_THAN、LESS_THAN_EQUALS_TO、BETWEEN(正好两个值)、CONTAINS(部分字符串匹配)。
客户端不支持 SQL 筛选器
/v1/filters/apply 仅支持标准筛选器对象。SQL 筛选器仅限服务端使用。
安全
客户端过滤器和 pfilters 仅用于 UX —— 对最终用户可见且可修改。请使用服务端编程式过滤器进行数据隔离。
TypeScript 类型定义
type ClientFilterOperand =
| 'IN'
| 'NOT_IN'
| 'EQUALS'
| 'NOT_EQUALS'
| 'GREATER_THAN'
| 'GREATER_THAN_EQUALS_TO'
| 'LESS_THAN'
| 'LESS_THAN_EQUALS_TO'
| 'BETWEEN'
| 'CONTAINS';
interface ClientFilter {
column: string;
operand: ClientFilterOperand;
values: (string | number | boolean)[];
dataType?: 'STRING' | 'NUMBER' | 'DATE';
dataSourceId?: string;
}
interface DomoRpcRequest {
id: string;
jsonrpc: '2.0';
method: string;
params: Record<string, any>;
}
interface DomoRpcEvent {
method: string;
params?: Record<string, any>;
result?: any;
error?: { code: number; message: string };
}
interface DrillEventParams {
filters: ClientFilter[];
}
interface FiltersChangeParams {
filters: ClientFilter[];
}
interface FrameSizeChangeParams {
width: number;
height: number;
}
interface AppDataParams {
appData: any;
}
快速参考
阅读 references/rpc-methods.md 以获取完整的 RPC 方法参考和消息格式详情。