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

已發表: 2022-03-10
快速總結 ↬物聯網正在發展到包括擁有許多所有者的許多設備。 Web 開發人員將面臨尋找使所有者能夠與其設備交互的方法的問題。 但是,這個問題引起了大量的業務。 讓我們探討一下已經有需求的物聯網 (IoT) 網頁開發的某些方面。

物聯網市場仍處於早期階段,但正在蓄勢待發。 我們正處於物聯網歷史的風口浪尖。 從 2015 年到 2020 年,市場在五年內翻了兩番。對於 Web 開發人員來說,這種物聯網增長意義重大。 對物聯網網絡技術的需求已經很大。

許多設備將在地理空間上分佈,其所有者將希望進行遠程控制和管理。 必須製作完整的網絡堆棧才能創建遠程操作通道。 此外,交互將一次與一個或多個物聯網設備進行。 交互必須是物理世界的實時。

本次討論使用 Vue.js 作為催化劑深入探討了接口需求,並從許多替代品中說明了一種網頁到設備通信的方法。

以下是本次討論計劃的一些目標:

  1. 創建一個單頁 Web 應用 SPWA,託管 IoT 人機界面組(我們可以將這些稱為“面板組”);
  2. 作為查詢服務器的結果顯示面板組標識符列表;
  3. 作為查詢的結果顯示選定組的面板;
  4. 確保面板顯示延遲加載并快速變為動畫;
  5. 確保面板與物聯網設備同步。
跳躍後更多! 繼續往下看↓

物聯網和網頁的快速增長

用於可視化和遠程控制硬件的圖形呈現以及網頁與實時物理過程的同步屬於物聯網未來固有的網頁問題解決領域。

我們中的許多人都在開始尋找物聯網表示技術,但是我們現在可以開始使用一些 Web 標準以及一些表示技術。 當我們一起探索這些標準和技術時,我們可以加入這個物聯網浪潮。

需要儀表板和數據可視化。 此外,超越提供表單或顯示列表或文本內容的網頁的需求很高。 物聯網儀表板需要是像形的、動畫的。 動畫必須與實時物理過程同步,以便為用戶提供機器狀態的真實視圖。 機器狀態(例如火焰是否燃燒)勝過應用程序狀態,並為操作員提供關鍵信息,甚至可能是安全信息。

儀表板需要的不僅僅是數據的可視化。 我們必須記住,物聯網的一部分是不僅具有傳感器而且還具有控制接口的設備。 在硬件實現中,MCU 擴展了開關、閾值開關、參數設置等。 不過,網頁可能會取代那些硬件控制組件

沒什麼新鮮的。 硬件的計算機接口已經存在了很長時間,但是這些接口的網頁使用的快速增長是我們目前經驗的一部分。 WebRTC 和 Speech API 正處於 2012 年開始的開發路徑上。WebSockets 的開發時間也差不多。

物聯網在我們腦海中已經存在很長時間了。 自 1832 年以來,物聯網一直是人類對話的一部分。但是,眾所周知,物聯網和無線是特斯拉在 1926 年左右設想的。福布斯 2018 年物聯網狀態告訴我們物聯網當前的市場焦點。 Web 開發人員感興趣的文章提到了儀表板:

“物聯網早期採用者或倡導者優先考慮儀表板、報告、物聯網用例,這些用例提供分析、高級可視化和數據挖掘不可或缺的數據流。”

物聯網市場巨大。 這篇市場規模文章對將出現的設備數量進行了預測:2018 年:231.4 億 ⇒ 2025 年:754.4 億。 而且,它試圖在上面加上一個財務數據:2014 年:2.99 萬億美元⇒ 2020 年:8.90 萬億美元。 對物聯網技能的需求將是增長最快的:物聯網需求。

當我們為控制和監視設備開發清晰的接口時,我們在開發接口時遇到了一個新問題。 所有數十億台設備將由許多人(或組織)擁有。 此外,每個人都可以擁有任意數量的設備。 甚至可能會共享一些設備。

為機器控製而設計的現代界面通常具有特定於特定機器或幾台機器安裝的明確定義的佈局。 例如,在智能家居中,高端系統將配備 LCD 面板,用於精心放置的設備。 但是,隨著我們隨著物聯網網絡版本的發展,將有任意數量的面板用於動態甚至移動設備流。

設備面板的管理變得類似於管理社交網站上的社交聯繫。

“我們的用戶界面必須是動態的,以管理必須在任何時候為每個特定用戶顯示哪個高度動畫的實時面板。”

儀表板是單頁 Web 應用程序 SPWA。 而且,我們可以想像一個面板數據庫。 因此,如果單個用戶要訪問其遍布全球的設備的多個面板和配置,則 SPWA 需要按需訪問面板組件。 面板和它們的一些支持 JavaScript 將不得不延遲加載。

“我們的界面必須與網頁框架配合使用,這些框架可以允許在不重新初始化其框架的情況下合併異步組件綁定。”

讓我們使用 Vue.js、WebSockets、MQTT 和 SVG 邁向物聯網市場。

推薦閱讀使用 Vue.js 構建交互式信息圖

IoT Web 應用程序的高級架構

在設計物聯網網頁的界面時,總是有很多選擇。 一種選擇可能是將一頁專用於一台設備。 該頁面甚至可能在服務器端呈現。 服務器將負責查詢設備以獲取其傳感器值,然後將這些值放入 HTML 字符串中的適當位置。

我們中的許多人都熟悉允許使用特殊標記編寫 HTML 模板的工具,這些標記指示變量值的放置位置。 在這樣的模板中看到{{temperature}}告訴我們和視圖引擎獲取從設備查詢的溫度並用它替換{{temperature}}符號。 所以,在等待服務器查詢設備、設備響應、渲染頁面、下發頁面後,用戶終於可以看到設備上報的溫度了。

