面向物聯網和創客的 SVG 網頁組件(第 2 部分)

已發表: 2022-03-10
快速總結↬在為物聯網網頁設計界面時,總是有很多選擇。 在本文的前一部分中,Richard Leddy 闡明了 IoT 的含義以及如何使用 Vue.js 來託管 IoT 人機界面組。 今天,讓我們仔細看看延遲加載面板,以及如何讓 Vue 狀態與設備保持同步。

因此,我們已經有了動態加載 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 元素的可見性的屬性。 這是一個例子:

一種以上類型的面板和每種類型的多個動畫實例。
最後,一組不同類型的面板,每個面板都有分配給單獨 MCU 的實例。 (大預覽)

波浪形按鈕面板的每個實例都是獨立的。 所以,有些是開啟的,有些是關閉的。

這段 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 圖片的工具。 無論如何,接下來的步驟可能有很多事情可以做。