本文已参与「新人创作礼」活动,一起开启掘金创作之路。
Node-Red(一):编辑器的使用
一、是什么
设备采集类数据,建议用二级制, 如果是供多个上层业务应用再考虑json
node-red 是一个基于 NodeJs 的应用广泛的物联网接入平台
- 支持基于浏览器的拖拽,动态生成事件驱动架构应用
- 支持部署于边缘设备(树莓派 jess 已内置)和云端
- 提供一种(面向数据流)可视化编程技术,减少代码量
🙄博主的很菜申明流程
二、能干什么?
node-red 广泛应用于
- 健康管理
- 农业
- 智能家居
- 工业自动化
- 其他无限的可能
三、Node-Red Editor 的使用
1. 编辑器的框架
四、功能说明
1. 常用快捷键
-
按住 ctrl (command)进入快速连接模式
-
按住 ctrl (command)点击空白地区,快速添加节点(会优先展示最近使用的节点)
-
一直按住 ctrl (command)一直快
-
按住 ctrl(command)点击连线,快速加入中间节点
-
按住 shift 修改连线。如果有多个连线则,批量移动端点
-
按住 shift 移动到空白区域,删除连线
-
双击节点,编辑节点属性
- 给节点起一个好的名字
- 每个节点可以有 markdown 格式的说明
- 可以修改节点的 icon 图标
- 可以给 inPort 和 outPort 添加 tooltip 注释
- 可以使节点无效(界面上显示为虚线,临时禁用一般用于测试)
2. 部署
node-red 支持 4 种部署策略
-
部署 --> 全面(默认选项)
- runtime 运行时将停止所有节点,然后加载新配置重启
-
部署-->已修改的流程
- 未受修改影响的流程继续运行
-
部署-->已更改的节点
- 未修改的节点将继续运行
-
部署-->重启
3. 导航
- 右侧的工具栏可以导航全部流程
4. 上下文
上下文是一个在流程之外的存储信息的地方,上下文有作用域。RedNode 的上下文分为:
-
全局上下文(作用范围:全局,所有流程,所有页签)
- 所以一定要取一个好一点的名字
-
节点上下文(作用范围:本节点)
-
流程上下文(作用范围:相同的流程 或者相同的页签内的节点可以访问)
- 含子流程的环境变量
-
子流程上下文
- 子流程的流程上下文的作于域只存在于子流程节点的子流程内
- 子流程中能拿到父流程流程参数(使用 $parent.xxx)
如何确定上下文的作用域
- 决定于需要在哪些节点使用这个数据
上下文中的变量默认情况下只保存在内存中。
-
将上下文保存到文件时,上下文也会同步到内存中,加快读取速度
-
上下文存储在 node-red 中做了插件化设计
-
你可以选择性的持久化某些文件到磁盘,使用 Multi-Context-Storage
- 在 settings.js 中新增配置 contextStorage:{store: { module: "localfilesystem"},default: { module: "memory" }}
- flow.set("ceshi", "12345678", "store");
-
使用 功能节点下的 change 节点可以快捷的设置上下文变量,右边可以选择存储策略(如果配置了文件存储的话)
-
如果使用 context.flow() 等,来设置上下文变量,
-
myObject 加属性 myObject.property = xx 时,需要调用 context.flow.set('xx', 'xxx') 进行回写
-
如果修改了 myObject 又不想修改流程变量,建议使用 RED.util.cloneMessage(myObject)
-
你可以通过编辑器右侧的 context 侧边栏来方便的管理 上下文
使用上下文的时机
- 上下文表示,你有一些和流程无关的变量
- 如果你考虑到可扩展性,也就是会启动多个,实栗,做并行处理,考虑上下文同步问题
5. 子流程
子流程将一系列的节点并成一个聚合节点
- 降低流程的复杂度
- 为了复用
- 子流程创建后,将出现在左边的工具箱,成为一个独立的实栗
如何创建子流程?
- 选择要合并的流程
- 选择右上角的主菜单 > 子流程 > 将选择部分更改为子流程
- 左边工具箱将出现子流程分组和刚添加的子流程
如何编辑和配置子流程?
-
双击子流程打开属性面板
-
点击属性面板左上角“编辑流程模板”
-
就可以在子流程做编辑了
- 可以编辑子流程的输入、输出数量
- 可以编辑子流程内的环境变量
- 在流程中使用 `${变量名}`` 使用该环境变量
来自博主的补充:
-
node-red 导出的时候对子流程的处理
- 没有像 activiti 一样使用嵌套,而是平铺
- 这点给 node-red 点赞
6. 消息对象 msg
node-red 的工作方式就是,在节点之间传递 msg 对象
- msg 是一个 JavaScript 对象,可以有任意个属性,支持任意类型
- msg.payload 默认的数据主体(好像 payload 的toString() 方法能自动进行 mqtt 数据转码)
功能节点下的 change 节点支持 JSONata
- 栗如:(payload.temperature-32)*5/9
msg 序列
- 可以将 csv 等多个数据通过 split 节点,将一条消息拆分为多条
- 这是使用 debugger 节点,将有多次输出
- 这时候使用函数节点操作一条数据,将会修改 msg 序列中的所有数据
- 可以通过 join 节点,将多条消息重新合并为一条消息
- 栗如:[1, 2, 3, 4] 调用 split 节点 将生成 msg 序列:4,3,2,1
- 栗如:“123 换行 456 换行 789”调用 split 节点,将生成 msg 序列:“789”“456”“123”
- 栗如:{"a":1,"b":2,"c":3} 调用 split 节点,将生成 msg 序列:“c 3”“b 2”“a 1”
每个 msg 包含一个 msg parts 属性
-
msg.parts.id:消息序列唯一ID
-
msg.parts.index:msg 在消息序列中的位置
-
msg.parts.count:消息序列的长度(如果能提前获取)
-
其他节点特有的元数据(metadata)
- split 拆分节点特有的:join 规则
7. 路由节点
如果不使用路由节点,出口连接多个分支,则每个分支都是同样的数据
功能节点下的路由节点(switch)可以根据属性做选择分支
- 每新增一个 condition 表达式,节点就会在相应的位置新增一个出口
- condition 表达式中,可以使用流程上下文做为参数,实现参数化流程
- condition 表达式同样支持 JSONata
- 你还可以使用 函数节点做动态分发 node.send()
8. 延迟节点,触发器节点
如果想要限制消息发送频率,你可以使用延迟节点(delay)或者触发器节点(trigger)
-
延迟节点
-
每个流经的消息都将延迟等待指定时间
-
支持等待随机时间
-
支持等待 msg.delay 属性的值
-
延迟节点可以选择:限制消息速率模式
- 可以配置将多余的消息压入队列
- 丢弃多余的消息
- 和将多余的消息拼接到下一条消息
-
-
触发器节点(好像是重试机制)
-
处理完本条消息后,等待一定时间,然后重发
- 勾选:如有新信息,延长延迟,可以用来做断线检测
- 栗如:mqtt 的遗言
-
典型应用,双 trigger 节点组成
- 在正常业务下并行的创建两个 trigger 节点
- 第一个 节点做成断线检测
- 第二个节点会每 5 秒重发指定内容
- 第一个节点在重新收到消息后,会发送一个 reset 消息
- 第二个 trigger 节点在接收到 reset 消息后停止尝试重发
-
处理完本条消息后,一定时间内,如果没有接收到任何消息,发送一条消息
-
9. 节点合并
-
使用 join 节点合并消息
-
可以使用 key/value 模式合并
- 栗如:合并两个传感器数据:
- 传感器1 topic = internal,payload = ["23.94", "24.24","20.00"]
- 传感器2 topic = external,payload =["22.68", "24.24", "20.00"]
- 用 topic 做为key,当 message 部分到达量为 2 合并为
- {internal: "23.94", external: "22.68"}
-
直接合并
-
合并成一个对象
-
-
使用 函数节点合并消息
- 当 join 节点不符合要求的时候,可以通过函数节点来处理
// 设置 context 的值
context.set(msg.topic, msg.payload);
// 获得最新的 context 的值
let internal = context.get("internal");
let external = context.get("external");
// 检查
if (internal !== undefined && external !== undefined) {
return {
payload: {
internal: internal,
external: external
}
}
}
return;
10. 节点选择组
节点可以聚合起来成为一个选择组,一个选择组可以在编辑器中同时移动,拷贝,删除
- 节点选择组提供了一种组织节点的方式
- 节点选择组提供更好的展现方式(添加文字提示)
如何添加选择组
- 创建组:选择多个节点,选择右上角菜单,组,选择组
- 加入组:将节点拖拽到选择组框内,就能将节点加入该选择组
- 移出组:选择一个节点,选择右上角菜单,组,从组中移除
- 双击选择组,可以编辑选择组的外观、名称
- ctrl + shift + c 快速复制一个分组的样式
- ctrl + shift + v 快速粘贴一个分组的样式
11. JSONata 语法
- JSONata 能做什么?
Address.City
FirstName & ' ' & Surname
Phone[type = 'mobile'].number
$sum(Order.Product.(Price * Quantity))
(Numbers[2] != 0) and (Numbers[5] != Numbers[1])
- 属性拼接
firstName & ' ' & lastName
- 属性获取
xx: { Phone: [ { type: 'xxx', number: 'xxxxxx' }, { type: 'xxxx', number: 'yyyyyy' }, { type: 'xxxxx', number: 'zzzzzzz' } ] }
/** 属性获取 */ Phone.number
/** 返回 */ [ "xxxxxx", "yyyyyy", "zzzzzzz" ]
/** 属性获取 */ Phone[0].number
/** 返回 */ "xxxxxx"
/** 属性获取 */ Phone[type='xxxx'].number
/** 返回 */ "yyyyyy"
- 属性计算(查询 + 抽取数据)
/** 属性获取 */
Account.Order.Product.(Price * Quantity)
/** 返回 */
[
68.9,
21.67,
137.8,
107.99
]
/** 属性获取 */
$sum(Account.Order.Product.(Price * Quantity))
/** 返回 */
336.36
-
内置函数
-
String 类
$string(5)=>"5"$length("Hello World")=>11$substring("Hello World", -4)=>"orld"$substringBefore("Hello World", " ")=>"Hello"$substringAfter("Hello World", " ")=>"World"$uppercase("Hello World")=>"HELLO WORLD"$lowercase("Hello World")=>"hello world"$trim(" Hello \n World ")=>"Hello World"$pad("foo", 5)=>"foo "$pad("foo", -5, "#")=>"##foo"$contains("abracadabra", "bra")=>true$split("so many words", " ")=>[ "so", "many", "words" ]$join(['a','b','c'])=>"abc"$match("ababbabbcc",/a(b+)/)=> ?$replace("John Smith and John Jones", "John", "Mr")=>"Mr Smith and Mr Jones"$eval("[1,2,3]")=>[1, 2, 3]$base64encode("myuser:mypass")=>"bXl1c2VyOm15cGFzcw=="$base64decode("bXl1c2VyOm15cGFzcw==")=>"myuser:mypass"$encodeUrlComponent("?x=test")=>"%3Fx%3Dtest"$encodeUrl("https://mozilla.org/?x=шеллы")=>"https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B"$decodeUrlComponent("%3Fx%3Dtest")=>"?x=test"$decodeUrl("https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B")=>
-
Number 类
$number("5")=>5["1", "2", "3", "4", "5"].$number()=>[1, 2, 3, 4, 5]$abs(-5)=>5$floor(5.8)=>5$ceil(5.8)=>6$round(123.456, -1)=>120$power(2, 0.5)=>1.414213562373$sqrt(2)=>1.414213562373$random()=>0.4029142127028$formatNumber(12345.6, '#,###.00')=>"12,345.60"$formatBase(100, 2)=>"1100100"$formatInteger(1999, 'I')=>"MCMXCIX"$parseInteger('12,345,678', '#,##0')=>12345678
-
聚合类
$sum([5,1,3,7,4])=>20$max([5,1,3,7,4])=>7$min([5,1,3,7,4])=>1$average([5,1,3,7,4])=>4
-
布尔类
$boolean(arg)$not()$exists(arg)
-
数组类
$count([1,2,3,1])=>4$append([1,2,3], [4,5,6])=>[1,2,3,4,5,6]
$sort(Account.Order.Product, function($l, $r) { $l.Description.Weight > $r.Description.Weight })[1..5] ~> $reverse()=>[5, 4, 3, 2, 1]$shuffle([1..9])=>[6, 8, 2, 3, 9, 5, 1, 4, 7]$distinct([1,2,3,3,4,3,5])=>[1, 2, 3, 4, 5]$zip([1,2,3],[4,5],[7,8,9])=>[[1,4,7], [2,5,8]]
-
对象类
$keys(object)$lookup(object, key)$spread(object)$merge(array<object>)$sift(object, function)$each(object, function)$error(message)$assert(condition, message)$type(value)
-
时间日期类
$now()=>"2017-05-15T15:12:59.152Z"$millis()=>1502700297574$fromMillis(1510067557121)=>"2017-11-07T15:12:37.121Z"$toMillis("2017-11-07T15:07:54.972Z")=>1510067274972
-
???
$map([1..5], $string)=>["1", "2", "3", "4", "5"]$filter(array, function)$single(array, function)$reduce(array, function [, init])$sift(object, function)
-
-
你可以自己扩展其内置的函数