Componenti della pagina Web SVG per IoT e Maker (Parte 2)
Pubblicato: 2022-03-10Quindi, abbiamo già modi per caricare dinamicamente un menu di icone SVG fatte per reagire caricando i pannelli se lo desideriamo, ma le icone non erano componenti reali. Siamo stati in grado di utilizzare un semplice trucco per inserire l'SVG per ogni icona e passarlo nell'applicazione Vue. È stato abbastanza semplice generare un elenco di icone e ogni icona ha reagito in modo simile tranne che per piccole differenze di dati. La differenza di dati ha permesso di associare il nome di un pannello a ciascuna icona in modo tale che il gestore del clic del pulsante dell'icona potesse trasmetterlo.
Quando un pannello viene caricato sotto forma di componente Vue, è necessario caricare tutto ciò che riguarda il pannello e i suoi componenti, modelli, JavaScript e altro. Quindi, il compito di gestire solo il caricamento del pannello è più grande di quello che abbiamo incontrato finora in questa discussione.
Diamo un'occhiata al modo in cui Vue fornisce un hook per il caricamento asincrono. Il seguente frammento è tratto dalla guida 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) })
La guida ci dice che la funzione setTimeout è un esempio di come utilizzare la sincronicità con i componenti Vue. Si noti che dove prima c'era un oggetto come secondo parametro di Vue.component
, ora c'è una funzione, che viene definita funzione di fabbrica. All'interno del callback di resolve
c'è una definizione del componente, che sarebbe stato il secondo parametro di Vue.component
prima.
Quindi, ho dovuto fissare questo esempio un po' prima che avesse senso per me. Ecco un altro esempio, che mi si addice di più:
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 } );
Sembra la cosa giusta da fare per creare una funzione più generale per aggirare questo modulo.
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 } ); }
Quindi, in generale, per caricare un componente, avremmo solo bisogno di una riga come la seguente:
componentLoader('ThermoPanel','./JSON/thermo-panel.json');
Quindi ora, qual è il JSON che viene caricato? Può includere tutto ciò che riguarda il componente. In questo caso, come componente del pannello, può includere termometri, interruttori della macchina, cursori, calibri e altro. Anche se sembrava più carino mantenere le parti dei componenti sulla pagina web, in realtà potrebbe funzionare meglio usare il campo del sottocomponente che è nell'esempio più lungo per "pannello termico" che abbiamo realizzato prima e anche per altri pannelli costruiti in modo simile. Il JSON conterrà una struttura di pannelli completa.
Tuttavia, se il lettore noterà l'inclusione della chiamata di funzione a transformJSONToJSObject
, capirà che JSON potrebbe essere codificato in qualche modo per rendere più semplice il trasporto e per rendere più facile per un server gestire la definizione. Dopotutto, la definizione includerà modelli SVG completi, definizioni di funzioni e altre espressioni JavaScript. Inoltre, l'oggetto JSON può contenere più della semplice definizione del pannello perché alcune informazioni possono semplicemente aiutare nella contabilità o nella convalida. Quindi, ci si può aspettare che ci sarà un trattamento dell'oggetto al momento della ricezione.
Per quanto riguarda la codifica, i dati in arrivo dal server possono essere codificati in diversi modi. Forse sarà semplicemente codificato in URL. O più sicuro, potrebbe essere cifrato. Per questa discussione, possiamo semplicemente usare la codifica URL.
Alcuni degli strumenti disponibili per la creazione di applicazioni Vue si occupano senza dubbio della trasformazione JSON. Ma questa discussione ha finora evitato l'uso di strumenti da riga di comando. Questa omissione non è poi così male in quanto abbiamo anche utilizzato Vue con il minimo di risorse, utilizzando un solo tag di script per fare riferimento alla CDN. Tuttavia, consiglio vivamente di esaminare gli strumenti della riga di comando soprattutto per l'organizzazione dei progetti.
Quando il JSON arriva alla pagina, dato che il componente è completamente assemblato con i sottocomponenti, non è più necessario eseguire il lavoro per recuperare le parti. Possiamo supporre che tutti i componenti verranno completamente definiti per il resto di questa discussione. Tuttavia, l'assemblaggio di gerarchie di componenti complete richiederà prima o poi strumenti da riga di comando.
Anche il processo di modifica SVG richiederà un po' di lavoro. I processi di modifica SVG consentono a un designer di disegnare un pannello e tutti i componenti su di esso. Ma ogni sottocomponente deve essere identificato, chiamato in un gruppo o assegnato un segnaposto. Qualsiasi approccio all'utilizzo del disegno richiede un trattamento dell'SVG in modo che i tag dei componenti Vue possano sostituire i gruppi o gli elementi grafici. In questo modo, qualsiasi rendering dell'artista può diventare un modello. Inoltre, i sottocomponenti disegnati dovranno essere disassemblati in modelli per i sottocomponenti Vue.
Questo tipo di parsimonia è contraria al flusso di lavoro della maggior parte dei framework JavaScript. I framework riguardano l'assemblaggio di pagine. Ma, la modifica o il disegno, si traduce in qualcosa già assemblato da un artista. In pratica, il risultato della modifica non fornisce un file di testo che corrisponda direttamente alla definizione di un componente del framework.
Maggiori informazioni sul processo di modifica possono essere considerate in qualche altra discussione. C'è molto da fare. Ma, per ora, abbiamo gli strumenti di cui abbiamo bisogno per caricare i componenti gerarchici e renderli vivi.
L'applicazione pigra
Per la nostra costruzione di pannelli IoT, abbiamo già una barra di selezione che risponde alle ricerche. Inoltre, abbiamo un modo per caricare i componenti quando ne abbiamo bisogno. Dobbiamo solo collegare queste parti. E, infine, dobbiamo assicurarci che i pannelli appaiano e che inizino a funzionare quando lo fanno.
Il caricamento lento dei pannelli eseguito dal codice asincrono sopra fornisce uno schizzo di un'idea. Ma, per fortuna, alcune persone hanno sperimentato per trovare il modo di assicurarsi che tutti i tipi di componenti possano essere caricati. C'è una voce codepen che mostra come aggiornare le app Vue con nuovi componenti di vario tipo. Questo è il meccanismo necessario per aggiornare una parte designata della pagina con diversi tipi di pannello.
Con la possibilità di aggiungere diversi tipi di pannelli e con un semplice meccanismo per caricare le loro definizioni, possiamo finalmente avere la nostra pagina di ricerca dei pannelli.
Ecco l'HTML di cui abbiamo bisogno nella nostra pagina in modo che l'app Vue possa inserire i componenti in modo dinamico:
<template v-for="(panel, index) in panelList"> <component :is="panel" :key="panel.name"></component> </template>
Il tag del component
è un meta tag Vue. Vedere il riferimento per i componenti dinamici. Le proprietà, attributi speciali, utilizzati per il tag component
in questo caso sono is e chiave. L'attributo is
esiste per i componenti dinamici. Inoltre, la key
garantisce che i nuovi bambini abbiano identità diverse l'uno dall'altro e aiuta Vue a decidere cosa disegnare.
“I figli dello stesso genitore comune devono avere chiavi univoche. Le chiavi duplicate causeranno errori di rendering".
Il tag template
scorrerà attraverso i componenti forniti nel campo dati panelList
dell'applicazione.
Quindi, a partire dalla definizione Vue a livello di applicazione per l'app icona, possiamo apportare modifiche per includere il panelList negli elementi di dati. (Chiamiamola ora 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 */ }); } } });
Oltre all'aggiunta nel pannello, goGetPanel
è ora in una forma richiesta per ottenere la definizione di un componente da un database o da un altro negozio. Il lato server deve prestare attenzione a fornire il codice JavaScript nel formato corretto. Per quanto riguarda l'aspetto dell'oggetto proveniente dal server, l'abbiamo già visto. È il tipo di oggetto utilizzato come parametro per Vue.component
.
Ecco il corpo completo dell'app Vue che fornisce un menu come risultato di ricerca e un posto dove inserire i pannelli prelevati dal server quando l'utente fa clic su un'icona.
<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>
Nell'ultimo div
, il tag del component
ha ora un parametro ref
associato al nome del pannello. Il parametro ref consente all'app Vue di identificare quale componente aggiornare con i dati e mantiene i componenti separati. I parametri ref
consentono inoltre alla nostra applicazione di accedere ai nuovi componenti caricati dinamicamente.
In una versione di prova dell'app del pannello, ho il seguente gestore di intervalli:
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)
Il codice fornisce una piccola animazione, cambiando i termometri in modo casuale. Ogni pannello ha due termometri e l'app consente all'utente di continuare ad aggiungere pannelli. (Nella versione finale, alcuni pannelli devono essere gettati via.) Si accede ai refs utilizzando panelApp.$refs
, un campo che Vue crea date le informazioni sui refs
nel tag del component
.
Quindi, ecco come appaiono i termometri che saltano casualmente in un'istantanea:
Collegamento del pannello al dispositivo IoT
Quindi, l'ultimo pezzo di codice è un test setInterval
che aggiorna i termometri con valori casuali ogni due secondi. Ma quello che vogliamo fare è leggere dati reali da macchine reali. Per fare ciò, avremo bisogno di una qualche forma di comunicazione.
Ci sono una varietà di modi. Ma usiamo MQTT che è un sistema di messaggi pub/sub. Il nostro SPWA può iscriversi ai messaggi dai dispositivi in qualsiasi momento. Quando riceve quei messaggi, l'SPWA può indirizzare ogni messaggio al gestore dati appropriato per la centrale mappata sul dispositivo identificato nel messaggio.
Quindi, fondamentalmente quello che dobbiamo fare è sostituire setInterval
con un gestore di risposta. E questo sarà per un pannello. Probabilmente vogliamo mappare i pannelli ai gestori mentre vengono caricati. E spetta al server web verificare che venga fornita la mappatura corretta.
Una volta che il server Web e l'SPWA hanno la pagina pronta per il funzionamento, il server Web non deve più occuparsi della messaggistica tra la pagina e il dispositivo. il protocollo MQTT specifica un server di routing per gestire pub/sub. Sono stati realizzati numerosi server MQTT. Alcuni di loro sono open source. Uno molto popolare è Mosquito e ce ne sono alcuni sviluppati su Node.js.
Il processo per la pagina è semplice. L'SPWA si iscrive a un argomento. Una buona versione di un argomento è un identificatore per un MCU come un indirizzo MAC o un numero di serie. Oppure, l'SPWA potrebbe iscriversi a tutte le letture della temperatura. Ma poi la pagina dovrebbe fare il lavoro di filtrare i messaggi da tutti i dispositivi. La pubblicazione in MQTT è essenzialmente una trasmissione o multicast.
Diamo un'occhiata a come l'SPWA si interfaccerà con MQTT.
Inizializzazione di MQTT su SPWA
Ci sono diverse librerie client tra cui scegliere. Uno, ad esempio, è un MQTT.js. Un altro è Eclipse Paho. Ce ne sono di più ovviamente. Usiamo Eclipse Paho poiché ha una versione memorizzata CDN. Dobbiamo solo aggiungere la seguente riga alla nostra pagina:
<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.min.js" type="text/javascript"></script>
Il client MQTT deve connettersi a un server prima di poter inviare e ricevere messaggi. Quindi, anche le righe che impostano la connessione devono essere incluse in JavaScript. Possiamo aggiungere una funzione MQTTinitialize
che imposta il client e le risposte per la gestione della connessione e la ricezione dei messaggi.
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 }; }
Configurazione dell'abbonamento
Con la connessione pronta, il client può iscriversi ai canali di messaggi, inviare messaggi su di essi, ecc. Solo poche routine possono svolgere la maggior parte del lavoro necessario per collegare i pannelli con i percorsi MQTT.
Per il pannello SPWA, il momento della sottoscrizione può essere utilizzato per stabilire l'associazione tra il pannello e l'argomento, l'identificatore MCU.
function panelSubcription(topic,panel) { gTopicToPanel[topic] = panel; gPanelToTopic[panel] = topic; mqttClient.subscribe(topic); }
Dato che un MCU sta pubblicando sul suo argomento, l'SPWA riceverà un messaggio. Qui, il messaggio Paho è spacchettato. E, quindi, il messaggio viene trasmesso ai meccanismi dell'applicazione.
function onMessageArrived(pmessage) { // var topic = pmessage.destinationName; var message = pmessage.payloadString; // var panel = gTopicToPanel[topic]; deliverToPanel(panel,message); }
Quindi, ora tutto ciò che dobbiamo fare è creare deliverToPanel
che dovrebbe essere un po' come il gestore di intervalli che avevamo prima. Tuttavia, il pannello è chiaramente identificato e solo i dati codificati inviati nel messaggio specifico possono essere aggiornati.
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] } }
Questa funzione deliverToPanel
è sufficientemente astratta da consentire qualsiasi definizione di pannello con un numero qualsiasi di punti dati per l'animazione.
Invio di messaggi
Per completare il ciclo dell'applicazione tra MCU e SPWA, definiamo una funzione per inviare un messaggio.
function sendPanelMessage(panel,message) { var topic = gPanelToTopic[panel]; var pmessage = new Paho.MQTT.Message(message); pmessage.destinationName = topic; mqttClient.send(pmessage); }
La funzione sendPanelMessage
non fa altro che inviare il messaggio sullo stesso percorso dell'argomento a cui è iscritto l'SPWA.
Poiché prevediamo di rendere i pulsanti delle icone responsabili dell'inserimento di un certo numero di pannelli per un singolo cluster di MCU, ci sarà più di un pannello di cui occuparci. Tuttavia, teniamo presente che ogni pannello corrisponde a un singolo MCU, quindi abbiamo una mappatura one-one, per la quale possiamo utilizzare due mappe JavaScript per la mappa e l'inverso.
Allora, quando inviamo i messaggi? Di solito, l'applicazione del pannello invierà un messaggio quando desidera modificare lo stato dell'MCU.
Mantenere lo stato di visualizzazione (Vue) sincronizzato con i dispositivi
Una delle grandi cose di Vue è che è molto facile mantenere il modello di dati sincronizzato con l'attività dell'utente, che può modificare i campi, fare clic sui pulsanti, utilizzare gli slider, ecc. Si può essere certi che le modifiche ai pulsanti e ai campi verranno riflettersi immediatamente nei campi dati dei componenti.
Tuttavia, vogliamo che le modifiche inviino messaggi all'MCU non appena si verificano le modifiche. Quindi, cerchiamo di utilizzare gli eventi dell'interfaccia che Vue potrebbe governare. Cerchiamo di rispondere a un tale evento, ma solo dopo che il modello di dati Vue è pronto con il valore corrente.
Ho creato un altro tipo di pannello, questo con un pulsante dall'aspetto abbastanza artistico (forse ispirato a Jackson Pollock). E sono andato a trasformarlo in qualcosa il cui clic riporta lo stato al pannello che lo contiene. Non è stato un processo così semplice.
Una cosa che mi ha sconvolto è che avevo dimenticato alcune delle stranezze nella gestione di SVG. Per prima cosa ho provato a cambiare la stringa di stile in modo che il campo di display
dello stile CSS fosse "Nessuno" o "qualcosa". Ma il browser non ha mai riscritto la stringa degli stili. Ma, poiché era ingombrante, ho provato a cambiare la classe CSS. Anche questo non ha avuto effetto. Ma c'è l'attributo di visibility
, che la maggior parte di noi ricorda dal vecchio HTML (forse versione 1.0), ma che è molto aggiornato in SVG. E funziona bene. Tutto quello che dovevo fare era far propagare l'evento del clic del pulsante.
Vue ha progettato proprietà per propagarsi in una direzione, da genitore a figlio. Quindi, per modificare i dati nell'applicazione, o nel pannello, devi inviare un evento di modifica al genitore. Quindi, puoi modificare i dati. La modifica dell'elemento dati che controlla il pulsante fa sì che Vue aggiorni la proprietà che influisce sulla visibilità dell'elemento SVG che abbiamo scelto di indicare lo stato. Ecco un esempio:
Ogni istanza del pannello dei pulsanti ondulati è indipendente. Quindi, alcuni sono attivi e altri sono spenti.
Questo frammento di SVG contiene l'indicatore giallo dall'aspetto strano:
<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)" />
La visibilità è popolata da stateView
, una variabile calcolata che associa lo stato booleano a una stringa per SVG.
Ecco il modello di definizione dei componenti del pannello:
<script type="text/x-template"> <div> <control-switch :state="bstate" v-on:changed="saveChanges" ></control-switch> <gauge :level="fluidLevel" ></gauge> </div> </script>
E questa è la definizione JavaScript del pannello Vue con i suoi figli come sottocomponenti:
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' } } }
Quindi, ora è stato predisposto il meccanismo per un singolo pulsante incorporato in un pannello. E ci deve essere un gancio per dire all'MCU che è successo qualcosa. Deve essere richiamato subito dopo l'aggiornamento dello stato dei dati del componente del pannello. Definiamolo qui:
function relayToMCU(panel,switchName,bstate) { var message = switchName + ':' + bstate // a on element parameter string. sendPanelMessage(panel,message) }
C'è il cambio di stato in arrivo all'hardware in sole due righe di codice.
Ma questo è un caso abbastanza semplice. Qualsiasi interruttore può essere visto come una chiamata di funzione a un pezzo di hardware nel mondo. Pertanto, la stringa potrebbe contenere il nome dello switch e molti altri elementi di dati. Quindi, il metodo del componente che registra le modifiche dovrà avere una gestione personalizzata in modo che possa raccogliere tutti i pezzi di dati impostati sul pannello e inviarli insieme in una stringa di comando. Anche la stringa di comando è un po' semplice. Se l'MCU è piuttosto piccolo, potrebbe essere necessario tradurre la stringa di comando in un codice. Se l'MCU ha una grande capacità, la stringa di comando potrebbe essere effettivamente una struttura JSON o forse tutti i dati ospitati dal pannello.
In questa discussione, i pulsanti sul pannello delle icone contengono il nome del pannello da recuperare. Anche questo potrebbe essere abbastanza semplificato. Sembra logico che quel parametro possa rappresentare qualsiasi pannello che potrebbe essere archiviato nei database di un'azienda. Ma forse è una formula. Forse, le informazioni sul pannello dovrebbero essere racchiuse attorno alla definizione del pannello che riceviamo dal server. In ogni caso, le basi possono essere facilmente ampliate una volta che alcuni mal di testa sono stati eliminati, come fare in modo che l'SVG risponda correttamente ai clic.
Conclusione
Questa discussione ha delineato alcuni passaggi e decisioni di base che portano alla realizzazione di un'app Web a pagina singola (SPWA) in grado di interfacciarsi con i dispositivi IoT. Ora sappiamo come ottenere i pannelli da un server web e trasformarli in un'interfaccia MCU.
C'è molto di più in questa discussione con alcune altre discussioni che potrebbero seguire. Iniziare con Vue è una cosa a cui pensare. Ma poi c'è l'intera storia del MCU, di cui abbiamo solo brevemente accennato.
In particolare, selezionando MQTT come substrato di comunicazione, assumiamo che i dispositivi IoT all'altro capo possano essere in qualche modo governati da MQTT. Ma potrebbe non essere sempre così. A volte sono necessari gateway se MQTT vuole accedere a un dispositivo con collegamenti seriali o Bluetooth. O forse tutto ciò di cui si ha bisogno dalla pagina web è WebSockets. Tuttavia, abbiamo utilizzato MQTT come esempio per mostrare come Vue possa ricevere e inviare dati mantenendo il suo stato dei dati sincronizzato con i dispositivi.
Ancora una volta abbiamo solo una parte della storia. Questa volta è per la sincronizzazione perché la pagina dovrebbe essere in grado di gestire gli avvisi e disturbare l'utente se sta accadendo qualcosa di critico. A volte i messaggi possono andare persi. Quindi, dobbiamo avere un meccanismo per i ringraziamenti.
Infine, è mia opinione che Vue renda piuttosto elegante l'aggiornamento dei dati al momento della ricezione. Ma inviare i cambiamenti di stato non è così semplice. Non sembra rendere il lavoro molto più semplice di quanto si possa fare con JavaScript vanilla. Ma c'è un modo e ha senso.
Forse è possibile creare una libreria pulita per creare un insieme universale di componenti per tutti i pannelli. Sono stati brevemente menzionati gli elementi per creare tali librerie e averle archiviate in un database. Potrebbe essere necessario sviluppare strumenti che vadano oltre la semplice creazione di immagini SVG. In ogni caso, ci sono probabilmente molte cose che possono essere fatte per i passaggi successivi.