對於每個設備架構的此頁面,用戶可能希望向設備發送命令。 沒問題,他可以填寫 HTML 表單並提交。 服務器甚至可能有一個只針對設備的路由,或者更巧妙一點,一個針對設備類型和設備 ID 的路由。 然後,服務器會將表單數據轉換為要發送到設備的消息,將其寫入某個設備處理程序並等待確認。 然後,服務器最終可能會響應發布請求並告訴用戶設備一切正常。

一種將物聯網視為表單服務器的網頁架構——尋找更好的東西。
一種將物聯網視為表單服務器的網頁架構——尋找更好的東西。 (大預覽)

許多 CMS 以這種方式工作以更新博客條目等。 它似乎沒有什麼奇怪的。 似乎 HTML over HTTP 的設計總是用於獲取已呈現的頁面並發送表單數據以供 Web 服務器處理。 更重要的是,有數以千計的 CMS 可供選擇。 因此,為了讓我們的物聯網系統正常運行,瀏覽這數千個 CMS 以查看哪一個適合這項工作似乎是合理的。 或者,我們可以先在 CMS 上應用一個過濾器。

我們必須考慮我們正在處理的實時性。 因此,雖然原始形式的 HTML 對於許多企業任務來說非常好,但它需要一點幫助才能成為物聯網管理的交付機制。 因此,我們需要一個 CMS 或自定義 Web 服務器來幫助 HTML 完成這項 IoT 工作。 我們也可以只考慮服務器,因為我們假設 CMS 提供服務器功能。 我們只需要記住,服務器必須提供事件驅動的動畫,因此頁面不能 100% 完成靜態打印。

以下是一些參數,可能會指導我們的設備鏈接網頁的選擇,它應該做的事情:

  1. 異步接收傳感器數據和其他設備狀態消息;
  2. 在客戶端渲染頁面的傳感器數據(幾乎是 1 的推論);
  3. 將命令異步發佈到特定設備或設備組;
  4. 可選擇通過服務器發送命令或繞過它。
  5. 安全維護設備與用戶的所有權關係;
  6. 通過不干涉或超控來管理關鍵設備操作。

當考慮將一頁用作選定設備的接口時,就會想到該列表。 當涉及到命令和數據時,我們希望能夠與設備自由通信。

至於頁面,我們只需要向 Web 服務器請求一次。 我們希望 Web 服務器(或相關應用程序)能夠提供安全的通信路徑。 而且,路徑不一定要通過服務器,或者它應該完全避開服務器,因為服務器可能有更高優先級的任務,而不是處理來自傳感器的數據的一頁通信。

事實上,我們可以想像每秒一次來自傳感器的數據,並且我們不會期望 Web 服務器本身為數千個單獨的傳感器流乘以數千個查看器提供恆定的秒更新。 當然,可以在負載平衡框架中對 Web 服務器進行分區或設置,但還有其他服務是針對傳感器交付和將命令編組到硬件而定制的。

Web 服務器將需要傳遞一些數據包,以便頁面可以與設備建立安全的通信通道。 我們必須小心在不提供對所通過消息類型的某種管理的通道上發送消息。 必須了解設備是否處於可以中斷的模式,或者如果設備失控,可能需要用戶採取行動。 因此,Web 服務器可以幫助客戶端獲取適當的資源,從而可以更多地了解設備。 消息傳遞可以通過 MQTT 服務器之類的東西來完成。 並且,當用戶通過 Web 服務器訪問他的面板時,可以啟動一些用於準備 MQTT 服務器的服務。

由於具有實時要求的物理世界以及額外的安全考慮,我們的圖表與原始圖表有些不同。

與一個 MCU 對話的單頁應用程序。
與一個 MCU 對話的單頁應用程序。 它現在獨立於網頁服務器與 MCU 進行異步交互。 (大預覽)

我們不能停在這裡。 為每個設備設置一個頁面,即使它具有響應性並且可以很好地處理通信,這也不是我們所要求的。 我們必須假設用戶將登錄到他的帳戶並訪問他的儀表板。 從那裡,他會要求提供一些內容項目列表(最有可能是他正在從事的項目)。 列表中的每個項目都將引用許多資源。 當他通過單擊或點擊選擇一個項目時,他將訪問一組面板,每個面板都將包含有關特定資源或物聯網設備的一些信息。

響應於作為用戶界面動作的結果而生成的查詢而交付的任意數量的面板可以是與現場設備交互的那些面板。 因此,一旦面板出現,它將顯示實時活動並能夠向設備發送命令。

如何在頁面上看到面板是一個設計決定。 它們可能是浮動窗口,也可能是可滾動背景上的框。 無論如何呈現,面板將記錄時間、溫度、壓力、風速或您能想像到的任​​何其他內容。 我們希望面板能夠根據各種圖形比例進行動畫處理。 溫度可以用溫度計表示,速度可以用半圓形速度計表示,聲音可以用流動波形表示,等等。

Web 服務器的任務是向正確的用戶提供正確的面板,給面板數據庫提供查詢,並且設備必須在物理上可用。 更重要的是,鑑於會有許多不同類型的設備,每個設備的面板可能會有所不同。 因此,Web 服務器應該能夠提供渲染面板所需的象形信息。 但是,儀表板的 HTML 頁面不必加載所有可能的面板。 不知道會有多少。

以下是一些可能指導我們的儀表板頁面選擇的參數,它應該做的事情:

  1. 提供一種選擇相關設備面板組的方法;
  2. 對一些設備使用同時設備通信機制;
  3. 當用戶請求時激活設備面板;
  4. 為獨特的面板設計加入延遲加載的圖形;
  5. 針對每個面板使用安全令牌和參數;
  6. 與用戶檢查的所有設備保持同步。
與多個 MCU 進行異步通信且獨立於網頁服務器的單頁應用程序。
與多個 MCU 進行異步通信且獨立於網頁服務器的單頁應用程序。 (大預覽)

