2020年11月2日 星期一

node-red教程 5.4 context global与函数节点的其它功能

node-red教程 5.4 context global与函数节点的其它功能

5.4 context与global的应用

5.4.1 使用context实现计数器

  在之前的流中,我们总是在执行函数节点时新建一个变量。函数节点执行完毕以后,这个变量的值就会丢失,没有办法保存。如果需要保存一个变量的值该如何操作?
  可以借助context对象。它可以理解为上下文,或者语境,用于保存内存中的数据,这个数据可以持续保存到下一个消息到来。Context可用于保存索引,计数或者消息中的数据。
  拖入inject节点与debug节点。拖入一个函数节点,进行如下设置:
这里写图片描述
  完成以后连线并部署。多次点击inject节点的输入按钮,并在调试窗口观察现象。
这里写图片描述
  发现调试窗口中的数据在递增
这里写图片描述
  接下来结合现象分析代码:

var count = context.get('count')||0;
//如果count不存在就初始化为0,已存在则获取count的值
count += 1;
context.set('count',count);
//执行完+1操作以后保存count的值
msg.payload = count;
return msg;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

  我们可以查到Context的一些API(应用程序接口)如下

context.get(..) : 获取一个节点范围内的上下文属性
context.set(..) : 设置一个节点范围内的上下文属性
context.keys(..) : 返回所有节点范围上下文属性键的列表
context.flow : 同 flow
context.global : 同lobal
  • 1
  • 2
  • 3
  • 4
  • 5

  每次的输入都不变,count每次都+1,最后输出递增的数,说明count记录了上一次输入的值。执行Context.get以后,就可以获取到之前保存的值。
  执行context.set以后,就能把处理过后的值保存下来。保存到哪里?’count’,带单引号的count里。接下来用流程图来表示逻辑。注意,带单引号的’count’与变量count不一样
这里写图片描述
  流程保存在这里

[{"id":"1e2326a1.bc3da9","type":"inject","z":"dfef6fbd.55491","name":"","topic":"","payload":"1","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":690,"y":160,"wires":[["ecb5fce2.ce1dc"]]},{"id":"7e2fd11b.d4b4","type":"debug","z":"dfef6fbd.55491","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","x":1100,"y":160,"wires":[]},{"id":"ecb5fce2.ce1dc","type":"function","z":"dfef6fbd.55491","name":"计数器","func":"var count = context.get('count')||0;\n//如果count不存在就初始化为0,已存在则获取count的值\ncount += 1;\ncontext.set('count',count);\n//执行完+1操作以后保存count的值\nmsg.payload = count;\nreturn msg;","outputs":1,"noerr":0,"x":880,"y":160,"wires":[["7e2fd11b.d4b4"]]}]
  • 1

5.4.2 使用flow在不同节点之间传递参数

  在5.4.1小节中,函数节点计数器实现了用’count’来保存数据的方法,那么这个’count’里边的内容,能否被别的节点使用?我们来尝试一下。
  在上一章节的基础上进行修改,添加计数器2,并做如下修改:
这里写图片描述
这里写图片描述
  如果计数器2可以使用’count’的内容,那么payload里的内容应该是递增的数据。然而,部署以后,观察实验现象,可以发现OUT2的内容并没有变化。这说明计数器2没有办法获取’count’里的数据。
这里写图片描述
  细心的读者可以发现,在context的API里,有这么一句解释:
context.get(..) : 获取一个节点范围内的上下文属性
  也就是使用context属性只能影响一个节点范围内。然而在实际的应用中,不同的节点之间可能会用到同一个变量,也就是说,有一个“全局变量”,该如何传递?
  我们查阅了函数的API,找到了flow的context用法,地址是

https://nodered.org/docs/writing-functions
flow.get(..) : 获得流作用域上下文属性
flow.set(..) : 设置流作用域上下文属性
flow.keys(..) : 返回所有流作用域上下文属性键的列表
  • 1
  • 2
  • 3
  • 4

  修改为“计数器”节点
这里写图片描述
  修改“计数器2”节点
这里写图片描述
  部署并点击inject节点的输入按钮,看到现象如下
这里写图片描述
  这说明计数器2获取到了计数器中的count值,也就是通过flow.set与flow.get实现了不同节点的参数传递。
