自定义小部件

Prev Next

使用 HTML、CSS 和 JavaScript 在 Tulip 中构建您自己的小部件

Who can use this feature

Users on Professional plans and above.

Requirement to use Custom Widget

Please allow Outgoing access to Custom Widgets. See Networking requirements for a Tulip Cloud Deployment to learn more.* *.tulip-custom-widgets.com

您拖放至应用中的元素称为小部件。这些可以是按钮、图片、输入框,以及工具栏中提供的任何其他元素。

自定义小部件允许您编写自己的代码并创建专属小部件,随后可将其拖放至应用程序中并进行交互。这一功能使 Tulip 用户能够突破平台功能的局限。

请观看以下视频,了解该功能的概述:

请在 Tulip 库中查看可用的自定义控件。

自定义小部件基础知识

您可以在账户设置页面中找到自定义控件管理界面。在此处,您可以创建和删除控件。自定义控件编辑器允许您编写控件代码、创建属性(Props)和事件(Events),并预览控件。下图说明了属性与事件的概念:

**属性(Props):**属性是自定义小部件与 Tulip 应用程序之间共享的数据。属性将在应用程序编辑器中显示,并允许应用程序编辑者选择要与该属性关联的变量、表记录或其他信息。

**事件:**事件是小部件可以发送给应用程序的信号。事件允许应用程序通过触发器做出响应,并可随事件携带相关信息。

可通过应用编辑器中的“自定义”下拉选择器访问自定义小部件。

创建小部件

Permission Required

Only users with access to account settings (i.e. Account Owners) or users with specific permission to access the custom widget page can use the custom widget editor.

小部件编辑器界面分为四个部分。在左下角区域编写的代码,一旦点击预览区域,就会显示在预览区域中。为了测试目的,可以在预览区域直接修改属性值。

提供用于与 Props 和事件交互的特殊函数。

获取 Props 的值

Warning

It is not guaranteed that the prop being used by a widget will load before that widget loads, so all props associated with Table record fields, Aggregations, or any other dynamic value should include logic to support cases where they are null at the point the widget is loaded. See this section for details on triggering logic when a prop changes.

//获取 Prop 的值 getValue('My Prop');``//将 Prop 存储到变量中 let myVariable = getValue('My Prop');

在 Prop 值发生变化时执行操作

// 每当 props 值发生变化时执行操作getValue('My Prop', (internalVariable) => { doSomething(internalVariable);});

设置 Props 的值


//将文本属性值设置为 'hello!'setValue('My Text Prop', 'hello!');

//设置对象中某个属性的值setValue('My Object Prop', { 'Key inside object': 'new value' });```

### 触发事件

```**触发事件**fireEvent('event',*payload*);

// 触发不带有效载荷的事件 fireEvent('My Event');

//触发带有效载荷的事件 fireEvent('My Event', myVariable);```

下图展示了应用程序中自定义组件的信息流。输入变量 data 被存储到 prop 中。值的变化会触发一个事件,随后触发器将数据存储到输出变量中。