我們可以開始看到遊戲是如何變化的,但在儀表板設計的世界中,遊戲在一段時間內一直在變化。 我們只需要將自己的範圍縮小到一些最新且有用的頁面開發工具,就可以讓自己振作起來。

讓我們從如何渲染面板開始。 這似乎已經是一項艱鉅的工作了。 我們正在想像許多不同種類的面板。 但是,如果您曾經使用過音樂 DAW,您會看到他們如何使用圖形使面板看起來像很久以前樂隊使用的模擬設備。 DAW 中的所有面板都是由對聲音進行操作的插件繪製的。 事實上,很多 DAW 的插件可能會使用 SVG 來渲染它們的界面。 因此,我們將自己限制在處理 SVG 接口上,而這反過來又可以是我們能想像到的任​​何圖形。

為面板選擇 SVG

當然,我喜歡 DAW 並會以它為例,但 SVG 是一種網頁標準。 SVG 是 W3C 標準。 它用於將線條圖傳送到網頁。 SVG 曾經是網頁上的二等公民,必須存在於 iFrame 中。 但是,自 HTML5 以來,它一直是一等公民。 也許,當 SVG2 出現時,它就可以使用表單元素了。 目前,表單元素是 SVG 中的外來對象。 但是,這不應該阻止我們將 SVG 用作面板的基板。

SVG 可以繪製、存儲以供顯示,也可以延遲加載。 事實上,當我們探索組件系統時,我們將看到 SVG 可用於組件模板。 在本次討論中,我們將使用 Vue.js 為面板製作組件。

畫SVG並不難,因為有很多畫線程序很容易搞定。 如果你花錢,你可以得到導出 SVG 的 Adob​​e Illustrator。 一段時間以來,Inkscape 一直是 SVG 創建的首選。 它是開源的,在 Linux 上運行良好,但也可以在 Mac 和 Windows 上運行。 然後,有幾個網頁 SVG 編輯程序是開源的,還有一些 SaaS 版本。

我一直在尋找一個基於 Web 的開源 SVG 編輯器。 環顧四周後,我發現了 SVG-Edit。 你可以將它包含在你自己的網頁中,也許如果你正在製作一個基於 SVG 的博客或其他東西。

SVG 中的電氣圖準備動畫。
電氣圖非常詳細,但我們可以很容易地在 SVG 中獲得它,並且只需要一點代碼就可以對其進行動畫處理。 (大預覽)

當您將作品保存到文件中時,SVG-Edit 會將其下載到您的瀏覽器中,您可以從下載目錄中獲取該文件。

我繪製的圖片顯示了一個控制積分器的與門。 這不是人們通常期望在 MCU 面板中看到的。 面板可能有一個按鈕來提供與門輸入之一,也許。 然後它可能有一個來自 ADC 的顯示器,用於讀取積分器的輸出。 也許這將是時間軸上的折線圖。 大多數面板都有圖形,允許用戶與 MCU 內部發生的事情相關聯。 而且,如果我們的電路要在任何地方使用,它將在 MCU 內部。

儘管如此,我們的電子圖可以用來討論動畫。 我們想要做的是查看 SVG,看看我們可以從哪裡獲得一些我們希望以某種方式更改的 DOM 標籤。 然後,我們可以使用一點原生 JavaScript 和計時器來為 SVG 設置動畫。 讓與門以不同的顏色閃爍。

我們正在尋找的 SVG 在以下代碼框中。 它對程序員來說看起來不是很友好,儘管用戶會很高興。 儘管如此,仍然有一些線索可以找到我們希望操作的 DOM 元素。 首先,大多數 SVG 繪圖工具都有獲取對象屬性的方法,特別是id屬性。 SVG-Edit 也有辦法。 在編輯器中,選擇 AND 門並觀察工具欄。 您還將看到id和 CSS class的字段。

一種 SVG 繪圖工具,可以使用提供的接口捕獲對象 ID。
一種 SVG 繪圖工具,可以使用提供的接口捕獲對象 ID。 (大預覽)