流程分析如下:
这里写图片描述
  流的信息保存在这里:

[{"id":"d5052172.91827","type":"inject","z":"a1f259d6.8791a8","name":"","topic":"","payload":"1","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":140,"y":180,"wires":[["9ddd2b2f.536d28"]]},{"id":"811f3cdb.bf65b","type":"debug","z":"a1f259d6.8791a8","name":"OUT1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","x":530,"y":180,"wires":[]},{"id":"9ddd2b2f.536d28","type":"function","z":"a1f259d6.8791a8","name":"计数器","func":"var count = context.get('count')||0;\n//如果count不存在就初始化为0,已存在则获取count的值\ncount += 1;\ncontext.set('count',count);\n//执行完+1操作以后保存count的值\nflow.set('flowSave',count);\n//设置一个flowSave,来保存变量count的值\nmsg.payload = count;\nreturn msg;","outputs":1,"noerr":0,"x":330,"y":180,"wires":[["811f3cdb.bf65b","dbecc13a.c9642"]]},{"id":"dbecc13a.c9642","type":"function","z":"a1f259d6.8791a8","name":"计数器2","func":"var count = flow.get('flowSave')||0;\n//如果count不存在就初始化为0,已存在则获取count的值\nmsg.payload = count;\nreturn msg;","outputs":1,"noerr":0,"x":450,"y":260,"wires":[["71a9791a.cd5f28"]]},{"id":"71a9791a.cd5f28","type":"debug","z":"a1f259d6.8791a8","name":"OUT2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","x":610,"y":260,"wires":[]}]
  • 1

5.4.3 使用flow实现计时器

  使用flow的属性在不同的节点之间传递参数,这个功能十分有用。接下来尝试使用flow实现一个节点执行时间的计数器。
  在JS中,可以使用Data对象的getTime方法获取从1970年1月1日至今的毫秒数。跟时间戳得到的数据类似。在开始节点和结束节点分别获取时间,两个值的差就是这两个节点之间所用的时间了。
  另外,需要一个延时节点来放在两个计时节点之间。在功能控件里恰好有一个名为“delay”的控件满足要求。它可以控制消息通过自己的时间或者限制它的传递速度。我们会使用它的随机延时的功能。
  这次我们先来画一个流程图来分析思路。所谓“谋定而后动”,在编程里也是一个好习惯。
这里写图片描述
  这是开始时间的设置
这里写图片描述
  这是结束时间的设置
这里写图片描述
  这是delay节点的设置
这里写图片描述
  整个流连线如下。完成以后部署并点击inject节点的输入按钮
这里写图片描述
  可以看到,delay节点出现了一个蓝色的标记,以及表示时间的数字。这就是节点的状态status。
这里写图片描述
  调试窗口信息如下
这里写图片描述
  这是代码,懒得自己输入的可以直接导入看结果。

[{"id":"710e935.672cf6c","type":"inject","z":"b0555ded.82baa","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":150,"y":200,"wires":[["c3b7267d.1578c8"]]},{"id":"8bc81259.86e39","type":"debug","z":"b0555ded.82baa","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","x":560,"y":200,"wires":[]},{"id":"c3b7267d.1578c8","type":"function","z":"b0555ded.82baa","name":"开始时间","func":"flow.set('stratTime',new Date().getTime());\n//使用data对象的getTime方法获取开始时间,\n//并存到'startTime'里\nreturn msg;","outputs":1,"noerr":0,"x":260,"y":280,"wires":[["28cea295.2be12e"]]},{"id":"9152870f.6bf4e8","type":"function","z":"b0555ded.82baa","name":"结束时间","func":"var currentTime = new Date().getTime();\n//获取当前时间\nvar stratTime = flow.get('stratTime',stratTime);\n//用flow.get获取之间节点储存的开始时间\nvar timeElapsed = (currentTime - stratTime)/1000;\n//计算时间差并从毫秒转换为秒\nmsg.payload = \"逝去的时间是: \"+timeElapsed+\"s\";\nreturn msg;","outputs":1,"noerr":0,"x":460,"y":280,"wires":[["8bc81259.86e39"]]},{"id":"28cea295.2be12e","type":"delay","z":"b0555ded.82baa","name":"","pauseType":"random","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":350,"y":200,"wires":[["9152870f.6bf4e8"]]}]
  • 1