![Custom Widget App Setup Diagram](https://cdn.document360.io/7c6ff534-cad3-4fc8-9583-912c4016362f/Images/Documentation/Custom%20Widget%20App%20Setup%20Diagram.jpg){height="" width=""}

## 组件构建演示

![Video](https://www.youtube.com/embed/H08-W2M2Ypo?rel=0){height="576" width="1024"}

## 应用导入/导出

请参阅[“导入与导出”](/r230/docs/import-export)以了解有关此功能的更多信息。

### 导出

1. 在自定义小部件概览页面中,选择要导出的小部件旁边的**三个点菜单**。

![](https://cdn.document360.io/7c6ff534-cad3-4fc8-9583-912c4016362f/Images/Documentation/Custom%20Widgets%20Overview_477703991.png){height="" width=""}

1. 选择**“导出”**。

![](https://cdn.document360.io/7c6ff534-cad3-4fc8-9583-912c4016362f/Images/Documentation/Custom%20Widgets%20Overview_477704053.png){height="" width="800"}

### 导入

1. 在自定义小部件概览页面中,点击顶部的**三个点菜单**。

![](https://cdn.document360.io/7c6ff534-cad3-4fc8-9583-912c4016362f/Images/Documentation/Custom%20Widgets%20Overview_477704175.png){height="" width="800"}

1. 选择**“导入”**。

![](https://cdn.document360.io/7c6ff534-cad3-4fc8-9583-912c4016362f/Images/Documentation/Custom%20Widgets%20Overview_477704228.png){height="" width="350"}

1. 找到 .json 格式的自定义小部件文件。

## 启用外部库

外部库进一步扩展了自定义小部件的功能。外部库负责处理在 JavaScript 中直接编写 HTML 的手动工作。对于您希望使用外部库的每个小部件,都必须启用该功能。

1. 在小部件界面中,点击页面右上角的**三个点菜单**。

![](https://cdn.document360.io/7c6ff534-cad3-4fc8-9583-912c4016362f/Images/Documentation/Custom%20Widgets%20Overview_477684182.png){height="" width=""}

1. 选择**“启用外部库**”。

![](https://cdn.document360.io/7c6ff534-cad3-4fc8-9583-912c4016362f/Images/Documentation/Custom%20Widgets%20Overview_477684247.png){height="" width="250"}

1. 选择您要启用的**扩展**程序。

![Screenshot 2025-02-06 at 9.13.14 AM](https://cdn.document360.io/7c6ff534-cad3-4fc8-9583-912c4016362f/Images/Documentation/Screenshot%202025-02-06%20at%209.13.14%20AM.png){height="" width=""}

以下是各扩展功能的基本说明:

* **jQuery**——可更流畅地选择 HTML 元素,并支持元素操作。
* **D3 -**数据可视化的黄金标准,虽然学习曲线较陡,但灵活性极高。
* **Google Visualization -**顾名思义,是一款出色的可视化工具。
* **Lodash -**Lodash 提供了大量工具,使处理 JavaScript 数据类型变得更加轻松。
* **ChartJS -**可能是最简单的图表库之一,且拥有海量的在线资源。
* **Moment -**Moment 提供了大量用于处理日期和时间的工具。

## 运行时版本

新运行时中自定义小部件的加载方式已更新(参见[“自定义小部件的改进”](/r230/docs/custom-widget-improvements))。此变更会影响您何时能够可靠地访问小部件的属性,例如通过`getValue()` 获取的属性。

要查看您的控件使用的是哪个版本的运行时:

* 在**“自定义控件”列表**页面或编辑器中,如果控件名称旁边带有**“Legacy”**标记,则该控件使用旧版运行时;如果没有该标记,则该控件使用新版运行时。
* 在“自定义小部件”信息侧边栏中,查看**“运行时**”部分。
* 在自定义小部件的**“权限设置”**中,如果**“自定义小部件运行时**”开关处于开启状态,则该小部件正在使用新运行时。

:::(Info) (NOTE)
By default, newly created Custom Widgets use the old runtime.
:::

若要切换自定义小部件的运行时版本,请在**“权限设置”**中使用**“自定义小部件运行时**”开关。![Custom widget runtime toggle](https://cdn.document360.io/7c6ff534-cad3-4fc8-9583-912c4016362f/Images/Documentation/Custom%20widget%20runtime%20toggle.png){height="" width=""}

## 示例

以下是[Tulip](https://tulip.co/library/)库中的几个自定义小部件,它们展示了上述功能特性:

### 简单计时器小部件

[简单计时器小部件](https://library.tulip.co/apps/simple-timer-widget)

一款易于使用的计时器小部件,用于跟踪应用工作流中的经过时间。操作员可直接在应用界面中启动、停止和重置计时器,非常适合用于计时任务或流程。

![Screenshot 2025-07-02 at 15.33.20.png](https://cdn.document360.io/7c6ff534-cad3-4fc8-9583-912c4016362f/Images/Documentation/Screenshot%202025-07-02%20at%2015.33.20.png){height="" width=""}

### 径向仪表小部件

[径向仪表小部件](https://library.tulip.co/apps/radial-gauge-widget)

该控件通过可自定义的径向仪表直观地呈现数值。它非常适合以快速、易于理解的格式显示机器速度、温度或进度等指标。

![Screenshot 2025-07-02 at 15.34.22.png](https://cdn.document360.io/7c6ff534-cad3-4fc8-9583-912c4016362f/Images/Documentation/Screenshot%202025-07-02%20at%2015.34.22.png){height="" width=""}

### 时间轴组件

[时间轴组件](https://library.tulip.co/apps/timeline-widget)

在水平时间轴上显示事件或步骤,帮助用户一目了然地直观了解流程阶段、已完成任务或基于时间的数据。

![Screenshot 2025-07-02 at 15.34.59.png](https://cdn.document360.io/7c6ff534-cad3-4fc8-9583-912c4016362f/Images/Documentation/Screenshot%202025-07-02%20at%2015.34.59.png){height="" width=""}

## 最佳实践

### 1. 防止加载时运行的小部件代码产生意外副作用

Tulip 会在小部件每次渲染时执行其实际代码——不仅在播放器中,还包括自定义小部件编辑器、应用编辑器以及频道的实时预览中。这使您能够获得小部件的准确实时预览。 脚本顶层中的任何会产生副作用的代码(例如 API 调用、数据表写入、通知)都会在小部件每次渲染时执行,包括小部件在编辑器中重新打开或预览页面重新加载的每次情况。

请将任何会产生实际影响的操作,通过显式触发器(例如按钮点击、响应用户操作的`fireEvent`,或 Prop 变化)进行控制,而非在加载时自动触发。

```// 正确示例:仅在应用显式请求时触发 getValue('TriggerIncrement', (shouldIncrement) => { if (shouldIncrement) { fetch("https://api.example.com/increment-counter", { method: "POST" }); }});

// 错误示例:每次小部件加载时都会触发,包括在编辑器和预览模式下fetch("https://api.example.com/increment-counter", { method: "POST" });```如果小部件需要自动加载数据,请使用只读或幂等端点,以避免重复加载导致意外后果。

### 2. 进行迭代测试,避免触发真实的副作用

在开发会调用具有实际副作用的生产环境 API 的小部件时,请将其指向测试或预发布端点,直到迭代完成,然后在发布前切换到生产端点。这可以防止因编辑和预览小部件而导致的重复记录、通知或其他副作用。

对于发布后仍需在生产环境中执行的操作,请在组件自身的用户界面中集成确认机制,而非依赖加载时机。通过要求执行可见操作(例如点击按钮),组件查看者可以自主控制效果何时生效。

```// 在实际操作触发前需要明确点击,// 无论小部件本身已渲染多少次document.getElementById('submitBtn')?.addEventListener('click', () => { fetch("https://api.example.com/submit-order", { method: "POST" });});

3. 属性管理——验证属性值

确保您的自定义组件能可靠地处理数据。props 是向组件传递信息的一种方式,但其值可能会发生变化,或者初始时为 undefined。尽管 props 有固定的类型,但它们初始时可能是 undefined,这可能会导致自定义组件代码中出现错误。遵循良好的模式可以防止错误,并使您的组件更加健壮。

下面的示例将回调函数作为 getValue 的第二个参数。每当自定义组件接收到更新的 prop 值时,该回调函数都会被调用。

由于 props 的初始值可能是未定义的,而后续可能会收到更新后的值,因此您需要确保在收到更新值后立即应用配置。若不使用回调函数直接调用 getValue,则会返回代码执行该时刻 props 的值。

getValue('config', function(config) { if (config) { applyConfiguration(config); }});

不建议使用以下示例,因为 prop 的初始值可能是 undefined:const config = getValue('config');applyConfiguration(config);

4. 事件处理——数据验证

事件数据具有特定类型。虽然触发数据类型不正确的事件不会导致程序崩溃或中断自定义组件代码的执行,但建议您添加自定义检查机制,以确保通过 fireEvent 发送的数据经过验证且类型正确。

此做法可防止出现意外行为,并使代码更具可预测性。

if (data != null && typeof data === 'string') { fireEvent(eventName, data);} else { // 可选:处理无效数据的情况。}

5. 使用回调函数进行异步属性访问

调用setValue() 时,属性更新是异步进行的。这意味着更新不会立即生效,因为您的自定义组件是通过 iframe 与主应用程序进行通信的。setValue()请求虽然已发送,但您的代码会继续执行,而不会等待更改生效。 如果你尝试紧接着使用getValue(),很可能获取到的是旧值。

**为了获取最新数据,请务必在调用getValue()时配合使用回调函数。**回调函数仅在属性成功更新后才会执行,这可确保您处理的是最新信息。

以下示例展示了这一最佳实践:```// 正确做法:异步访问属性值const newConfig = { setting: 'value' };setValue('config', newConfig);

// 属性更新完成后,回调函数将执行。getValue('config', function(updatedConfig) { console.log('属性已更新。新配置为:', updatedConfig); // 使用已确认的最新值执行操作 applyConfiguration(updatedConfig);});```

以下示例展示了应避免的做法:```// 错误:这可能无法按预期工作const newConfig = { setting: 'value' };setValue('config', newConfig);

// 这很可能输出旧值,而非新值。const currentConfig = getValue('config'); ```

6. 在 API 调用中使用受支持的头部

在自定义控件中进行外部 API 调用时,请谨慎处理 HTTP 头,以避免 CORS(跨源资源共享)问题。自定义控件在具有特定安全限制的 iframe 环境中运行。

**请仅使用明确支持的请求头。**设置不支持的请求头(如 Content-Type 或其他自定义请求头)可能会触发 CORS 错误,导致 API 调用无法正常工作。

以下示例展示了这一最佳实践:// 正确示例:不包含自定义标头的简单 API 调用fetch("https://akhatri.bulb.cloud/api/v3/w/1/tables", { headers: { Authorization: "<API key>", },}) .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.error("Error:", error));

以下示例展示了应避免的做法:// 错误示例:设置 Content-Type 头部会导致 CORS 问题fetch("https://api.example.com/data", { headers: { Authorization: "<API 密钥>", "Content-Type": "application/json", // 这将导致 CORS 错误 },})

Troubleshoot CORS error

If you encounter CORS errors, first remove any custom headers you've set (especially Content-Type) and test with minimal headers. The iframe security model restricts which headers can be set from custom widgets.

7. 根据运行时初始化小部件

有关更多信息,请参阅“运行时版本”。

新运行时

在新运行时中,src属性返回一个空的 HTML 壳文件。该壳文件加载后,会向父应用发送一条消息。父应用随后将自定义组件代码、辅助函数(如getValue())以及初始 prop 值发送过去,以便注入到 iframe 中。

因此,当空壳加载时(即在小部件代码被注入之前),DOMContentLoaded和window.onload事件会触发。附加到这些事件上的任何处理程序注册都为时已晚:事件已经触发,因此回调永远不会执行。这些处理程序中的代码会被静默跳过,如下例所示:

document.addEventListener('DOMContentLoaded', function() { // 在 SHELL 加载时触发,但用户代码尚未注入 // 用户代码甚至还没被注入! const myProp = getValue('myProp'); // 错误:getValue 未定义});

在custom widgets 中,您无需使用DOMContentLoaded或window.onload。Tulip按以下顺序加载资源:第三方库、用户HTML,然后是JavaScript。当您的JavaScript运行时,DOM已经就绪。您可以直接访问DOM元素,无需等待加载事件,如下例所示:

// 直接调用是安全的——执行时 DOM 已加载完毕。const btn = document.getElementById('my-btn');

旧版运行时

在旧运行时中,srcDoc属性会将自定义小部件代码注入 iframe,同时还会注入getValue()等辅助函数以及初始 prop 值。由于小部件代码从一开始就是文档的一部分,因此只要 iframe 内容加载完成,getValue()和您的 props 即可立即可用。因此,在旧运行时中,DOMContentLoaded和window.onload会按预期工作,如下例所示:

document.addEventListener('DOMContentLoaded', function() { // 当用户的 HTML 准备就绪时触发 // 属性已可用 const myProp = getValue('myProp'); // 立即生效});