跳到主要内容

编写自定义组件

自定义组件由一个 React 组件和一小段描述它的纯数据组成。你使用 @devicechain/widget-sdk 编写这两部分,将它们放进 npm 包发布,并在显示仪表板的应用中注册该包。内置组件也通过完全相同的接口声明,因此自定义组件可获得相同的数据绑定、选项校验和错误处理。

你的组件接收的是已解析好的数据。它不负责认证、解析数据源、轮询,也不建立遥测连接。这些都由宿主完成,并把一份快照交给你的组件。

仓库中的 examples/widget-tank-level 是一个可直接复制的完整包。它在单体仓库之外,基于已打包的 SDK tarball 构建。

安装 SDK​

npm install @devicechain/widget-sdk @devicechain/dashboards react

在你的组件包中,将 @devicechain/widget-sdk、@devicechain/dashboards 和 react 声明为对等依赖(peer dependencies),不要声明为普通依赖。SDK 定义了组件读取的 React 上下文(选择、时区、主题根),而上下文由创建它的对象标识。因此宿主和每个组件包必须解析到同一份 SDK。

SDK 根入口不含图表或地图代码。只绘制 DOM 的组件从根入口导入,不会向宿主的打包产物加入 Apache ECharts、MapLibre 或内置组件。图表辅助工具(EChart、buildLineOption、buildGaugeOption、useChartTheme)位于 @devicechain/widget-sdk/echarts,而 echarts 是可选的对等依赖,只有导入该入口的包才需要它。

声明组件​

import { WidgetFrame, defineWidget, formatValue, pickSample } from '@devicechain/widget-sdk';

export const tankLevelWidget = defineWidget({
descriptor: {
id: 'acme/tank-level',
version: '1.0.0',
sdkApiVersion: 1,
optionsVersion: 1,
display: { title: 'Tank level', category: 'Telemetry' },
channel: 'measurement',
datasource: { mode: 'required', selectors: ['device', 'slot'] },
binding: { window: { option: 'window', min: 1, default: 1 } },
options: {
unit: { kind: 'string', doc: 'Unit shown beside the level.', label: 'Unit' },
window: { kind: 'number', integer: true, min: 1, doc: 'Samples kept.', label: 'Samples kept' },
},
defaults: { unit: '%', window: 1 },
interactions: {},
},
implementation: {
kind: 'trusted-react',
component: ({ data }) => (
<WidgetFrame title="Tank level">{formatValue(pickSample(data.latest, undefined)?.value, 1)}</WidgetFrame>
),
},
});

defineWidget 会根据字面量推断通道和选项模式。组件数据与通道不一致、默认值与模式不一致、绑定引用了不存在的选项,或声明了该通道无法承载的交互,都会成为编译错误。宿主构建注册表时会再次检查同样的规则,以覆盖绕过编译器的调用方。

描述符​

描述符是可序列化为 JSON 的纯数据,不含组件、回调、导入 URL 或令牌,因此定义解析器和校验器无需加载或信任你的代码就能读取它。

字段含义
id带命名空间的 id,形如 scope/name,例如 acme/tank-level,只能使用小写字母、数字和连字符。不含斜杠的 id 保留给内置组件。当前版本中命名空间的归属只是运维人员之间的约定,并不证明包的发布者是谁。
version该组件的发布版本,格式为 MAJOR.MINOR.PATCH。它是来源信息。参见组件版本与兼容性。
sdkApiVersion组件所针对的 SDK 契约代际,目前为 1。
optionsVersion已存储选项的结构版本。仅当已存储选项的含义发生变化时才递增。
displaytitle,以及可选的 description 和 category,用于宿主的“添加”菜单。titleId 和 categoryId 是可选的消息 id,见标签与帮助文字。
channel组件消费哪类数据。见下文。
datasource{ mode: 'none' },或 mode: 'optional' / 'required' 加上作者可选的 selectors:device、anchor 或 slot。control 通道的组件只针对一台设备,因此只提供 device 和 slot。
binding宿主如何订阅,用常量和对你自己选项键的引用来表示。
options选项模式。
constraints可选,跨多个选项的规则,例如 greater-than。
defaults新添加组件的初始选项。
interactions组件请求的宿主回调。

通道与绑定策略​

一个组件声明一个通道:

通道组件接收绑定策略
measurement按测量名称组织的 latest、滚动的 samples 窗口和 errorwindow:保留多少个样本
alarm当前告警行、总数以及加载与错误标志pageSize,以及可选的 state、severity、acknowledged 过滤器
control目标设备的命令历史pageSize
location所绑定实体的位置样本无
selection无数据。组件根据宿主的候选项查询来驱动一个槽位。无
none无数据。静态组件。无

绑定值要么是常量 { const: 25 },要么是对选项键的引用 { option: 'window', min: 1, default: 1 }。引用在编译时会按选项类型检查。你不能提供 GraphQL 字符串或订阅函数:传输由宿主负责。selection 和 none 通道不打开任何数据通道,且必须声明 datasource: { mode: 'none' }。

当槽位没有设备,或所绑定的设备被删除时,宿主会显示其标准的未绑定或不可用状态,不会把之前那个实体的数据交给你的组件。

选项​

选项有 kind(string、number、boolean 或 enum)和一句 doc 说明。数字可声明 min、max 和 integer。枚举声明其 values。字符串可声明 format: 'json',要求其值能解析为 JSON。required 标记一个缺失就会让组件无内容可显示的选项;空字符串视为缺失。宿主会依据模式校验已存储的选项,控制台也据此生成设置表单。