5.4.4 使用全局变量

  全局变量名为global,它与flow功能类似,但是作用域比flow大,并且,即便重新部署,global储存的值也不会丢失。以下是global的API。

global.get(..) :获取全局范围的上下文属性
global.set(..) :设置全局范围的上下文属性
global.keys(..) :返回所有全局作用域上下文属性键的列表
  • 1
  • 2
  • 3

  在5.4.3中见到的例子使用global也完全可以实现,只需要把里边所有的“flow”换成“global”就可以,由于太简单,就不演示了。Flow的作用于只局限于同一个流,此处做一点flow做不到的事情——流之间的数据传递。程序设计思路如下:
这里写图片描述
  整个流程如下设计
这里写图片描述
  开始时间与当前时间分别设计如下
这里写图片描述
这里写图片描述
  完成以后部署,并先点击开始时间的输入按钮,再点击当前时间的输入按钮。调试窗口现象如下:
这里写图片描述
  从现象中可以看出来虽然两个流没有直接连接,但是第二个流仍然获取了在第一个流中设置的变量。说明实验成功。
接下来,观察重新部署时,global变量能否保存。为节点做一些无关紧要的修改,例如out改为OUT,以便能够点击“部署”按钮。
这里写图片描述
  部署完成以后,不要点击“开始时间”的输入按钮,直接点击“当前时间”的输入按钮,可以观察到如下的现象。
这里写图片描述
  逝去的时间并没有被重置,说明重新部署不影响global的内容。
  程序的代码保存在这里

[{"id":"eaf64e67.5277","type":"inject","z":"3769aa0d.6513d6","name":"开始时间","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":140,"y":220,"wires":[["8a1f7741.6eb3b8"]]},{"id":"d6468738.ed2948","type":"debug","z":"3769aa0d.6513d6","name":"startTime","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","x":460,"y":220,"wires":[]},{"id":"8a1f7741.6eb3b8","type":"function","z":"3769aa0d.6513d6","name":"开始时间","func":"global.set('stratTime',new Date().getTime());\n//使用data对象的getTime方法获取开始时间,\n//并存到'startTime'里\nreturn msg;","outputs":1,"noerr":0,"x":280,"y":220,"wires":[["d6468738.ed2948"]]},{"id":"4c55cdab.648834","type":"function","z":"3769aa0d.6513d6","name":"当前时间","func":"var currentTime = new Date().getTime();\n//获取当前时间\nvar stratTime = global.get('stratTime',stratTime);\n//用global.get获取之间节点储存的开始时间\nvar timeElapsed = (currentTime - stratTime)/1000;\n//计算时间差并从毫秒转换为秒\nmsg.payload = \"逝去的时间是: \"+timeElapsed+\"s\";\nreturn msg;","outputs":1,"noerr":0,"x":280,"y":300,"wires":[["c5123924.d3f928"]]},{"id":"9f83e717.eaf888","type":"inject","z":"3769aa0d.6513d6","name":"当前时间","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":120,"y":300,"wires":[["4c55cdab.648834","357d85e7.a522ba"]]},{"id":"c5123924.d3f928","type":"debug","z":"3769aa0d.6513d6","name":"OUT","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","x":440,"y":300,"wires":[]},{"id":"357d85e7.a522ba","type":"debug","z":"3769aa0d.6513d6","name":"crrentTime","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","x":280,"y":400,"wires":[]}]
  • 1

  接下来,可以试一试不同的页面内,global的数值能否传递。新建一个页面。点击这个加号:
这里写图片描述
  把第一个流放在“开始的时间的流”标签下。第二个流放在“当前时间的流”标签下。
这里写图片描述
这里写图片描述
  部署,并先后点击开始时间与当前时间的输入按钮,可以在调试窗口看到如下现象,说明,即便是跨标签页,global的数据仍可以传递。
这里写图片描述
  另外,我在官方的博客见到了没有使用set与get方法的另外一种写法,经过实验也可以使用,把它写在这里。

