实时数据通信
实时数据通信,是物联网平台中必备的功能。可以实时、无刷新的将界面组件的重要属性值,如数据、文本、形状、行为、动画等进行双向传输和控制,实现设备数据实时监控、数据大屏展示以及数字孪生。
实时数据通信对于meta2d.js来说,就是远程服务器端发送给当前网页端的数据,实时更新到图纸上的一个或多个组件的属性值。图纸上的组件(s),也能够根据事件规则的设定进行实施反馈于转发。
一、通信流程
meta2d.js支持mqtt、websocket和http三种方式的数据实时通信协议。http采用的是定时请求远程api,获取响应数据后,更新组件的指定属性。mqtt和websocket协议,则依赖于 mqtt.js 实现。
通信流程大致如下:
- 建立连接
- 监听数据
- 更新数据
1、建立连接
建立连接,可以通过2种方式:打开图纸和调用api手动控制连接。
- 打开图纸文件。如果图纸文件数据中,包含有协议配置的连接信息,则会自动连接。在 meta2d库的core.js 中,open方法相关代码如下:
connectSocket方法内部,调用了3类协议的连接方法。
typescript
connectSocket() {
this.connectWebsocket();
this.connectMqtt();
this.connectHttp();
}
这3个函数主要逻辑就是读取 store.data中名为 mqtt、websocket、https 这3个对象的设置信息。如果存在这些对象,就读取配置信息,调用相关的实现函数进行连接服务器。代码实现并不复杂,不在此进行进一步介绍。
- 通过手动调用api
如果,你设置了相关协议的配置信息,可以直接手动调用connectSocket方法连接3个,或者单独调用其中的一个。
2、监听数据
对于mqtt和websocket来说,本质上是在mqtt.js 连接对象上,注册消息监听事件的自定义函数,来接收信息并使用自定义的业务逻辑。当然,我们通过额外的设置,可以增加自己的处理逻辑,比如下面代码,以mqtt协议为例:
typescript
if (engine.mqttClient) {
// 监听连接事件
engine.mqttClient
.on('connect', () => {
console.info('连接Mqtt服务器成功: ', engine?.store.data.mqtt);
})
.on('message', (topic, message) => {
try {
console.info('收到Mqtt主题消息: ', topic, JSON.parse(message.toString()));
} catch (e) {
console.warn('收到Mqtt主题消息,但转换json格式失败: ', message.toString() + ' error: ' + e);
}
});
}
代码中,engine是meta2d对象的实例变量。其中内置的mqttClient属性,是建立连接后的 mqtt.js 对象。通过注册mqtt连接对象相关事件的监听函数,来实现自定义相关业务逻辑。
3、更新数据
上面章节已说明,注册了各类事件的自定义函数,会接收到协议通信的数据。而更新数据,就是在这些自定义函数内部中,将收到的数据,通过调用核心库的api,来更新图纸组件的属性值。
核心库的2个api至关重要:
1)根据组件的id、或者tag来查找组件,返回的是组件的数组对象。
typescript
find(id:string): Pen[]
2)设置属性的值
typescript
setValue(data: IValue, { render, doEvent, history, }?: {
render?: boolean;
doEvent?: boolean;
history?: boolean;
}): void;
看一下参数 data 的类型 IValue定义:
typescript
export declare type IValue = Pen & Partial<ChartData> & Partial<Record<'tag' | 'newId', string>> & {
[key: string]: any;
};
可以看出,在setValue这个场景下(更新组件的属性值),data应该代表数据对象本身,也就是Pen对象。其中必须包含id或者tag属性,以便核心库确定更新的是哪个pen。
另一个需要说明的参数是 render 参数。如果组件的某个属性值更新后会产生外观上的变化,canvas界面需要实时反映出变化结果,就需要将其设置为true。
注意:每一次让canvas数据更新,都会产生计算量。那么一个建议就是,当一次更新多个组件的属性时,setValue中的render属性应设置为false,在完成所有组件的更新后,再调用api让canvas更新视图。
typescript
for (let i = 0; i < selectedTarget.value.pen.length; i++) {
const p = selectedTarget.value.pen[i];
engineObj.value.setValue(
{
id: p.id,
[name]: value,
},
{
render: false, // 更新后,不渲染canvas
history: true,
}
);
}
engineObj.value.render(); // 渲染canvas
注:变量 selectedTarget 代表了选中的组件列表,engineObj代表了meta2d实例对象。
二、协议配置说明
2.1 mqtt配置
乐吾乐官网文档 对mqtt协议有说明,但必须说,需要进一步说明,才能够更容易理解。
上面的params是mqtt协议连接的配置对象,其中,第一个参数 mqtt 的值 就是需要进一步说明的。
由于mqtt协议的实现其、是基于mqtt.js实现,故连接实际上是在websocket协议之上建立的。因此协议头是 ws:// 或 wss:// 开头。至于是否采用ssl,取决于你的设置(你可以自定义,来决定采用的协议头)。完整的url定义如下:
ws(s)://服务器地址:端口/上下文路径
以emq免费mqtt服务器为例,url就是这样的:
wss://broker.emqx.io:8084/mqtt
2.2 websocket配置
websocket协议的配置,只有一个,就是url,和上面mqtt的url定义结构完全相同,这取决于你采用的websocket服务器的具体的实现和要求。
2.3 http配置
注意是3个参数:url、httpTimeInterval、httpHeaders的设置:
注意一点,在最新的meta2d版本中,store.data中的https,被设计为一个对象数组。应该是为了支持多个http请求地址。同时,也兼容老版本中,store.data中的http对象。因此,设置和读取http的配置时,选你自己喜欢的就行。
三、数据结构
无论采用哪一种协议和远程数据接口进行通信,在meta2d的监听和更新数据逻辑上,数据结构上必须循序三类:
- 按组件id,更新属性值
- 按组件标签,更新属性值
- 按数据绑定id,更新属性值
这里说的数据结构,指的是远程服务器端,发送给网页端数据的结构,通常是一个json字符串,meta2d通过上面介绍的3类通信协议,可以收到这个字符串并解析成json对象后,调用核心api来更新组件的属性。
3.1 按组件id更新属性值的数据结构
typescript
{
id: 'pen1',
text: 'new text'
}
如果需要一次性更新多个组件,结构是对象数组
typescript
[{
id: 'pen1',
text: 'new text'
}]
3.2 按组件标签更新属性值的数据结构
typescript
{
tag: 'tag', // tag的名字
text: 'new text' // 属性及新的属性值
}
3.3 按数据绑定id更新数据的数据结构
typescript
{
dataId: 'id',
value: 'value'
}
dataId,代表了一个关联关系。它将某个数据点和图纸上的某个属性进行了关联。例如:
某个设备的电压(数据点)的id是:id9527,通过关联操作,将其关联到id为 pen9527 的text属性。那么如果数据端发送了
typescript
{
dataId: 'id9527',
value: '200'
}
meta2d就会将id为pen9527的text的值,更新为:200
四、自定义数据结构处理
当设计器页面接收到的数据,不是上面的结构的时候,基本上这是大概率会发生的。比如我们的后台api返回的通常是下面的架构:
typescript
{
code: 200,
data: {deviceId: 'afmc8976', temperature: 67, ...}
}
遇到这样的情况,我们需要做的是
- 实现自定义结构解析函数
- 注册自定义函数
4.1 通信逻辑分析
实现自定义结构解析函数的主要作用,就将你的数据结构和上面章节介绍的3种结构进行转换,然后使用更新数据的方法。而注册自定义函数,就是定义引擎对象的socketFn函数(来自官网文档):
typescript
meta2d.socketFn = (message, context) => {
// Do sth
meta2d.setValue(pen);
//return false; //表示仅执行自定义的回调函数方法
//return true; //表示除了执行自定义的回调方法外,还会执行核心库方法
};
我们来搞清楚实现自定义函数的逻辑。先来看 socketFn 的定义:
typescript
socketFn: (
e: string,
// topic: string,
context?: {
meta2d?: Meta2d;
type?: string;
topic?: string;
url?: string;
}
) => boolean;
socketFn接收2个参数,e和context。翻看源码,我们会发现在socket建立连接和接收到消息时,会调用一个方法 socketCallback:
再看看socketCallback的定义:
typescript
socketCallback(
message: string,
context?: { type?: string; topic?: string; url?: string }
) {
this.store.emitter.emit('socket', { message, context });
if (
this.socketFn &&
!this.socketFn(message, {
meta2d: this,
type: context.type,
topic: context.topic,
url: context.url,
})
) {
return;
}
......
}
可以看到,socketCallback 会调用 socketFn 方法,将收到的字符串类型的消息,和上下文对象(包含引擎实例)传递给该方法。
至此我们就实现了自定义的数据结构的实时数据通信了。
4.2 扩展自定义数据结构解析函数
上面的方法,是将自定义函数固话到前端代码中。这种方式最少有2个不足:
- 如果需要修改则需要重新修改、边缘并发布到服务器上,维护成本其实并不低。
- 如果在自定义函数内部,需要调用我自己程序中相关的上下文对象,比如,我用的是quasar框架,我希望在收到某些消息时,调用quasar提供的消息处理对象,弹窗消息提示。这样的情景,该怎么办?
这里介绍一个方法,可以解决上面2个问题。
实现思路,是将函数体实现的内容代码,由前端程序中,迁移到页面编辑器中。编写一个方法,将内容代码的字符串转换为socketFn要求的类型,并增加传入自定义类型的对象。
在我的另一篇文档开源可视化引擎 Meta2d.js 实战 - 集成monaco编辑器 介绍了如何在页面集成编辑器编写前端代码。基于此在程序中能够获取到编辑器的内容,也就是一个 js代码 的 字符串。我们要做的就是,将字符串转换为函数类型。大概的代码是这样:
typescript
// 注册自定义js处理函数
if (!StringUtil.isBlank(customJs) {
engine.socketFn = (e, context) => {
if (e && typeof e === 'string' && customJs) {
executeJsExpressions(customJs, { message: e, context, notify: NotifyUtil });
}
return true;
};
}
上面代码中,customJs是编辑器的内容,这比较好理解。重点是executeJsExpressions方法,此方法接收2个参数:代表实现自定义数据结构解析的函数代码字符串,和一个对象。这个对象包含了收到的消息,meta2d上下文对象,以及扩展所需要的用于提示的对象。当然,如果需要你可以增加任何你所需要的对象进去。
来看一下executeJsExpressions的具体实现,可以直接使用。
typescript
// 执行多行js表达式,每行表达式必须以分号结尾/分割
export const executeJsExpressions = (
// 表达式字符串,一般为编辑器/文本域中的值
expressions: string,
// 要传入的上下文对象 key 代表代码中引用的对象名,value为实际的对象实例。如 context: { 'context': any, 'message': string, notify: NotifyUtil}
context: { [key: string]: any },
// 每个表达式区分的分隔符,默认为分号
separator = ';'
): any[] => {
const tmpStat = expressions.replace(/;/g, separator);
const func = new Function(...Object.keys(context), tmpStat);
return func(...Object.values(context));
};
这里需要说明的是:将一个字符串转换为函数类型,存在潜在的安全风险,在此不再赘述,各位自行把握.
至此,实时数据通信逻辑分析与实现介绍完毕。