如果由於某種原因無法使用編輯工具,您可以在瀏覽器中打開 SVG 並檢查 DOM。 無論如何,我們發現我們的門有id = “svg_1”。

 <svg width="640" height="480" xmlns="https://www.w3.org/2000/svg" xmlns:svg="https://www.w3.org/2000/svg"> <g class="layer"> <title>Layer 1</title> <path d="m80.59881,87.020171l14.714795,0m-14.714793,-11.938687l14.714797,0.000004m-0.033867,-6.543869l0,24.758504c42.377882,2.221929 43.364812,-27.139117 0,-24.758504zm47.366321,12.333056l-15.303943,0m-48.188699,-6.489897l1.454753,0l0,1.454751l-1.454753,0l0,-1.454751zm-0.068425,11.869359l1.454753,0l0,1.454753l-1.454753,0l0,-1.454753zm63.545246,-6.089294l1.454751,0l0,1.454751l-1.454751,0l0,-1.454751z" fill="#FF0000" stroke="#000000"/> <path d="m48.58886,119.662231l18.234678,0l2.523043,-7.173309l4.128604,13.808613l4.587337,-13.987948l4.013933,13.808613l4.35797,-13.629278l4.35797,13.718944l2.408353,-6.72497l18.349357,0m-64.482612,-0.623112l1.515724,0l0,1.515728l-1.515724,0l0,-1.515728zm64.484275,-0.103111l1.515721,0l0,1.515728l-1.515721,0l0,-1.515728z" fill="#FF0000" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" transform="rotate(90.3367 80.0675 119.304)"/> <polygon cx="108.5" cy="79.5" edge="0" fill="#ffffff" orient="x" shape="regularPoly" sides="3" strokeWidth="null" strokecolor="#000000"/> <polygon cx="215.5" cy="192.5" edge="0" fill="#ffffff" orient="x" shape="regularPoly" sides="3" strokeWidth="null" strokecolor="none"/> <polygon cx="165.5" cy="164.5" edge="0" fill="#ffffff" orient="x" shape="regularPoly" sides="3" strokeWidth="null" strokecolor="none"/> <polygon cx="161.5" cy="138.5" edge="0" fill="#ffffff" orient="x" shape="regularPoly" sides="3" strokeWidth="null" strokecolor="none"/> <polygon cx="160.5" cy="161.5" edge="0" fill="#ffffff" orient="x" shape="regularPoly" sides="3" strokeWidth="null" strokecolor="none"/> <g> <path d="m225.016923,53.008793l0,3.419331m-4.558966,-1.709666l9.11791,0m10.303228,4.235512l-25.770656,0m-34.429182,0l24.544724,0m0.220544,-4.058194l1.543807,0l0,8.164451l-1.543807,0l0,-8.164451zm7.939567,-4.473673l1.543805,0l0,16.999955l-1.543805,0l0,-16.999955zm-34.176663,8.126854l1.474036,0l0,0.747515l-1.474036,0l0,-0.747515zm61.677552,0.018809l1.474038,0l0,0.747515l-1.474038,0l0,-0.747515z" fill="#FF0000" sides="3" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-width="null"/> <polygon cx="171.5" cy="159.5" edge="43.256342" fill="#ffffff" orient="x" points="223.47406005859375,91.5 186.01296997070312,113.128173828125 186.01296997070312,69.871826171875 223.47406005859375,91.5 " shape="regularPoly" sides="3" stroke="#000000" stroke-width="null" strokeWidth="null" strokecolor="#000000"/> <line fill="none" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-width="null" x1="171" x2="186" y1="103.5" y2="103.5"/> <path d="m130.801817,80.659041l15.333707,0l2.12165,-4.564833l3.47178,8.787299l3.857534,-8.901421l3.375353,8.787299l3.664657,-8.673176l3.664657,8.730237l2.025206,-4.279526l15.430142,0m-54.224016,-0.396526l1.274586,0l0,0.964554l-1.274586,0l0,-0.964554zm54.225414,-0.065616l1.274584,0l0,0.964554l-1.274584,0l0,-0.964554z" fill="none" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-width="null"/> <line fill="none" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-width="null" x1="171.5" x2="171.5" y1="103.75" y2="135.388167"/> <line fill="none" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-width="null" x1="177.75" x2="177.75" y1="58.75" y2="80.255951"/> <line fill="none" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-width="null" x1="223.75" x2="266.854524" y1="91.75" y2="91.75"/> <line fill="none" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-width="null" x1="241.75" x2="241.75" y1="59.75" y2="91.754167"/> <line fill="none" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-width="null" x1="168.25" x2="180.75" y1="135.75" y2="135.75"/> <line fill="none" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-width="null" x1="169.75" x2="179.25" y1="138.5" y2="138.5"/> <line fill="none" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" x1="171" x2="179.75" y1="141.25" y2="141.25"/> </g> </g> </svg>

我們現在只需要一點 JavaScript。 我們首先註意到元素屬性“fill”的存在。 然後就是下面的簡單程序:

 <html> <head> </head> <body> <!-- ALL THE SVG FROM ABOVE GOES HERE --> </body> <html> </svg> <script> // Set up a timer interval flash the color. var gateElement = document.getElementById("svg_1"); if ( gateElement ) { setInterval( () => { var fillC = gateElement.getAttribute("fill"); gateElement.setAttribute("fill", (fillC == "#00FF00") ? "#FF0000" : "#00FF00" ); }, 2000 ) } </script>

請注意,我們擁有的是一個最小的 HTML 頁面。 您可以將代碼剪切並粘貼到您喜歡的編輯器中。 並且,不要忘記剪切和粘貼 SVG 以替換註釋。 我的 Chrome 版本要求頁面是 HTML 才能有 JavaScript 部分。 所以,這是一個仍然將 SVG 視為獨立的瀏覽器的瀏覽器。 但是,距離<iframe>時代還有很長的路要走。

如果你剪切和粘貼得恰到好處,你可以調出頁面並看到與門一遍又一遍地從紅色變為綠色。

推薦閱讀SVG圓分解到路徑

使用 VUE 組件構建面板

我們已經在讓任何一個面板變得活躍起來,但如果我們想以合理的方式管理大量面板集合,我們就會為我們完成工作。 如果我們只是在第一個示例的基礎上構建,情況尤其如此。

雖然第一個示例向我們展示瞭如何異步更改對象視圖,但它沒有向我們展示如何將視圖與任何數據對象的狀態聯繫起來,更不用說管理機器的數據對象了。 我們當然可以理解setInterval演示如何被fetch處理程序替換,但我們甚至可能無法從為包含 SVG 的頁面提供服務的 Web 服務器獲取機器的狀態。 此外,當我們獲取數據時,我們的程序現在需要了解給定頁面的 DOM 結構。

幸運的是,Vue等框架已經流行起來,它們可以為我們節省很多工作。

很容易找到關於 Vue 的信息。 Vue 文檔非常易於訪問。 所以,如果這個討論太過分了,那麼你可能會花一些時間在它自己的網站上學習 Vue。 但是,在 Smashing 頁面中有很好的討論。 Krutie Patel 寫了一篇關於製作信息圖的精彩文章。 Souvik Sarkar 告訴我們如何使用 Vue 構建天氣儀表板。

相關面板分組選擇

第一步,我們應該解決搜索面板組的問題。 首先這樣做的一個原因是它處於我們人類交互的框架級別。

用戶搜索他感興趣的東西。也許他對一個城鎮中位置的所有設備感興趣。 也許他有很多批次的液體產品,他想縮小到一種產品類型,每批產品都由一小部分物聯網設備管理。 因此,用戶將首先搜索以獲取一個小列表。

