面向物联网和创客的 SVG 网页组件(第 2 部分)
已发表: 2022-03-10因此,我们已经有了动态加载 SVG 图标菜单的方法,如果我们愿意,可以通过加载面板做出反应,但图标不是实际组件。 我们能够使用一个简单的技巧,为每个图标引入 SVG 并将其传递给 Vue 应用程序。 生成图标列表非常简单,每个图标的反应方式相似,只是数据差异很小。 数据差异使得将面板的名称绑定到每个图标成为可能,以便图标按钮单击的处理程序可以传递它。
当以 Vue 组件的形式加载面板时,必须加载有关面板及其组件的所有内容、模板、JavaScript 等。 因此,仅管理加载面板的工作比我们在本次讨论中遇到的要大。
让我们看看 Vue 为异步加载提供钩子的方式。 以下片段来自 Vue 指南。
Vue.component('async-example', function (resolve, reject) { setTimeout(function () { // Pass the component definition to the resolve callback resolve({ template: '<div>I am async!</div>' }) }, 1000) })
该指南告诉我们 setTimeout 函数是如何使用 Vue 组件的同步性的一个示例。 请注意,之前有一个对象作为Vue.component
的第二个参数,现在有一个函数,称为工厂函数。 在resolve
回调中是一个组件定义,它之前是Vue.component
的第二个参数。
所以,我不得不盯着这个例子看了一会儿,然后它才对我有意义。 这是另一个更适合我的例子:
Vue.component('async-example', function (resolve, reject) { // Vue will call this function and promise itself to handle // it when it gets back with data. // this function can then call a promising object loader // here the 'loader' function is some abstract function. // Most likely the application will use 'fetch' // but it could be something else. loader('/my/resource/on/server.json'). then(function (JSON_data) { var object = transformJSONToJSObject(JSON_data); resolve(object) }).catch( (error) => { handle it } );
做一个更通用的函数来绕过这个表单似乎是正确的做法。
function componentLoader(c_name,resource_url) { Vue.component(c_name, function (resolve, reject) { loader(resource_url). then(function (JSON_data) { var object = transformJSONToJSObject(JSON_data); resolve(object) }).catch( (error) => { handle it } ); }
所以,一般来说,要加载一个组件,我们只需要如下一行:
componentLoader('ThermoPanel','./JSON/thermo-panel.json');
那么现在,正在加载的 JSON 是什么? 它可以包括有关组件的所有内容。 在这种情况下,作为面板组件,它可以包括温度计、机器开关、滑块、仪表等。 虽然将组件部分保留在网页上似乎更好,但实际上使用较长示例中的子组件字段可能会更好,用于我们之前制作的“热面板”以及其他类似构造的面板。 JSON 将包含一个完整的面板结构。
但是,如果读者注意到包含对transformJSONToJSObject
的函数调用,他就会理解 JSON 可能以某种方式编码以使传输更容易,并使服务器更容易处理定义。 毕竟,定义将包括完整的 SVG 模板、函数定义和其他 JavaScript 表达式。 此外,JSON 对象可能不仅仅包含面板定义,因为某些信息可能只是有助于簿记或验证。 因此,可以预期在收到该对象时会进行一些处理。
至于编码,从服务器进来的数据可以有多种编码方式。 也许它只是简单的 URL 编码。 或者更安全的是,它可能会被加密。 对于这个讨论,我们可以只使用 URL 编码。
一些可用于创建 Vue 应用程序的工具无疑负责 JSON 转换。 但是,到目前为止,该讨论避免使用命令行工具。 这个遗漏并没有那么糟糕,因为我们也用最少的资源使用了 Vue,只使用一个脚本标签来引用 CDN。 但是,我当然建议您查看命令行工具,尤其是用于组织项目的工具。
当 JSON 到达页面时,鉴于该组件已与子组件完全组装在一起,因此无需再做任何工作来获取这些部件。 我们可以假设所有组件都将在接下来的讨论中完全定义。 但是,组装完整的组件层次结构有时需要命令行工具。
SVG 编辑过程也需要一些工作。 SVG 编辑过程允许设计人员绘制面板及其上的所有组件。 但是,每个子组件都必须被识别,在一个组中调用,或者给定一个占位符。 任何使用绘图的方法都需要对 SVG 进行一些处理,以便 Vue 组件标签可以替换组或图形元素。 这样,任何艺术家的渲染都可以成为模板。 而且,绘制的子组件必须被拆解成 Vue 子组件的模板。
这种简约与大多数 JavaScript 框架的工作流程背道而驰。 这些框架是关于组装页面的。 但是,编辑或绘图会产生艺术家已经组装好的东西。 实际上,编辑的结果并没有提供直接对应于框架组件定义的文本文件。
在其他讨论中可能会考虑更多关于编辑过程的信息。 它有很多。 但是,就目前而言,我们拥有加载分层组件并使它们活跃起来所需的工具。
懒惰的应用程序
对于我们的物联网面板结构,我们已经有一个响应搜索的选择栏。 而且,我们有一种在需要时加载组件的方法。 我们只需要连接这些部分。 最后,我们必须确保面板出现并且它们在它们出现时开始工作。
上面异步代码完成的面板延迟加载提供了一个想法的草图。 但是,值得庆幸的是,有些人已经尝试找到确保可以加载各种组件的方法。 有一个 codepen 条目展示了如何使用不同类型的新组件更新 Vue 应用程序。 这是使用不同类型的面板更新页面的指定部分所需的机制。
通过添加不同类型面板的能力和加载其定义的简单机制,我们终于可以拥有我们的面板搜索页面。
这是我们页面中需要的 HTML,以便 Vue 应用程序可以动态放置组件:
<template v-for="(panel, index) in panelList"> <component :is="panel" :key="panel.name"></component> </template>
component
标签是一个 Vue 元标签。 请参阅动态组件的参考。 在这种情况下,用于component
标签的属性、特殊属性是 is 和 key。 is
属性存在于动态组件中。 并且, key
确保了新的孩子彼此之间会有不同的身份,并帮助 Vue 决定要绘制什么。
“同一个共同父母的孩子必须拥有唯一的钥匙。 重复的键会导致渲染错误。”
template
标签将遍历应用程序的panelList
数据字段中提供的组件。
因此,从图标应用程序的应用程序级 Vue 定义开始,我们可以进行更改以将 panelList 包含在数据元素中。 (我们现在称它为 panelApp)。
var panelApp = new Vue({ el: '#PanelApp', data: { iconList: [ // Where is the data? Still on the server. ], panelList: [ ], queryToken : "Thermo Batches" // picked a name for demo }, methods : { goGetPanel: function (pname) { // var url = panelURL(pname); // this is custom to the site. fetch(url).then((response) => { // this is now browser native response.text().then((text) => { var newData = decodeURIComponent(text); eval(pHat); // widgdef = object def, must be assignment pHat = widgdef; var pnameHat = pname + pcount++; pHat.name = pnameHat; // this is needed for the key this.panelList.push(pHat); // now it's there. }).catch( error => { /* handle it */ }); } } });
除了在面板中添加之外, goGetPanel
现在是从数据库或其他商店获取组件定义所需的形式。 服务器端必须小心以正确格式提供 JavaScript 代码。 至于来自服务器的对象是什么样子,我们已经看到了。 它是一种用作Vue.component
参数的对象。
这是 Vue 应用程序的完整主体,它提供了一个菜单作为搜索结果,以及一个放置用户单击图标时从服务器获取的面板的位置。
<div> <!-- Recognize the name from the Vue doc --> <div> <h2 itemprop="name">Request MCU Groups</h2> <p itemprop="description">These are groups satistfying this query: {{queryToken}}.</p> <button>Find All</button> <button>Find 5 Point</button> <button>Find 6 Point</button> </div> <!-- Here is a Vue loop for generating a lit --> <div class="entryart"> <button v-for="iconEntry in iconList" @click="goGetPanel(iconEntry.name)" > <div v-html="iconEntry.icon"> </div> </button> </div> <div class="entryart" > <template v-for="(panel, index) in panelList"> <component :is="panel" :key="panel.name" :ref="panel.name" ></component> </template> </div> </div>
在最后一个div
中, component
标签现在有一个ref
参数绑定到面板名称。 ref 参数允许 Vue 应用程序识别要使用数据更新的组件并保持组件分离。 ref
参数还允许我们的应用程序访问新的动态加载的组件。
在面板应用程序的一个测试版本中,我有以下间隔处理程序:
setInterval(() => { var refall = panelApp.$refs; // all named children that panels for ( var pname in refall ) { // in an object var pdata = refall[pname][0]; // off Vue translation, but it's there. pdata.temp1 = Math.round(Math.random()*100); // make thermos jump around. pdata.temp2 = Math.round(Math.random()*100); } },2000)
该代码提供了一个小动画,随机改变温度计。 每个面板都有两个温度计,该应用程序允许用户不断添加面板。 (在最终版本中,必须丢弃一些面板。)使用panelApp.$refs
refs
引用,Vue 根据component
标签中的引用信息创建一个字段。
所以,这就是随机跳跃的温度计在一张快照中的样子:

将面板连接到 IoT 设备
因此,最后一段代码是setInterval
测试,每两秒用随机值更新温度计。 但是,我们要做的是从真实机器中读取真实数据。 为了做到这一点,我们需要某种形式的沟通。

有多种方式。 但是,让我们使用 MQTT,它是一个发布/订阅消息系统。 我们的 SPWA 可以随时订阅来自设备的消息。 当它收到这些消息时,SPWA 可以将每条消息定向到映射到消息中标识的设备的面板的适当数据处理程序。
所以,基本上我们需要做的是用响应处理程序替换setInterval
。 而且,这将是一个面板。 我们可能希望在加载面板时将它们映射到处理程序。 并且,由 Web 服务器决定是否提供了正确的映射。
一旦 Web 服务器和 SPWA 准备好操作页面,Web 服务器就不再需要处理页面和设备之间的消息传递。 MQTT 协议指定一个路由服务器来处理发布/订阅。 已经制作了许多 MQTT 服务器。 其中一些是开源的。 一个非常流行的是Mosquito ,并且有一些是在 Node.js 之上开发的。
页面的过程很简单。 SPWA 订阅一个主题。 一个好的主题版本是 MCU 的标识符,例如 MAC 地址或序列号。 或者,SPWA 可以订阅所有温度读数。 但是,页面必须完成过滤来自所有设备的消息的工作。 MQTT 中的发布本质上是广播或多播。
让我们看看 SPWA 将如何与 MQTT 交互。
在 SPWA 上初始化 MQTT
有几个客户端库可供选择。 例如,一个是 MQTT.js。 另一个是eclipse paho。 当然还有更多。 让我们使用 Eclipse Paho,因为它有一个 CDN 存储版本。 我们只需在页面中添加以下行:
<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.min.js" type="text/javascript"></script>
MQTT 客户端必须先连接到服务器,然后才能发送和接收消息。 因此,建立连接的行也需要包含在 JavaScript 中。 我们可以添加一个函数MQTTinitialize
,它设置客户端以及连接管理和消息接收的响应。
var messagesReady = false; var mqttClient = null; function MQTTinitialize() { mqttClient = new Paho.MQTT.Client(MQTTHostname, Number(MQTTPort), "clientId"); mqttClient.onMessageArrived = onMessageArrived; // connect the client mqttClient.connect({ onSuccess: () => { messagesReady = true; } }); // set callback handlers mqttClient.onConnectionLost = (response) => { // messagesReady = false; // if (response.errorCode !== 0) { console.log("onConnectionLost:"+response.errorMessage); } setTimeout(() => { MQTTinitialize() },1000); // try again in a second }; }
设置订阅
连接就绪后,客户端可以订阅消息通道,在通道上发送消息等。只需几个例程即可完成将面板与 MQTT 路径连接所需的大部分工作。
对于panel SPWA,可以通过订阅时刻建立panel和topic的关联,MCU标识。
function panelSubcription(topic,panel) { gTopicToPanel[topic] = panel; gPanelToTopic[panel] = topic; mqttClient.subscribe(topic); }
鉴于 MCU 正在发布其主题,SPWA 将收到一条消息。 在这里,Paho 消息被解包。 然后,消息被传递到应用程序机制中。
function onMessageArrived(pmessage) { // var topic = pmessage.destinationName; var message = pmessage.payloadString; // var panel = gTopicToPanel[topic]; deliverToPanel(panel,message); }
所以,现在我们需要做的就是创建deliverToPanel
,它应该有点像我们之前的间隔处理程序。 然而,面板被清楚地识别,并且只有在特定消息中发送的键控数据可以被更新。
function deliverToPanel(panel,message) { var refall = panelApp.$refs; // all named children that panels var pdata = refall[panel][0]; // off Vue translation, but it's there. var MCU_updates = JSON.parse(message); for ( var ky in MCU_updates ) { pdata[ky] = MCU_updates[ky] } }
这个deliverToPanel
函数足够抽象,可以允许任何面板定义具有任意数量的动画数据点。
发送消息
为了完成 MCU 和 SPWA 之间的应用循环,我们定义了一个发送消息的函数。
function sendPanelMessage(panel,message) { var topic = gPanelToTopic[panel]; var pmessage = new Paho.MQTT.Message(message); pmessage.destinationName = topic; mqttClient.send(pmessage); }
sendPanelMessage
函数只不过是在 SPWA 订阅的同一主题路径上发送消息。
由于我们计划让图标按钮负责为单个 MCU 集群引入一些面板,因此需要处理的面板不止一个。 但是,我们要记住每个面板对应一个 MCU,所以我们有一个一对一的映射,我们可以使用两个 JavaScript 映射来映射和逆向映射。
那么,我们什么时候发送消息呢? 通常,面板应用程序在想要更改 MCU 的状态时会发送消息。
保持视图 (Vue) 状态与设备同步
Vue 的一大优点是它很容易使数据模型与用户的活动保持同步,用户可以编辑字段、单击按钮、使用滑块等。可以确定按钮和字段的更改会立即反映在组件的数据字段中。
但是,我们希望在更改发生后立即向 MCU 发送消息。 因此,我们寻求利用 Vue 可能管理的接口事件。 我们试图响应这样的事件,但只有在 Vue 数据模型准备好当前值之后。
我创建了另一种面板,这个面板带有一个颇具艺术感的按钮(可能是受杰克逊·波洛克的启发)。 并且,我着手将其转换为单击将状态报告回包含它的面板的东西。 这不是一个简单的过程。
让我失望的一件事是我忘记了管理 SVG 的一些奇怪之处。 我首先尝试更改样式字符串,以便 CSS 样式的display
字段为“无”或“某物”。 但是,浏览器从不重写样式字符串。 但是,由于这很麻烦,我尝试更改 CSS 类。 那也没有效果。 但是,我们大多数人都记得旧 HTML(可能是 1.0 版)中的visibility
属性,但这在 SVG 中是最新的。 而且,效果很好。 我所要做的就是让按钮单击事件传播。
Vue 将属性设计为向一个方向传播,即父级到子级。 因此,要更改应用程序或面板中的数据,您必须向父级发送更改事件。 然后,您可以更改数据。 控制按钮的数据元素的更改导致 Vue 更新影响我们选择指示状态的 SVG 元素的可见性的属性。 这是一个例子:

波浪形按钮面板的每个实例都是独立的。 所以,有些是开启的,有些是关闭的。
这段 SVG 片段包含看起来很奇怪的黄色指示器:
<path :visibility="stateView" d="m -36.544616,12.266886 c 19.953088,17.062165 5.07961,-19.8251069 5.317463,8.531597 0.237853,28.356704 13.440044,-8.847959 -3.230451,10.779678 -16.670496,19.627638 14.254699,-2.017715 -11.652451,3.586456 -25.90715,5.60417 10.847826,19.889979 -8.095928,-1.546575 -18.943754,-21.436555 -1.177383,14.210702 -4.176821,-12.416207 -2.999438,-26.6269084 -17.110198,8.030902 2.14399,-8.927709 19.254188,-16.9586105 -19.075538,-8.0837048 9.448721,-5.4384245 28.52426,2.6452804 -9.707612,-11.6309807 10.245477,5.4311845 z" transform="translate(78.340803,6.1372042)" />
可见性由stateView
填充,它是一个将状态布尔值映射到 SVG 字符串的计算变量。
这是面板组件定义模板:
<script type="text/x-template"> <div> <control-switch :state="bstate" v-on:changed="saveChanges" ></control-switch> <gauge :level="fluidLevel" ></gauge> </div> </script>
而且,这是 Vue 面板的 JavaScript 定义,它的子组件作为子组件:
var widgdef = { data: function () { var currentPanel = { // at the top level, values controlling children bstate : true, fluidLevel : Math.round(Math.random()*100) } // return currentPanel }, template: '#mcu-control-panel-template', methods: { saveChanges: function() { // in real life, there is more specificity this.bstate = !this.bstate relayToMCU(this.name,"button",this.bstate) // to be defined } }, components: { 'control-switch' : { // the odd looking button props: ['state'], template: '#control-switch-template', // for demo it is in the page. computed: { // you saw this in the SVG above. stateView : function() { return ( this.state ) ? "visible" : "hidden" } }, methods : { // the button handler is in the SVG template at the top. stateChange : function () { // can send this.$emit('changed'); // tell the parent. See on the template instance } } }, 'gauge' : { // some other nice bit of SVG props: ['level'], template: '#gauge-template' } } }
因此,现在已经布置了嵌入面板中的单个按钮的机制。 而且,必须有一个钩子来告诉 MCU 发生了什么事。 它必须在面板组件的数据状态更新后立即调用。 让我们在这里定义它:
function relayToMCU(panel,switchName,bstate) { var message = switchName + ':' + bstate // a on element parameter string. sendPanelMessage(panel,message) }
仅用两行代码就可以改变硬件的状态。
但是,这是一个相当简单的案例。 任何开关都可以看作是对世界上某个硬件的函数调用。 因此,字符串可能包含开关名称和其他几个数据元素。 因此,注册更改的组件方法必须在其中进行一些自定义处理,以便它可以收集面板上的所有数据集并将它们发送到一个命令字符串中。 甚至命令字符串也有点简单。 如果 MCU 非常小,则可能必须将命令字符串转换为代码。 如果 MCU 有很多功能,那么命令字符串实际上可能是 JSON 结构,或者可能是面板托管的所有数据。
在本次讨论中,图标面板上的按钮包含要获取的面板的名称。 这也可以相当简化。 该参数可以代表任何可能存储在企业数据库中的面板,这似乎是有道理的。 但是,也许它是一些公式。 也许,关于面板的信息应该包含在我们从服务器接收到的面板定义中。 无论如何,一旦解决了某些令人头疼的问题,就可以轻松扩展基础知识,例如使 SVG 正确响应点击。
结论
该讨论列出了一些基本步骤和决策,这些步骤和决策导致实现可以与 IoT 设备交互的单页 Web 应用程序 (SPWA)。 我们现在知道如何从 Web 服务器获取面板并将它们转换为 MCU 接口。
这个讨论还有很多内容,可能还会有很多其他讨论。 从 Vue 开始是一件需要考虑的事情。 但是,还有整个 MCU 的故事,我们只是简单地谈到了。
特别是,通过选择 MQTT 作为通信基板,我们假设另一端的 IoT 设备可以以某种方式被 MQTT 统治。 但是,情况可能并非总是如此。 如果 MQTT 要访问具有串行链路或蓝牙的设备,有时需要网关。 或者,也许网页上需要的只是 WebSockets。 尽管如此,我们还是使用 MQTT 作为示例来展示 Vue 如何在保持数据状态与设备同步的同时接收和发送数据。
再一次,我们只有故事的一部分。 这次是为了同步,因为页面应该能够处理警报并在发生关键事件时打扰用户。 有时消息可能会丢失。 所以,我们必须有一个确认机制。
最后,我认为 Vue 在收到数据后更新数据非常优雅。 但是,发送状态变化并不是那么简单。 它似乎并没有使这项工作比使用 vanilla JavaScript 完成的简单得多。 但是,有一种方法,它是有道理的。
也许可以构建一个干净的库来为所有面板制作一套通用的组件。 已经简要提到了制作此类库并将它们存储在数据库中的元素。 可能必须开发不仅仅制作 SVG 图片的工具。 无论如何,接下来的步骤可能有很多事情可以做。