context.global.startTime = new Date().getTime();
//global.set('stratTime',new Date().getTime());
var timeElapsed = (currentTime - context.global.startTime)/1000;
//var stratTime = global.get('stratTime',stratTime);
//var timeElapsed = (currentTime - stratTime)/1000;
  • 1
  • 2
  • 3
  • 4
  • 5

5.5 函数控件的其它功能

  函数控件的功能强大,甚至可以用函数控件实现其它所有功能控件的功能。在函数空间中,除了可以调用JS自带的各种方法以外,含有一些node-red特有的API可以使用,我把它写在这里

node.id :函数节点的ID,在0.19版本添加
node.name :函数节点的名字,在0.19版本增加
node.log(..) : 记录一条消息
node.warn(..) : 记录一条警告消息
node.error(..) : 记录一条错误消息
node.debug(..) :记录一条调试消息
node.trace(..) : 记录跟踪消息
node.on(..) : 注册一个事件处理函数
node.status(..) : 更新节点的状态
node.send(..) : 发送消息
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

  这其中从log到trace都是用来向命令换输出信息的,由于用法十分简单,就不讲解了。

5.5.1 使用send方法发送数据

  在5.2.3小节时,我们遇到一个问题:如果“apple”既满足条件1,又满足条件3,但是条件1执行return 的操作以后,条件3就不再执行了,有没有既可以发送数据,又不会结束函数的方法?当然有。我们查询了节点可以调用的API,发现有send函数,满足我们的要求。
  查一下之前的代码,把“判断主题”节点做如下修改
这里写图片描述
  然后部署并观察现象,可以发现,当点击apple的输入按钮时,OUT1与OUT3都可以收到消息,说明实验成功。
这里写图片描述
  代码如下

[{"id":"aaa307ba.874e58","type":"function","z":"b0555ded.82baa","name":"判断主题","func":"if (msg.topic === \"apple\") {\n   node.send([ msg, null, null]);\n} \nif(msg.topic === \"banana\"){\n   node.send( [ null, msg, null]);\n}\nif(msg.topic.indexOf(\"e\") != -1){\n   node.send([null,null, msg ]);\n}\n","outputs":3,"noerr":0,"x":360,"y":300,"wires":[["e8ac6685.9b2a48"],["503899c3.750508"],["1053e26d.f8c7ae"]]},{"id":"8e22df59.c7795","type":"inject","z":"b0555ded.82baa","name":"","topic":"apple","payload":"apple","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":150,"y":240,"wires":[["aaa307ba.874e58"]]},{"id":"1692dfa1.47a2a","type":"inject","z":"b0555ded.82baa","name":"","topic":"banana","payload":"banana","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":160,"y":300,"wires":[["aaa307ba.874e58"]]},{"id":"bb012d65.92486","type":"inject","z":"b0555ded.82baa","name":"","topic":"orange","payload":"orange","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":160,"y":360,"wires":[["aaa307ba.874e58"]]},{"id":"e8ac6685.9b2a48","type":"debug","z":"b0555ded.82baa","name":"OUT1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","x":550,"y":240,"wires":[]},{"id":"503899c3.750508","type":"debug","z":"b0555ded.82baa","name":"OUT2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","x":550,"y":300,"wires":[]},{"id":"1053e26d.f8c7ae","type":"debug","z":"b0555ded.82baa","name":"OUT3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","x":550,"y":360,"wires":[]}]
  • 1

5.5.2 流程之间传递函数

  Context元素除了可以传递变量以外,也可以传递函数。我们在流1中新建一个名为hello的函数,在流2中调用它。
  流1的函数节点
这里写图片描述
  流2的函数节点
这里写图片描述
  先点击IN1的输入按钮,再点击IN2的输入按钮,可以看到OUT2中打印出了hello world,其中的“hello”是通过调用流1 的hello()函数打印出来的,说明实验成功了。
这里写图片描述
  代码保存在这里