這是過程:

  1. 按功能/參數搜索面板組。
  2. 查看代表組的圖標列表。
  3. 選擇一個圖標(單擊/點擊)。
  4. 當它們出現時開始使用帶有圖標的面板。

這是一個很好的第一步的另一個原因是我們可以以最簡單的形式使用 Vue。 無需構建工具。 我們只會在 HTML 中包含帶有 script 標籤的vue.js 事實上,我們甚至不必下載它。 有一個站點正在提供vue.js的工作副本。

我們只需要以下標籤:

 <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>

我直接從關於安裝的 Vue 文檔中復制了腳本標籤。

現在,我們需要一個可以加載圖標並將它們變成可以點擊的東西的網頁。 Vue 讓這一切變得非常簡單。 事實上,我只是寫了一個小應用程序來使用 Vue 管理 Twitter 列表。 它只管理文本字段。 由於它比使用圖標的SPWA簡單一點,我們可以看一下它,然後將其更改為我們想要的單頁應用程序框架。

這是頁面外觀的一部分:

一個基於文本的頁面,用作構建圖形應用程序的起點。
一個基於文本的頁面,用作構建圖形應用程序的起點。 (大預覽)

這看起來是一個相當簡單的頁面。 每個外部數字條目是一個時間段,其中包含一兩條推文。 第二條推文是可選的。 如果你編輯一條推文,Vue 機制會更新一個 JavaScript 對象。 該頁面由用戶單擊“更新條目”按鈕以通過其按鈕處理函數告訴服務器某些內容已更改。

為了讓按鈕處理程序將數據中繼到服務器,它必須將 Vue 數據對象更改為 JSON 字符串。 現在,您可能想知道將 Vue 對象轉換為 JSON 會有多困難。 原來是一行代碼。 您可以在下面的源代碼中找到該行,但如果您想更快地找到它,它會在源代碼後面的段落中突出顯示。