使用 SDK 的辅助函数 optString、optNumber 和 optBoolean 以防御方式读取选项。对于手工编辑后类型错误的值,它们返回 undefined,而不是抛出异常。

标签与帮助文字​

为每个选项提供可移植的 label,需要时再提供一句 help。从未见过你的组件的宿主会原样显示这些文字,因此你的选项无需宿主提供任何翻译即可被理解。每段文字还可以带一个消息 id,宿主会先在自己的翻译中查找它:

文字可移植文字可选的消息 id
选项标签labellabelId
选项帮助helphelpId
枚举选项valueLabels,按值索引valueLabelIds,按值索引
枚举的未设置项placeholderplaceholderId
组件标题与类别display.title、display.categorydisplay.titleId、display.categoryId

控制台在自己的翻译定义了该消息 id 时使用它,否则使用你的可移植文字。两者都没有的标签会回退为可读化的选项键(maxRows 显示为“Max rows”),两者都没有的枚举选项显示其原始值。宿主未定义的消息 id 不是错误,因此你的包可在从未见过它的宿主中正常工作。

options: {
show: {
kind: 'enum',
values: ['bar', 'number'],
doc: 'Whether the card draws the fill bar under the number.',
label: 'Show',
valueLabels: { bar: 'Number and fill bar', number: 'Number only' },
placeholder: 'Number and fill bar',
},
},

专用编辑控件​

选项可以在其 control 字段中指定一个专用的编辑流程。它与 control 数据通道无关。不指定时,选项使用与其类型对应的通用字段:文本框、数字框、复选框或下拉列表。

该词汇表是开放的。任何非空字符串都可以注册,未实现所指定控件的宿主会改为显示该选项的通用字段。因此,即使宿主比包所指定的控件更旧,包仍然可以安装。请在你自己的命名空间中命名你的控件,例如 acme/capacity-presets。不带前缀的名称属于宿主。

控制台实现了 HOST_OPTION_CONTROLS 中列出的控件,该常量由 @devicechain/dashboards 导出:

控件流程
command-vocabulary从所绑定设备的命令中选择一个,并一起写入其键、标签和参数。
slot-target从仪表板的槽位中选择一个。
basemap标记栅格地图瓦片源及其署名。

控制台未实现的控件(包括所有带命名空间的控件)使用通用字段。若要为你的选项提供专属的编辑体验,请提供一个编辑器。

自定义编辑器​

implementation.editor 是一个可选的 React 组件。组件提供了编辑器时,控制台会用它替代生成的选项字段。数据源字段和控制台的提示仍由控制台负责。编辑器接收由 @devicechain/widget-sdk 导出的 WidgetEditorProps:

属性含义
options组件已存储的选项,只读。
setOptions(patch)修改选项的唯一方式。一次调用中的多个键会一起生效。
services宿主服务,以显式方式传入。见下文。
descriptor你所注册的描述符,用于读取其选项的标签、边界和枚举值。

setOptions 在生效前会依据你的选项模式进行检查。模式未声明的键或类型错误的值,会在编辑器出错的那一刻抛出异常,而不是被存储下来却无人读取。控制台会捕获该错误并显示在你的编辑器旁边,且不会写入任何内容。undefined 和空字符串会删除一个键,布尔选项的 false 也是如此。超出声明边界的数字会被限制到边界之内。

services(WidgetEditorServices)包含:

服务含义
slots仪表板的槽位,按名称索引。仪表板没有槽位时为空。
deviceToken组件直接绑定的设备的令牌(如果是直接绑定)。
commandChoices(deviceToken)设备接受的命令。可选:仅当你的描述符在某个选项上声明了 command-vocabulary 控件时才会提供;没有命令词汇表的宿主也不提供它。

编辑器不会获得 hub、客户端或访问令牌,也不得从宿主应用导入任何内容。宿主无法提供的服务会缺失,你的编辑器必须能够应对。如果编辑器在渲染时抛出异常,控制台会用一条错误消息和一个重试按钮替换它,作者在仪表板上的其他更改都会保留。

implementation: {
kind: 'trusted-react',
component: TankLevelCard,
editor: function TankLevelCardEditor({ options, setOptions, descriptor }) {
return (
<label>
{descriptor.options.unit?.label ?? 'unit'}
<input value={options.unit ?? ''} onChange={(e) => setOptions({ unit: e.target.value })} />
</label>
);
},
},

在 defineWidget 中,options 和 setOptions 的类型来自你的选项模式,因此拼错的键也会成为编译错误。示例包中的卡片组件带有一个完整的编辑器。

交互与信任模型​

interactions 列出组件使用的宿主回调:alarmActions(确认与清除,仅限告警通道)、commandSend(仅限控制通道)、selection 和 candidates。宿主只提供你声明过的回调,并进一步限制在宿主支持且查看者被允许的范围内。没有回调的组件表现为只读。服务器始终是权威:查看者无权执行的操作会被服务器拒绝,且该拒绝会传达到你的组件。

组件包是受信任的代码。 它在构建时被打包进宿主应用,并在宿主页面中以宿主的权限运行。描述符中的声明用于规范 API,并不是安全边界,无法阻止包中的恶意代码。SDK 不会把访问令牌、客户端或原始 hub 传给你的组件,但同一页面中的代码仍然可能获取它们。只安装你已审查过、来自可信发布者的包。宿主会用错误边界包裹每个组件,因此抛出异常的组件只会在自己的框内显示错误,同级组件继续渲染。但这无法遏制死循环或恶意代码。

下一步​