mcpField
J
jsapi-filters

jsapi-filters

github.com/stahura开发24193 次安装26 周前更新
代码仓库

关于 jsapi-filters

jsapi-filters 是用于为嵌入式 Domo 内容添加客户端筛选的技能,面向开发宿主页面交互的开发者,可即时更新筛选而无需重新加载嵌入内容。

主要功能

  1. 01通过 MessagePort 筛选嵌入内容
  2. 02广播筛选条件或指定 iframe
  3. 03订阅钻取和筛选事件
  4. 04向 App Studio 发送上下文数据
  5. 05处理应用就绪和框架尺寸事件

适用场景

  1. 01将下拉菜单或日期选择器连接到 Domo 嵌入内容
  2. 02同步多个嵌入内容的筛选条件
  3. 03将钻取筛选条件应用到另一个嵌入内容
  4. 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)与宿主页面通信:

  1. Domo iframe 发送一个带有 MessagePort 和 referenceId 的 message 事件
  2. 宿主页面捕获该端口并监听消息
  3. 宿主通过该端口发送 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
PFiltersJS 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/onAppReadyApp Studio 应用加载完成,已准备好接收输入
/v1/onAppDataApp 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 方法参考和消息格式详情。

评论

支持 Markdown