頁面看起來很簡單。 人不可貌相。 當然,頁面看起來很簡單,但是代碼簡單嗎? 是的,確實如此! 使用 Vue,頁面幾乎可以神奇地管理字段的內容。 這是代碼:

 <!DOCTYPE html> <html lang="en" prefix="og: https://ogp.me/ns#"> <!-- define microdata scope and type --> <head itemscope itemtype="https://schema.org/Article"> <title>Tweet Keeper</title> <style> body { margin: 2em; } .entryart { border: solid 1px navy; width: 80%; padding: 2px; padding-left: 6px; margin-bottom: 3px; background-color: #EEF4EE; } </style> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script> </head> <body onload="GetTweets()"> <!-- some old fashioned handling --> <!-- The Vue app starts here. This is the HTML part of the Vue object --> <div> <!-- Recognize the name from the Vue doc --> <div itemscope itemtype="https://schema.org/Article"> <h1 itemprop="name">mangage tweets</h1> <p itemprop="description">My personal Tweet engine. This page accesses a personal tweet page that belongs to {{tweetOwner}}.</p> <!-- {{tweetOwner}} is in the data model. --> <button>Update Entries</button> </div> <!-- Here is a Vue loop for generating a lit --> <ol> <li v-for="tweet in tweets"> <!-- here is the first tweet represented as an object with a lable and tweet text --> <div class="entryart"> <input v-model="tweet.def[0].label" /> <input v-model="tweet.def[0].tweet" /> </div> <!-- here is the second tweet in the slot. But, notice that it is optional. --> <div class="entryart" v-if="tweet.def.length > 1"> <input v-model="tweet.def[1].label"/> <input v-model="tweet.def[1].tweet"/> </div> </li> </ol> </div> <script> var twtApp = new Vue({ el: '#tweetAppDiv', data: { tweets: [ // Where is the data? Still on the server.s ], tweetOwner : "Lucky Dude" // picked a name for demo } }); </script> </body> </html> <script> // Notice that you don't have to do everything in the Vue framework. // Here we are using some native API calls var gDefaultPostInfo = { // there server is beyond simple - an example from node.js docs method: 'POST', // or 'PUT' mode: "cors", // no-cors, cors, *same-origin cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached credentials: "same-origin", // include, *same-origin, omit redirect: "follow", // manual, *follow, error referrer: "no-referrer", // no-referrer, *client body: "", headers:{ 'Content-Type': 'application/json' } } // // // recall the "onload" function GetTweets(event) { var url = "https://localhost:8080/twitlist1.json" // We have a fixed file name. fetch(url).then((response) => { // this is now browser native response.text().then((text) => { var newData = JSON.parse(text); // DATA UPDATE! This is it. twtApp.tweets = newData // the page update right away with new data. }); }); } function sendTweets() { // recall the button up above. This is not a Vue style button, but still in the Vue app. var url = "https://localhost:8080/" var data = twtApp.tweets; // GET THE DATA OUT OF VUE. That's all folks. // // so happens that Vue pulls out the right data and stringifies it. var jdata = JSON.stringify(data); // data can be `string` or {object}! // gDefaultPostInfo.body = jdata; // that's for fetch - not Vue related // fetch(url,gDefaultPostInfo).then(res => { // We use fetch to POST as well as GET res.json() }).then(response => { console.log('Success:', JSON.stringify(response)) // promises }).catch(error => { console.error('Error:', error) }); } // // // </script>

因此,為了突出顯示框架強大功能的驚人線條,讓我們在這裡重複一遍:

A. 這是提取數據。

 postOptionsObject.body = JSON.stringify(twtApp.tweets);

B. 這是將數據放入 Vue 並看到屏幕更新:

 twtApp.tweets = JSON.parse(text) // text is the server response

那是多少工作量?

看起來將有一種很好的方式來表達數據將如何更新物聯網面板。

現在,讓我們將推文變成可點擊的圖標,用於從 Web 服務器獲取組件。

從推文到面板獲取圖標

人們喜歡將 SVG 用於圖標。 據我所知,他們更喜歡 SVG 的這種用途。 我只介紹銷售或贈送 SVG 圖標的網站數量。 賣點是線條圖形的字節數比圖像少。 而且,如果我要詢問具有類似按鈕行為的圖片列表,我可能會在 SVG 在 iframe 中的日子裡抓取 PNG 或 JPEG。 但是,我們甚至可以在 Vue 貢獻者列表中找到幫助我們提供圖標服務的庫。

We can turn the tweets page into an icon list returned as a search result. Just a little code has to be changed. Of course, there are a few things to be careful about if we want SVG icons to be loaded as buttons. Vue provides mechanisms for putting HTML into the application. These mechanisms have to be used or DOM elements fetched from the server don't get interpreted.

Here is the kind of rendering you can get from view if you follow your first impulse in creating a handlebars style variable location in the application DOM.

Vue will quote the HTML an insert it as text.
Vue will quote the HTML an insert it as text. (大預覽)

Here is the code that produces the result in the picture:

 <div> <div class="entryart"> <span class="oneItem" v-for="icon in iconList"> {{icon}} </span> </div> </div> <script> var iconApp = new Vue({ el: '#iconAppTry', data: { iconList: [ // Where is the data? Still on the server. ], queryToken : "Thermo Batches" // picked a name for demo } }); </script>

Notice that we have gone from looping over tweets to looping over icons. tweet in tweets changed into icon in iconList . Our twtApp hooks into the DOM element #tweetAppDiv , while our iconApp hooks into the DOM element #iconAppTry . Within the Vue option object, the data subobject has a tweets in the first app, and iconList in the second. The fields are both empty arrays that receive data when the fetch routine does its job.

But, we have imitated our tweet app too closely. In the code above, the iconList is an array, and the server is expected to send an array of strings. So, let's say the server has sent us HTML, and we have it properly decoded with the array assigned to data.iconList . Then, the picture above can be seen.

Now, let's change the code just a little. In this revised code, we can see the following:

 v-html="icon">

Vue responds to the v-html syntax by putting in the DOM of the icon element. Notice that the syntax is included after the loop directive as another attribute to the span tag.

By removing the handlebars syntax and using v-html , our picture changes to something more comprehensible:

 <div> <div class="entryart"> <span class="oneItem" v-for="icon in iconList" v-html="icon"> </span> </div> </div> <script> var iconApp = new Vue({ el: '#iconAppTry2', data: { iconList: [ // Where is the data? Still on the server. ], queryToken : "Thermo Batches" // picked a name for demo } }); </script> 
Using the right directive, Vue inserts DOM, resulting in the rendering of desired graphics.
Using the right directive, Vue inserts DOM, resulting in the rendering of desired graphics. (大預覽)

While v-html is a quick way to do things, the Vue team recommends using components to get the desired HTML into the page. That seems like a good idea, and we shall soon set about doing that.

But, let's use the v-html syntax for our next example.

It's time to set up our working example for fetching SVG icons. Let's have those icons be responsive to a button click. Once those are working, we can get the panels associated with an icon.

Let's suppose that the SVG required for icons is stored in a database. For our example, we can just fetch a JSON file from the server. The grown-up version of the icon server would store many such files in a database, and deliver them to the page with the same mechanisms.

Also, it's best if the SVG arrives on the page URL encoded since we will be using JSON parse. The SVG can be decoded by calling JavaScript's decodeURIComponent function.

In order to simulate the response to searching, we can make use of several JSON files. The page can have one button for each file. Here is the code for the page:

 <!DOCTYPE html> <html lang="en" prefix="og: https://ogp.me/ns#"> <!-- define microdata scope and type --> <head itemscope itemtype="https://schema.org/Article"> <title>Search Bar</title> <style> body { margin: 2em; } div { margin: 6px; } .entryart { border: solid 1px navy; width: 80%; padding: 2px; padding-left: 6px; margin: 2px; margin-bottom: 3px; background-color: #EEF4EE; } .oneItem { background-color: #EEFFFF; margin: 2px; padding: 4px; border: solid 1px purple; } </style> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script> </head> <body> <!-- some old fashioned handling --> <!-- The Vue app starts here. This is the HTML part of the Vue object --> <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> <!-- {{tweetOwner}} is in the data model. --> <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> <script> var iconApp = new Vue({ el: '#iconAppTry', data: { iconList: [ // Where is the data? Still on the server. ], queryToken : "Thermo Batches" // picked a name for demo }, methods : { goGetPanel: (pname) => { // `this` inside methods points to the Vue instance alert('Hello ' + pname + '!') } } }); </script> </body> </html> <script> // // recall the "onclick" on the <buttons> function GetIcons(points) { // special file names instead of search parameters // var url = (points == 11) ? "https://localhost:8080/batchQuery-all.json" : ((points == 5) ? "https://localhost:8080/batchQuery-five.json" : "https://localhost:8080/batchQuery-six.json") fetch(url).then((response) => { // this is now browser native response.text().then((text) => { var newData = JSON.parse(text); // DATA UPDATE! This is it. newData = newData.map(obj => { obj.icon = decodeURIComponent(obj.icon); return(obj) }); iconApp.iconList = newData; // the page update right away with new data. }); }); } </script>

Here is one display of icons that have been fetched from the server:

Icons that might be returned from a search for MCU groups.
An artistic idea suggesting how search could return icons indicating certain groups of MCU's to interact with. (大預覽)

The data being sent is an array with the following kind of structure:

{ "style" : { "color" : "red", "backgroundColor" : "yellow" }, "icon" : svg1, "name" : "thermos" },

在這裡, svg1是從文件中獲取的 SVG。 當然,正義的服務器會從數據庫中獲取結構,其中 SVG 將存儲在結構中。

這是上面代碼的一個片段。 這是獲取 JSON 並將結構數組放入 Vue 應用程序的代碼。 可以看到使用中的fetch的 promise 結構。 文本被解析,在下一行中,編碼的 SVG 被解碼。 再多一行,Vue 更新頁面。 按鈕欄中的按鈕數量將等於 JSON 數組的長度。

 fetch(url).then((response) => { // this is now browser native response.text().then((text) => { var newData = JSON.parse(text); // DATA UPDATE! This is it. newData = newData.map(obj => { obj.icon = decodeURIComponent(obj.icon); return(obj) }); // the page update right away with new data. iconApp.iconList = newData; }); });

現在,只有兩個片段。 Vue 應用程序。 讀者會注意到@click指令已包含在按鈕中。 數據元素iconEntry.name被傳遞給引號內的方法。

該方法在 Vue 應用程序中定義:

 <div class="entryart"> <button v-for="iconEntry in iconList" @click="goGetPanel(iconEntry.name)" > <div v-html="iconEntry.icon"> </div> </button> </div> </div>

這是方法定義的片段。 methods對像被添加到 app 參數對像中的data對象之後:

 , methods: { goGetPanel: (pname) => { // `this` inside methods points to the Vue instance alert('Hello ' + pname + '!') } }

讀者應該找到goGetPanel定義,並指出了@click處理程序的使用。 在我們的最終應用程序中, alert調用可以替換為從服務器獲取面板的函數。

物聯網面板的組件庫

我們可以決定我們從服務器獲取的面板可以是 HMTL 或只是 SVG 繪圖,但如果有多種面板,我們希望創建面板的工作可以通過組件庫來簡化從中選擇。 我們可以想像,可以改進 SVG 編輯器以允許將庫組件作為編輯的一部分拖放到圖片上。 然後,如果 SVG 編輯器可以輸出帶有組件標籤的圖片版本,那麼使用 Vue 將允許創建圖片,同時確保 JavaScript 自動化和動畫整齊地編織在一起。 對於我們的討論,一些手工編輯可以幫助我們到達那裡。

如果我們想用 Vue 組件創建面板,那麼我們最好弄清楚如何製作這些組件,然後將它們組合成有用的東西。 我們將不得不切換到使用 Vue 提供的命令行工具並組織我們的工作流程。

成分

Vue文檔指出,組件定義的組件data部分(子對象)需要是返回數據的函數。 原因是 Vue 需要在實例之間保持數據分離。 因此,在從 Vue 應用程序初始化到組件定義的過程中,還有另一個小的代碼更改。

在這第一段代碼中,正在初始化一個 Vue 應用程序:

 var iconApp = new Vue({ el: '#iconApp', data: { // this is the data field that can be easily updated }, methods : { ... } });

在這個新的代碼片段中,正在定義和註冊一個組件。 首先,請注意,不是創建一個new Vue實例,而是註冊了一個名為iconic的組件。 然後, data字段返回 Vue 應用程序創建的任何iconic實例的自定義數據。 最後, template字段出現在組件註冊的末尾。 任何可能已經寫在網頁上以顯示組件的 HTML 都可以成為template的一部分。

 Vue.component('iconic', data: () => { var instanceData = { // data fields named for the // variables appearing in the template onevar : "test" } return(instanceData); }, methods : { ... }, template: '<div>This appears in every instance {{onevar}}</div>' });

所以,我們可以想像一個帶有溫度計的面板。 因此,如果有人提供了thermometer組件,我們會期望在代碼中的某處有一個組件定義。 因此:

 Vue.component('thermometer', data: () => { var instanceData = { // data fields named for the // variables appearing in the template temperature : 0 } return(instanceData); }, methods : { ... }, template: '<div>Some SVG will go here</div>' });

我們正在嘗試創建如下所示的內容:

在探索組件之前,Vue 中的動畫溫度計應用程序。
在探索組件之前,Vue 中的動畫溫度計應用程序。 (大預覽)

溫度計組件與您將在 Vue 教程中遇到的第一個組件非常相似。 但是,弄清楚如何更新它有點棘手。 有一種更好的方法可以使用屬性來定義組件的反應性。 而且,在以下內容中:

 Vue.component('thermometer', { props: ['temperature'], computed : { y: function() { var t = this.temperature/100; var h = 54.724472; var y_bar = 41.176476 // starts near the top // pretend the scale is 1 to 100, so that the temperature is a precentage return((1 - t)*h + y_bar) }, height : function() { var t = this.temperature/100; var h = 54.724472; // as high as the whole range var y_bar = 41.176476 // pretend the scale is 1 to 100, so that the temperature is a precentage return(t*h) } }, template: '#thermometer-template' })

因此,而不是將溫度表示為數據元素。 它表示為props下的屬性。 然後,有一個新部分, computed ,它提供作為屬性函數的變量。 我們看到this.temperature用於yheight 。 這些計算變量在 SVG 中用作矩形的屬性。

在 SVG 中, y自上而下增長。 因此,當我們希望溫度計底部的矩形變小時,紅色框的y必須更低,並且高度必須減小,以使 ( y + height ) 保持在溫度計零處。

注意組件定義中的template字段。 它實際上是一個文檔元素 ID。 被引用的元素是具有特殊類型的腳本部分: type="text/x-template" 。 腳本元素是溫度計的 SVG 所在的位置。 而且,SVG 使用了 Vue 變量和控制項,因此可以定義反應性。

這是一些 SVG:

 <script type="text/x-template"> <svg xmlns:svg="https://www.w3.org/2000/svg" xmlns="https://www.w3.org/2000/svg" width="20" height="70" version="1.1" > <g transform="translate(0,-180)"> <g transform="matrix(2.0111869,0,0,1.0489665,-215.11053,144.5592)"> <rect stroke-linecap="null" stroke-linejoin="null" width="2.9665921" height="54.724472" x="111.90748" y="41.176476" /> <rect stroke-linecap="null" stroke-linejoin="null" width="2.9665921" x="111.90748" :height="height" :y="y" /> <g transform="matrix(0.76503813,0,0,1,26.586929,0)"> <line y2="57.306953" y1="57.306953" x2="113.15423" x1="107.22105" stroke-linejoin="null" stroke-linecap="null" /> <line y2="74.408356" y1="74.408356" x2="113.15423" x1="107.22105" stroke-linejoin="null" stroke-linecap="null"

讀者可以在頂部找到id="thermometer-template" ,再往下看rect元素,可以找到計算變量。

在這裡,變量用途被分離出來。 v-bind的 Vue 簡寫語法正在使用中,使用:height="height"y相同:

 x="111.90748" :height="height" :y="y"

當 SVG 元素的父元素設置作為溫度計屬性temperature輸入的變量時,Vue 會重新計算heighty 。 結果,紅框的位置和高度發生了變化。

列出使用溫度計的 Vue 應用程序會有所幫助。

 <body> <!-- The Vue app starts here. This is the HTML part of the Vue object --> <div> <!-- Recognize the name from the Vue doc --> <div> <h2 itemprop="name">Set Temperature</h2> <p itemprop="description">These are groups satistfying this query: {{queryToken}}.</p> <!-- {{tweetOwner}} is in the data model. --> <button @click="updateTemp(50,50)">mid</button> <button @click="updateTemp(20,80)">low</button> <button @click="updateTemp(80,20)">high</button> </div> <thermometer :temperature="temp1" ></thermometer> <thermometer :temperature="temp2" ></thermometer> </div> <script> var thermoApp = new Vue({ el: '#thermoApp', data: { temp1 : 30, temp2 : 60, queryToken : "HEAT" }, methods : { updateTemp: function (tval1,tval2) { this.temp1 = tval1; this.temp2 = tval2; } } }); </script> </body>

這就是全部。 有三個按鈕調用thermoApp Vue 應用程序的updateTemp方法。 數據部分有兩個溫度變量。 並且,每個thermometer都會在值發生變化時更新其溫度。

下面調用的兩個溫度計的代碼可以在分配給 Vue 應用程序的 HTML 中找到。

 <thermometer :temperature="temp1" ></thermometer> <thermometer :temperature="temp2" ></thermometer>

請注意,應用程序將function形式主義用於方法定義。 以這種方式定義updateTemp updateTemp: function (tval1,tval2)允許訪問實例變量this

此外,以這種方式定義updateTemp updateTemp: (tval1,tval2) =>this分配給不響應和更新視圖的內部數據結構。

組裝面板

每個物聯網面板都可以是一個組件。 Vue 提供了一種使用子組件定義組件的方法。 或者,有一種插槽機制可用於生成可以環繞任何 HTML 內容的組件。

在接下來的幾段中,讓我們看看如何用子組件製作一個面板。 從我們的示例中可以快速得出兩種形式。 在一種情況下,溫度計可以是用 JavaScript 調用的子組件。 在另一種情況下,組件是獨立定義的,但在 HTML 中被提及。

在這兩種情況下,模板都可以使用相同的 HTML。 這是我們的面板作為模板:

 <script type="text/x-template"> <div> <thermometer :temperature="temp1" ></thermometer> <thermometer :temperature="temp2" ></thermometer> </div> </script>

應用程序的第一個細節之間的唯一區別是div元素圍繞著兩個溫度計。 如果模板缺少頂級 DOM 元素,Vue 將拋出錯誤。 div通過了 Vue 的要求,裡面可以包含多個元素。

現在,我們可以並排看到兩個溫度計。 將溫度從頂部傳遞到最終溫度計具有級聯下降的值。 在頂層,當應用程序 DOM 中包含一行時,面板會加入應用程序。

 <themo-panel :temp1="temp1" :temp2="temp2" ></themo-panel>

面板的模板雖然簡單,但似乎表明面板可以很容易地根據組件進行設計。 就好像只有物聯網組件的語言是可能的。

現在,面板的模板定義很簡單。 這是獨立定義的子組件:

 Vue.component('thermo-panel', { props: ['temp1','temp2'], template: '#thermo-panel-template' });

這大約是使面板正常工作所需的量。 確實,此版本依賴於一長串屬性來定義在消息進入頁面時要更新的值。 但是,這是一個好的開始。 更新頂層的data對象可以為溫度計設置動畫。 然而,隨著面板變得複雜,可能需要另一種方法來顯示變化。

已經提到了指定子組件的其他方法,對於面板,我們應該看看它。 這裡是:

 Vue.component('thermo-panel', { props: ['temp1','temp2'], template: '#thermo-panel-template', components: { // a sub component for the labels 'thermometer': { props: { temperature: Number, }, template: '#thermometer-template', computed : { y: function() { var t = this.temperature/100; var h = 54.724472; var y_bar = 41.176476 // starts near the top // pretend the scale is 1 to 100, so that the temperature is a precentage return((1 - t)*h + y_bar) }, height : function() { var t = this.temperature/100; var h = 54.724472; // as high as the whole range var y_bar = 41.176476 // pretend the scale is 1 to 100, so that the temperature is a precentage return(t*h) } } } } });

當然還有更多代碼,但那是因為thermometer組件的 JavaScript 包含在thermo-panel的組件列表中。 這兩種方法做同樣的工作,但它們提供了不同的方式來打包組件定義。

目前,我的偏好是第一種方式。 如果只需要更改模板和屬性,修改面板並動態檢索它們應該會容易得多。 為此,獨立定義的組件形成組件庫。 但是,雖然這看起來更好,但在下面使用第二種看起來更冗長的方式會變得更方便。

鑑於我們可以以明確定義的方式從組件​​中製作響應式面板,我將在我的文章的下一部分中解釋我們如何將它們作為可以進行簡單查詢的數據庫進行管理。