- 物联网
- 后端
- 数据可视化
- 消息队列
【免费下载链接】thingsboard
All-in-one IoT Platform - Device management, data collection, processing and visualization.
本文以 ThingsBoard 前端仓库(ui-ngx)内置的官方示例 timeseries_widget.md 为核心,手把手讲解如何在Widgets Bundle 视图中从零创建一个自定义Time-Series 时序部件:通过 HTML/CSS/JavaScript 三个代码区实现"每个数据源一个页签、页签内以表格展示最新时序数据"的效果,并深入解析部件subscription(订阅)对象与defaultSubscription的数据结构,帮助读者掌握 ThingsBoard 自定义部件的完整开发与调试能力。
一、前置认知:Widget Editor 与 Time-Series 部件类型
ThingsBoard 的自定义部件开发都在Widget Editor(部件编辑器)中完成,它允许开发者用 HTML、CSS、JavaScript(Angular 模板语法)定义部件的外观与行为。部件类型决定订阅的数据结构:
- Latest values(最新值)与Time-Series(时序)部件:订阅对象提供
datasources(数据源数组)与data(最新/时序数据数组); - Alarm(告警)部件:提供
alarmSource与alarms; - RPC / Static部件:订阅对象可选,通常不包含必要的数据信息。
官方对部件生命周期函数与订阅对象的完整说明见 widget_js_fn.md 与 widget_js_subscription_object.md。本示例聚焦Time-Series类型,其最终效果为:多个数据源以mat-tab页签切换,每个页签内渲染一张"时间戳 + 各数据键(dataKey)数值"的表格。
二、创建步骤:从 Widgets Bundle 进入 Time-Series 模板
- 打开Widgets Bundle(部件库)视图,点击屏幕右下角的大"+"按钮;
- 在弹出的菜单中点击"Create new widget type"(创建新部件类型);
- 在Select widget type(选择部件类型)弹窗中点击Time-Series按钮;
- Widget Editor随即打开,并自动填充默认的Time-Series模板部件内容(此时"Resources"区已包含 CSS、HTML 两个标签页,"JavaScript"区已包含默认部件骨架代码)。
接下来只需分别替换三处代码并点击Run即可看到运行结果。
三、替换 CSS:定义表格表头左对齐样式
将 "Resources" 区CSS标签页的内容替换为:
.my-data-table th { text-align: left; }该样式为示例表格定义了my-data-table类,使表头(th)文本左对齐,避免默认居中样式影响阅读。
四、替换 HTML:以页签 + 表格渲染多数据源时序数据
将 "Resources" 区HTML标签页的内容替换为:
<mat-tab-group style="height: 100%;"> <mat-tab *ngFor="let datasource of datasources; let $dsIndex = index" label="{{datasource.name}}"> <table class="my-data-table" style="width: 100%;"> <thead> <tr> <th>Timestamp</th> <th *ngFor="let dataKeyData of datasourceData[$dsIndex]">{{dataKeyData.dataKey.label}}</th> <tr> </thead> <tbody> <tr *ngFor="let data of datasourceData[$dsIndex][0].data; let $dataIndex = index"> <td>{{data[0] | date : 'yyyy-MM-dd HH:mm:ss'}}</td> <td *ngFor="let dataKeyData of datasourceData[$dsIndex]">{{dataKeyData.data[$dataIndex] && dataKeyData.data[$dataIndex][1]}}</td> </tr> </tbody> </table> </mat-tab> </mat-tab-group>该模板使用 Angular 结构型指令*ngFor(官方参考 Angular NgForOf API):
- 外层
*ngFor="let datasource of datasources; let $dsIndex = index":遍历当前订阅的所有数据源,为每个数据源渲染一个mat-tab页签,页签标题为datasource.name; - 表头
*ngFor="let dataKeyData of datasourceData[$dsIndex]":按数据源索引读取$scope.datasourceData,将每个dataKeyData的dataKey.label渲染为列标题(首列固定为 "Timestamp"); - 表体
*ngFor="let data of datasourceData[$dsIndex][0].data":以第一个 dataKey 的data数组为行基准,data[0]为毫秒级 Unix 时间戳(通过 Angulardate管道格式化为yyyy-MM-dd HH:mm:ss),dataKeyData.data[$dataIndex][1]为对应时间点的数值; - 最后一行使用Angular 表达式求值短路(
A && B):当某 dataKey 在对应索引处无数据点时返回空,避免渲染undefined。
五、替换 JavaScript:初始化订阅数据并响应数据更新
将JavaScript区的内容替换为:
self.onInit = function() { self.ctx.widgetTitle = 'My first Time-Series widget'; self.ctx.$scope.datasources = self.ctx.defaultSubscription.datasources; self.ctx.$scope.data = self.ctx.defaultSubscription.data; self.ctx.$scope.datasourceData = []; var currentDatasource = null; var currentDatasourceIndex = -1; for (var i=0;i<self.ctx.$scope.data.length;i++) { var dataKeyData = self.ctx.$scope.data[i]; if (dataKeyData.datasource != currentDatasource) { currentDatasource = dataKeyData.datasource currentDatasourceIndex++; self.ctx.$scope.datasourceData[currentDatasourceIndex] = []; } self.ctx.$scope.datasourceData[currentDatasourceIndex].push(dataKeyData); } self.ctx.updateWidgetParams(); } self.onDataUpdated = function() { self.ctx.detectChanges(); }代码要点:
self是部件实例的内置引用,每个部件函数都应定义为self的属性(详见 widget_js_fn.md);self.ctx是 WidgetContext 类型引用,提供部件所需的全部 API 与数据;onInit在部件初始化时调用:将defaultSubscription.datasources(数据源数组)与defaultSubscription.data(各 dataKey 的时序数据数组)挂载到$scope,使其在 HTML 模板中可直接访问;$scope.datasourceData是本示例引入的关键重组结构:由于data是按datasource+dataKey平铺的(见下文订阅结构),这里遍历data,每当dataKeyData.datasource发生变化就开启一个新的数据源索引,把属于同一数据源的所有dataKeyData收集到datasourceData[索引]数组中——HTML 中便可以通过datasourceData[$dsIndex]按数据源索引灵活取用;updateWidgetParams()同步部件参数(底层实现见下节源码解析);onDataUpdated在订阅收到新数据时被调用,这里仅执行detectChanges(),触发一次新的 Angular 变更检测,使表格中的数据及时刷新。
六、运行与预览
完成上述替换后,点击Widget Editor Toolbar上的Run按钮,即可在Widget preview(部件预览)区看到运行结果:
预览中可以看到:每个数据源对应一个页签,表格首列展示格式化的时间戳,其余列按 dataKey 的 label 展示各数据键的数值。
七、源码级解析:subscription 订阅对象与 defaultSubscription
示例中的核心数据来源是self.ctx.defaultSubscription,它是 IWidgetSubscription 的实例(源码定义于 widget-api.models.ts),包含该订阅的全部信息与当前数据。在 widget-component.models.ts 中,WidgetContext声明了defaultSubscription: IWidgetSubscription = null,部件代码中可随时读取。
7.1 datasources:数据源数组
datasources = [ { // datasource type: 'entity',// type of the datasource. Can be "function" or "entity" name: 'name', // name of the datasource (in case of "entity" usually Entity name) aliasName: 'aliasName', // name of the alias used to resolve this particular datasource Entity entityName: 'entityName', // name of the Entity used as datasource entityType: 'DEVICE', // datasource Entity type (for ex. "DEVICE", "ASSET", "TENANT", etc.) entityId: '943b8cd0-576a-11e7-824c-0b1cb331ec92', // entity identificator presented as string uuid. dataKeys: [ // array of keys (attributes or timeseries) of the entity used to fetch data { // dataKey name: 'name', // the name of the particular entity attribute/timeseries type: 'timeseries', // type of the dataKey. Can be "timeseries", "attribute" or "function" label: 'Sin', // label of the dataKey. Used as display value (for ex. in the widget legend section) color: '#ffffff', // color of the key. Can be used by widget to set color of the key data. funcBody: "", // only applicable for datasource with type "function" and "function" key type. settings: {} // dataKey specific settings with structure according to the defined Data key settings form. }, //... ] }, //... ]type为function时代表模拟数据源,配合funcBody生成仿真数据;为entity时代表真实实体数据源,aliasName指向仪表板中配置的实体别名。
7.2 data:各 dataKey 的时序数据点
data = [ { datasource: {}, // datasource object of this data. See datasource structure above. dataKey: {}, // dataKey for which the data is held. See dataKey structure above. data: [ // array of data points [ // data point 1498150092317, // unix timestamp of datapoint in milliseconds 1, // value, can be either string, numeric or boolean ], //... ] }, //... ]data的每个元素(DatasourceData)对应一个数据源 + 一个 dataKey的组合,数据点以[时间戳, 数值]二元数组表示。这也解释了示例 JavaScript 为什么要按dataKeyData.datasource归组:data是平铺结构,而 HTML 表格需要按数据源聚合,datasourceData正是为这种按数据源索引访问而引入的重组视图。
7.3 IWidgetSubscription 接口中的相关成员
在 IWidgetSubscription 接口中可看到与示例直接对应的成员:
datasources?: Array<Datasource>—— 订阅使用的数据源数组;data?: Array<DatasourceData>—— 订阅范围内收到的数据;latestData?: Array<DatasourceData>—— 最新值数据(Latest values 部件使用);type: widgetType、callbacks: WidgetSubscriptionCallbacks等成员共同支撑订阅的生命周期与回调(如onDataUpdated对应接口中的WidgetSubscriptionCallbacks.onDataUpdated)。
7.4 WidgetContext 的 detectChanges 与 updateWidgetParams 底层实现
示例中调用的两个 API 在 widget-component.models.ts 中有明确实现:
- detectChanges():在
ngZone内调用changeDetectorValue?.detectChanges()触发 Angular 变更检测(带updateWidgetParams可选参数,为true时先同步部件参数)。这正是示例注释中所说的"收到新数据时必须执行新的变更检测周期"的底层原因; - updateWidgetParams():通过
setTimeout(..., 0)异步调用dashboardWidget.updateWidgetParams()同步部件参数。
八、调试技巧与扩展方向
官方文档 widget_js_fn.md 还给出了部件代码的两类调试方法:
- Web 控制台输出:在部件 JavaScript 任意位置放置
console.log(...),点击Run重启部件代码,在浏览器控制台观察输出; - 浏览器调试器:在感兴趣的位置放置
debugger;语句并点击Run,浏览器会自动在断点处暂停,可用调试工具逐步分析执行过程。
基于本示例的datasourceData重组模式,可以进一步扩展:
- 把
data[0]的毫秒时间戳配合date管道展示更丰富的日期格式; - 参照 latest_values_widget.md 使用
latestData实现最新值部件; - 为表格列标题绑定
dataKey.color实现图例着色; - 借鉴 widget_js_fn.md 中
onResize、onDestroy、getSettingsForm、typeParameters、actionSources等生命周期函数,为部件增加设置表单与用户动作。
至此,一个可运行的多数据源 Time-Series 表格部件已完成创建。整个示例的全部代码与说明均可在仓库 ui-ngx/src/assets/help/en_US/widget/editor/examples/timeseries_widget.md 中查看与复现。
- 物联网
- 后端
- 数据可视化
- 消息队列
【免费下载链接】thingsboard
All-in-one IoT Platform - Device management, data collection, processing and visualization.
相关推荐
Neon 怎么把 Postgres 上游小版本合并进 neon postgres 并更新仓库引用
Neon 怎么把 Postgres 上游小版本合并进 neon postgres 并更新仓库引用 Neon 把 Postgres 以 fork(neondata
物联网后端数据可视化消息队列ThingsBoard Alarm Widget 从零到实战:基于 Widget Editor 开发自定义告警表格组件
ThingsBoard Alarm Widget 从零到实战:基于 Widget Editor 开发自定义告警表格组件 本指南以 ThingsBoard 官方示
物联网后端数据可视化消息队列ThingsBoard 静态 Widget 开发实战:从 Widget Editor 创建无数据源的自定义 UI 组件
ThingsBoard 静态 Widget 开发实战:从 Widget Editor 创建无数据源的自定义 UI 组件 导读 静态 Widget(Static
物联网后端数据可视化消息队列
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考