[{"id":"eaf64e67.5277","type":"inject","z":"3769aa0d.6513d6","name":"IN1","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":210,"y":160,"wires":[["1012b4b4.a746fb"]]},{"id":"d6468738.ed2948","type":"debug","z":"3769aa0d.6513d6","name":"OUT1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","x":530,"y":160,"wires":[]},{"id":"1012b4b4.a746fb","type":"function","z":"3769aa0d.6513d6","name":"hello","func":"context.global.hello = function(){return \"hello\"};\nmsg.payload = context.global.hello();\nreturn msg;","outputs":1,"noerr":0,"x":370,"y":160,"wires":[["d6468738.ed2948"]]},{"id":"8331681d.a15558","type":"function","z":"3769aa0d.6513d6","name":"+world","func":"msg.payload = context.global.hello() + \" World\";\nreturn msg;","outputs":1,"noerr":0,"x":350,"y":260,"wires":[["8214b226.35f87"]]},{"id":"eff72cea.339ae","type":"inject","z":"3769aa0d.6513d6","name":"IN2","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":190,"y":260,"wires":[["8331681d.a15558"]]},{"id":"8214b226.35f87","type":"debug","z":"3769aa0d.6513d6","name":"OUT2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","x":520,"y":260,"wires":[]}]
  • 1

5.5.3 使用状态标志

  在5.4.3小节时,我们第一次使用delay节点,用于产生一个随机数。此节点在在运行的时候显示出了一个表示时间的字符,显示这个随机的时间是多少。我觉得这个功能很有用,很直观的可以显示出我们感兴趣的一些数据或状态,可以省去到处用debug节点打印的烦恼。翻看函数节点的API,正好有一条可以用来显示状态:
  node.status(..) : 更新节点的状态
这里写图片描述
  我们来研究一下如何使用。
  首先要搭建一个可变的输入信息,并且要持续一段时间,避免状态变得太快看不到。相信详细学过函数控件用法以后,对于聪明的你来说,这一定不是难事。——用函数节点+delay节点可以实现。先拖拽节点,形成数据流。
这里写图片描述
  第一个函数节点用于发送5个数字:
这里写图片描述
  Delay节点用于限制数据包的速度,每秒一条。
这里写图片描述
  如此就能实现,第二个函数节点每秒接收到一个数字。
这里写图片描述
  第二个函数节点的代码如下:

node.status({fill:"green",shape:"dot",text:"Received "+msg.payload});
return msg;
  • 1
  • 2

  也很好理解:填充绿色的点,文本显示“Received + 载荷”,只是格式死板一点,照着填写,不要遗漏标点或拼写错误。
接下来点击inject的输入按钮,可以在调试窗口看到每隔一秒收到一个数据。这都是意料中的现象。
这里写图片描述
  同时,第二个函数节点也能显示出一些信息。绿色的点,以及显示内容都是我们代码中自己编写的,所以,任务完成。
这里写图片描述
这里写图片描述
  代码在这里

[{"id":"5e91200b.6cd0c","type":"inject","z":"3769aa0d.6513d6","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":140,"y":120,"wires":[["27878fd7.889b1"]]},{"id":"27878fd7.889b1","type":"function","z":"3769aa0d.6513d6","name":"send1-5","func":"for (var i=0; i<5 ; i++){\n    node.send({payload:i});\n}\nreturn null;","outputs":1,"noerr":0,"x":240,"y":200,"wires":[["b99e8ad0.ce0d28"]]},{"id":"4705bd05.3042f4","type":"debug","z":"3769aa0d.6513d6","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":630,"y":120,"wires":[]},{"id":"b99e8ad0.ce0d28","type":"delay","z":"3769aa0d.6513d6","name":"","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":370,"y":120,"wires":[["1d46ef67.107e81"]]},{"id":"1d46ef67.107e81","type":"function","z":"3769aa0d.6513d6","name":"received1-5","func":"node.status({fill:\"green\",shape:\"dot\",text:\"Received \"+msg.payload});\nreturn msg;","outputs":1,"noerr":0,"x":510,"y":200,"wires":[["4705bd05.3042f4"]]}]
  • 1
  • 2

沒有留言:

張貼留言

Messaging API作為替代方案

  LINE超好用功能要沒了!LINE Notify明年3月底終止服務,有什麼替代方案? LINE Notify將於2025年3月31日結束服務,官方建議改用Messaging API作為替代方案。 //CHANNEL_ACCESS_TOKEN = 'Messaging ...