4、Widget核心API详解:self上下文、settings、datasources、timewindow、state等核心对象

各位同学,欢迎来到第四章。

前面几章我们搭好了开发环境,也跑通了第一个Hello World组件。但说实话,那只是皮毛。真正要写出能用的、能上生产环境的组件,你必须搞懂ThingsBoard暴露给你的那几个核心对象。

这一章,我们就来啃这块硬骨头。我会把self上下文、settingsdatasourcestimewindowstate这几个东西掰开揉碎了讲。嗯,都是我在实际项目中踩过坑的地方。

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状态和临时数据。

下一章,我们会深入onInitonDataUpdatedonDestroy这些生命周期钩子。到时候你会发现,理解了这些核心对象,生命周期函数用起来就顺手多了。

好了,这一章就到这儿。有什么问题,咱们课程群里聊。