4、Widget核心API详解:self上下文、settings、datasources、timewindow、state等核心对象
各位同学,欢迎来到第四章。
前面几章我们搭好了开发环境,也跑通了第一个Hello World组件。但说实话,那只是皮毛。真正要写出能用的、能上生产环境的组件,你必须搞懂ThingsBoard暴露给你的那几个核心对象。
这一章,我们就来啃这块硬骨头。我会把self上下文、settings、datasources、timewindow、state这几个东西掰开揉碎了讲。嗯,都是我在实际项目中踩过坑的地方。
4.1 self上下文:组件的灵魂
先问大家一个问题:你在组件里写的self到底是什么?
说白了,self就是当前组件实例的引用。它挂载了几乎所有你能用到的API。我个人习惯把它叫做「组件的大管家」。
核心要点:self是组件与ThingsBoard引擎通信的唯一桥梁。没有它,你的组件就是个孤岛。
来看看self上最常用的几个属性:
| 属性名 | 类型 | 说明 |
|---|---|---|
| self.settings | Object | 用户在配置面板里填的参数 |
| self.datasources | Array | 数据源配置列表 |
| self.timewindow | Object | 当前选中的时间窗口 |
| self.state | Object | 组件内部状态管理 |
| self.ctx | Object | 渲染上下文,包含canvas、容器等信息 |
| self.$scope | Object | AngularJS的作用域(老版本) |
我在项目中遇到过一个问题:有个同事在onInit里直接用了this,结果发现拿不到数据。为什么?因为this指向的是当前函数的作用域,而不是组件实例。记住,在ThingsBoard组件里,永远用self,别用this。
4.2 settings:配置面板的秘密
settings是什么?就是你在组件配置面板里填的那些参数。温度报警组件的阈值、图表组件的颜色、表格组件的列宽……全都存在这里。
它的结构完全由你定义的settingsSchema决定。举个例子:
// settingsSchema 定义
{
"schema": {
"type": "object",
"properties": {
"alarmThreshold": {
"type": "number",
"title": "报警阈值",
"default": 80
},
"unit": {
"type": "string",
"title": "单位",
"default": "℃"
},
"showLabel": {
"type": "boolean",
"title": "显示标签",
"default": true
}
}
}
}
// 在组件中读取
const threshold = self.settings.alarmThreshold;
const unit = self.settings.unit || '℃';
小技巧:我建议你在读取settings时,永远给一个默认值。因为用户可能没填,或者配置面板还没加载完。用||操作符兜底,能避免很多诡异的undefined错误。
嗯,这里要注意一点:settings是只读的。你不能在运行时修改它。如果你想保存一些临时状态,请用state。
4.3 datasources:数据从哪里来
这是整个组件开发里最核心的部分。没有数据,组件就是个空壳子。
self.datasources是一个数组,每个元素代表一个数据源。每个数据源包含:
- type:实体类型(设备、资产、租户等)
- entityId:实体的唯一ID
- entityName:实体名称(方便调试)
- dataKeys:你要订阅的键列表(温度、湿度、电压等)
来看看实际代码中怎么用:
self.onInit = function() {
// 遍历所有数据源
self.datasources.forEach(function(ds) {
console.log('数据源实体:', ds.entityName);
// 遍历数据键
ds.dataKeys.forEach(function(key) {
console.log('订阅的键:', key.name, '类型:', key.type);
// 订阅数据更新
self.ctx.subscription.subscribe(
{ datasource: ds, dataKey: key },
function(data) {
// 每次有新数据,这里会被调用
updateChart(data);
}
);
});
});
};
我曾经犯过一个错误:在onInit里直接读取self.datasources[0].dataKeys[0].latestValue。结果发现有时候有值,有时候是undefined。为什么?因为数据是异步加载的。正确的做法是在订阅回调里拿数据,或者等onDataUpdated事件触发后再读取。
避坑指南:千万不要在onInit里假设数据已经到位。数据加载是异步的,你需要监听onDataUpdated事件,或者使用订阅机制。
4.4 timewindow:时间窗口的玩法
时间窗口决定了你的组件看的是哪一段数据。是最近1小时?还是昨天全天?还是自定义时间段?
self.timewindow的结构大概是这样的:
{
"realtime": {
"interval": 1000, // 刷新间隔(毫秒)
"timewindowMs": 3600000 // 时间窗口大小(毫秒),这里是1小时
},
"history": {
"fixedTimewindow": {
"startTime": 1700000000000,
"endTime": 1700086400000
}
}
}
有两种模式:
- 实时模式(realtime):数据会不断刷新,适合仪表盘、实时监控
- 历史模式(history):固定时间段,适合报表分析
我个人习惯在组件初始化时判断当前模式:
self.onInit = function() {
if (self.timewindow.realtime) {
// 实时模式,设置定时刷新
startRealtimeUpdates();
} else if (self.timewindow.history) {
// 历史模式,一次性加载
loadHistoryData();
}
};
你想想看,如果不做这个判断,你的组件在实时模式下可能不会自动刷新,或者历史模式下还在不断请求新数据——这都是性能杀手。
4.5 state:组件的记忆
state是ThingsBoard 3.2之后引入的。它解决了组件内部状态管理的痛点。
以前我们怎么做?用全局变量。但全局变量有个问题:组件销毁时,变量还在内存里。而且多个组件实例之间会互相干扰。
state是组件实例级别的,生命周期和组件绑定。组件销毁,state自动清理。
// 设置状态
self.state.set('currentPage', 1);
self.state.set('selectedRow', { id: 123, name: '传感器A' });
// 读取状态
const page = self.state.get('currentPage');
const row = self.state.get('selectedRow');
// 监听状态变化
self.state.on('currentPage', function(newVal, oldVal) {
console.log('页码变了:', oldVal, '->', newVal);
refreshTable();
});
实用技巧:我建议你把所有需要持久化的UI状态都存到state里。比如表格的排序字段、分页页码、折叠面板的展开状态。这样即使用户切换页面再回来,状态也能恢复。
4.6 综合实战:一个带配置的温度仪表
光说不练假把式。我们来写一个完整的例子,把上面讲的知识点串起来。
这个组件要做的事:
- 从数据源读取温度值
- 根据settings里的阈值判断是否报警
- 支持实时和历史两种时间模式
- 用state保存报警历史
self.onInit = function() {
const threshold = self.settings.alarmThreshold || 80;
const unit = self.settings.unit || '℃';
// 初始化报警历史
if (!self.state.get('alarmHistory')) {
self.state.set('alarmHistory', []);
}
// 判断时间模式
if (self.timewindow.realtime) {
// 实时模式:订阅数据更新
self.ctx.subscription.subscribe(
{ datasource: self.datasources[0], dataKey: self.datasources[0].dataKeys[0] },
function(data) {
const temp = data[0].value;
updateDisplay(temp, unit);
if (temp > threshold) {
triggerAlarm(temp, threshold);
}
}
);
} else {
// 历史模式:一次性加载
self.ctx.subscription.subscribe(
{ datasource: self.datasources[0], dataKey: self.datasources[0].dataKeys[0] },
function(data) {
const values = data.map(d => d.value);
const maxTemp = Math.max(...values);
updateHistoryChart(values, unit);
if (maxTemp > threshold) {
self.state.get('alarmHistory').push({
time: new Date(),
value: maxTemp,
threshold: threshold
});
}
}
);
}
};
function triggerAlarm(temp, threshold) {
// 记录报警
const history = self.state.get('alarmHistory');
history.push({
time: new Date(),
value: temp,
threshold: threshold
});
self.state.set('alarmHistory', history);
// 显示报警
showAlarmIndicator(temp - threshold);
}
这个例子虽然简单,但涵盖了settings的读取、datasources的订阅、timewindow的模式判断、state的状态管理。你把这个吃透了,大部分组件的开发逻辑就都通了。
4.7 本章小结
来,我们捋一捋:
- self:组件的根对象,所有API的入口。记住,永远用
self,别用this。 - settings:用户配置,只读。读取时记得给默认值。
- datasources:数据源配置,异步加载。别在
onInit里直接读数据。 - timewindow:时间窗口,分实时和历史两种模式。一定要做模式判断。
- state:组件内部状态,生命周期与组件绑定。适合存UI状态和临时数据。
下一章,我们会深入onInit、onDataUpdated、onDestroy这些生命周期钩子。到时候你会发现,理解了这些核心对象,生命周期函数用起来就顺手多了。
好了,这一章就到这儿。有什么问题,咱们课程群里聊。