Компоненты веб-страницы 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 с минимумом ресурсов, используя только один тег script для ссылки на CDN. Тем не менее, я, безусловно, рекомендую изучить инструменты командной строки, особенно для организации проектов.
Когда JSON поступает на страницу, при условии, что компонент полностью собран с подкомпонентами, для извлечения частей больше не нужно выполнять никаких действий. Мы можем сделать предположение, что все компоненты будут полностью определены для остальной части этого обсуждения. Но для сборки полных иерархий компонентов в какой-то момент потребуются инструменты командной строки.
Процесс редактирования SVG также потребует некоторой работы. Процессы редактирования SVG позволяют дизайнеру рисовать панель и все компоненты на ней. Но каждый подкомпонент должен быть идентифицирован, выделен в группу или ему присвоено место. Любой подход к использованию рисунка требует некоторой обработки SVG, чтобы теги компонента Vue могли заменить группы или графические элементы. Таким образом, любой художественный рендеринг может стать шаблоном. А нарисованные подкомпоненты придется разбирать на шаблоны для подкомпонентов Vue.
Такая экономия противоречит рабочему процессу большинства фреймворков JavaScript. Фреймворки предназначены для сборки страниц. Но, редактируя или рисуя, получается что-то уже собранное художником. На практике результат редактирования не предоставляет текстовый файл, который непосредственно соответствует определению компонента фреймворка.
Подробнее о процессе редактирования можно узнать в другом обсуждении. Это очень важно. Но на данный момент у нас есть инструменты, необходимые для загрузки иерархических компонентов и их оживления.
Ленивое приложение
Для нашей конструкции панели IoT у нас уже есть панель выбора, которая реагирует на поиск. И у нас есть способ загрузки компонентов, когда они нам нужны. Нам просто нужно соединить эти части. И, наконец, мы должны убедиться, что панели появляются и начинают работать тогда, когда появляются.
Ленивая загрузка панелей, выполняемая асинхронным кодом выше, дает набросок идеи. Но, к счастью, некоторые люди экспериментировали, чтобы найти способы убедиться, что все виды компонентов могут быть загружены. Существует одна запись 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, который представляет собой систему сообщений pub/sub. Наш SPWA может подписаться на сообщения с устройств в любое время. Получив эти сообщения, SPWA может направить каждое сообщение соответствующему обработчику данных для панели, привязанной к устройству, указанному в сообщении.
Итак, в основном, что нам нужно сделать, это заменить setInterval
обработчиком ответа. И это будет за одну панель. Вероятно, мы хотим сопоставить панели с обработчиками по мере их загрузки. И именно веб-сервер должен следить за тем, чтобы было доставлено правильное сопоставление.
Как только веб-сервер и SPWA подготовят страницу к работе, веб-серверу больше не нужно заботиться об обмене сообщениями между страницей и устройством. протокол MQTT указывает сервер маршрутизации для обработки pub/sub. Создан ряд серверов MQTT. Некоторые из них имеют открытый исходный код. Одним из очень популярных является Mosquito , и есть несколько, разработанных на основе Node.js.
Процесс для страницы прост. SPWA подписывается на тему. Одной из хороших версий темы является идентификатор MCU, такой как MAC-адрес или серийный номер. Или SPWA может подписаться на все показания температуры. Но тогда странице придется выполнять работу по фильтрации сообщений со всех устройств. Публикация в MQTT по сути является широковещательной или мультикастовой.
Давайте посмотрим, как SPWA будет взаимодействовать с MQTT.
Инициализация MQTT на SPWA
Есть несколько клиентских библиотек на выбор. Например, это MQTT.js. Другое затмение пахо. Конечно, есть еще. Давайте воспользуемся 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.
Для панели SPWA момент подписки может быть использован для установления связи между панелью и темой, идентификатором MCU.
function panelSubcription(topic,panel) { gTopicToPanel[topic] = panel; gPanelToTopic[panel] = topic; mqttClient.subscribe(topic); }
Учитывая, что MCU публикует свою тему, SPWA получит сообщение. Здесь сообщение Пахо распаковывается. И затем сообщение передается в механику приложения.
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, поэтому у нас есть сопоставление «один к одному», для которого мы можем использовать две карты JavaScript для карты и наоборот.
Итак, когда мы будем отправлять сообщения? Обычно приложение панели отправляет сообщение, когда хочет изменить состояние MCU.
Синхронизация состояния представления (Vue) с устройствами
Одна из замечательных особенностей Vue заключается в том, что очень легко синхронизировать модель данных с действиями пользователя, который может редактировать поля, нажимать кнопки, использовать ползунки и т. д. Можно быть уверенным, что изменения кнопок и полей будут немедленно отражаться в полях данных компонентов.
Но мы хотим, чтобы изменения отправляли сообщения MCU, как только происходят изменения. Итак, мы стремимся использовать события интерфейса, которыми может управлять Vue. Мы стремимся отреагировать на такое событие, но только после того, как модель данных Vue будет готова с текущим значением.
Я создал панель другого типа, на этот раз с довольно художественно выглядящей кнопкой (возможно, вдохновленной Джексоном Поллоком). И я решил превратить его во что-то, чей щелчок сообщает о состоянии панели, которая его содержит. Это был не такой простой процесс.
Одна вещь, которая меня сбила с толку, это то, что я забыл о некоторых странностях в управлении SVG. Сначала я попытался изменить строку стиля, чтобы в поле display
стиля CSS было либо «Нет», либо «что-то». Но браузер никогда не переписывал строку стилей. Но так как это было громоздко, я попытался изменить класс CSS. Это тоже не повлияло. Но есть атрибут visibility
, который большинство из нас помнит из старого HTML (возможно, версии 1.0), но который очень актуален в 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>
И это определение JavaScript панели Vue с ее дочерними элементами в качестве подкомпонентов:
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 правильно реагировать на клики.
Заключение
В этом обсуждении изложены некоторые основные шаги и решения, ведущие к реализации одностраничного веб-приложения (SPWA), которое может взаимодействовать с устройствами IoT. Теперь мы знаем, как получить панели с веб-сервера и превратить их в интерфейс MCU.
Эта дискуссия включает в себя гораздо больше, и за ней может последовать немало других дискуссий. Стоит подумать о том, чтобы начать с Vue. Но есть и вся история MCU, которую мы лишь кратко затронули.
В частности, выбирая MQTT в качестве коммуникационного субстрата, мы предполагаем, что устройства IoT на другом конце каким-то образом могут управляться MQTT. Но так может быть не всегда. Иногда шлюзы необходимы, если MQTT должен получить доступ к устройству с последовательными каналами или Bluetooth. Или, возможно, все, что нужно на веб-странице, — это WebSockets. Тем не менее, мы использовали MQTT в качестве примера, чтобы показать, как Vue может как получать, так и отправлять данные, сохраняя при этом свое состояние данных в синхронизации с устройствами.
Еще раз у нас есть только часть истории. На этот раз это для синхронизации, потому что страница должна иметь возможность обрабатывать предупреждения и беспокоить пользователя, если происходит что-то критическое. Иногда сообщения могут теряться. Итак, у нас должен быть механизм для подтверждений.
Наконец, я считаю, что Vue делает обновление данных при получении довольно элегантным. Но отправка изменений состояния не так проста. Кажется, это не делает работу намного проще, чем это можно сделать с помощью ванильного JavaScript. Но есть способ, и он имеет смысл.
Возможно, можно собрать чистую библиотеку, чтобы сделать универсальный набор компонентов для всех панелей. Были кратко упомянуты элементы для создания таких библиотек и их хранения в базе данных. Возможно, потребуется разработать инструменты, выходящие за рамки простого создания изображений SVG. В любом случае, вероятно, есть много вещей, которые можно сделать для следующих шагов.