back to top

Creare un generatore di QR code con Vue.js

Nelle precedenti lezioni abbiamo appreso le basi di Vue.js e abbiamo iniziato a capire, al meno superficialmente, come funziona il framework. A questo punto abbiamo trattato abbastanza argomenti per realizzare un esempio più articolato anche se non del tutto completo ed ottimizzato. In questa lezione vedremo quindi come creare un’applicazione rudimentale per generare dei codici QR.

La versione finale dell’applicazione è disponibile su Bitbucket.

Continueremo ancora una volta a scaricare i file necessari di Vue.js ed eventuali librerie esterne da una CDN ed organizziamo la nostra applicazione in moduli javascript, visto che sono ormai implementati nella maggior parte dei browser moderni e tanto basta per lo scopo di questo esempio.

Per il momento tralasciamo degli strumenti come Vue CLI e alla fine di questa lezione potremo soffermarci a valutare quali benefici può introdurre quest’ultima nella fase di sviluppo di un’applicazione neanche troppo complessa.

Vediamo allora come creare la nostra applicazione cercando di suddividere il processo di sviluppo in più fasi.

Definire l’aspetto dell’applicazione in modo superficiale

Il primo passo è quello di realizzare il wireframe dell’applicazione. Si tratta di una rappresentazione superficiale della struttura dell’applicazione. Non andremo a disegnare nulla in dettaglio. Per questo passaggio bastano anche carta e penna oppure è possibile utilizzare dei software che forniscono dei componenti base come quelli mostrati nell’immagine sottostante (L’immagine riportata sotto è stata realizzata con Balsamiq Wireframes che è a pagamento con 30 giorni di prova gratuita).

wireframe applicazione per generare dei codici QR

Possiamo poi procedere alla suddivisione in componenti dell’applicazione. In questo caso non esiste un modo univoco. In base alle funzioni dell’applicazione e alle caratteristiche specifiche dei singoli elementi potremo individuare più componenti diversi, ognuno dei quali ha un ruolo ben determinato ed incapsula tutte le funzioni fra loro correlate.

wireframe applicazione per generare dei codici QR con suddivisione in componenti

Per questa applicazione avremo quindi un’istanza base che ha due componenti figli. Il primo si occupa di ricevere il testo digitato dall’utente e, in seguito al click di un pulsante, comunica le informazioni al componente adiacente il quale si limita a mostrare il codice QR. In quest’ultimo aggiungiamo anche un altro discendente contenente due pulsanti che consentono di scaricare il codice QR appena generato come immagine JPEG o in formato vettoriale SVG.

Configurare l’ambiente di lavoro

Creiamo allora una nuova cartella ed aggiungiamo una serie di file con estensione .js, un file index.html e uno style.css in modo da ottenere la seguente struttura.

tree 
.
├── app.js
├── components
│   ├── QrDisplay.js
│   ├── QrDownloadButtons.js
│   └── QrTextInput.js
├── imgs
│   └── logo.svg
├── index.html
└── style.css

2 directories, 7 files

Abbiamo creato due sottocartelle components e imgs. Tralasciando la cartella delle immagini, in components abbiamo i tre componenti principali dell’applicazione. Il nome dei file segue la notazione PascalCase come suggerito nella guida di stile del sito ufficiale di Vue.js.

Creare l’applicazione in Vue.js

Per semplicità saltiamo qualche passaggio e, sulla base del wireframe riportato sopra, strutturiamo la nostra applicazione nel file index.html. A questo punto abbiamo due alternative: posizionare i vari elementi personalizzati direttamente nel file index.html oppure inserire degli elementi HTML standard per definire la struttura dell’applicazione e riorganizzare successivamente il tutto spostando i blocchi di codice HTML all’interno dei componenti di competenza.

Per facilitare ulteriormente questo passaggio, potremmo anche limitarci a definire soltanto la struttura dell’applicazione con dei componenti statici e pensare solo in una seconda fase ad aggiungere eventuali direttive per la gestione degli eventi e per rendere l’applicazione dinamica.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Vue QR Generator</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <div id="root">

      <div class="form-container">
        <h1><a href="/">Vue Qr Code Generator</a></h1>
        <!-- Elemento relativo al componente QrTextInput -->
        <qr-text-input @text-value="onTextValue"></qr-text-input>
      </div>

      <!-- La direttiva v-if è accompagnata da v-else -->
      <!-- che è applicata all'elemento <qr-display> -->
      <p v-if="isPristine" class="instructions">
        Crea il tuo codice QR compilando il campo di testo
        e premi il pulsante <strong>GENERA CODICE QR</strong>
      </p>

      <!-- Elemento relativo al componente QrDisplay -->
      <qr-display v-else :value="textValue"></qr-display>

    </div>
    
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/qrcode.min.js"></script>
    
    <script type="module" src="app.js"></script>
  </body>
</html>

Nel nostro esempio scarichiamo Vue e la libreria node-qrcode da una CDN. Indichiamo inoltre che il file app.js deve essere trattato come un modulo javascript assegnando l’attributo type="module" ad un elemento <script>.

All’interno del corpo del file index.html inseriamo un elemento <div> con id="root" che funge da elemento base della nostra applicazione.

Al netto degli elementi che utilizziamo poi nei fogli di stile per strutturare l’applicazione, abbiamo essenzialmente due elementi personalizzati <qr-text-input> e <qr-display> che viene visualizzato solo se il valore della proprietà isPristine è false. Altrimenti mostriamo all’utente un messaggio con delle istruzioni che spiegano come utilizzare l’applicazione.

Passiamo allora ad analizzare il file app.js in cui creiamo l’istanza base come mostrato nel frammento di codice riportato sotto.

// file: app.js
import { QrTextInput } from './components/QrTextInput.js';
import { QrDisplay } from './components/QrDisplay.js';

const vm = new Vue({
  el: "#root",
  components: {
    QrTextInput,
    QrDisplay
  },
  data: {
    isPristine: true,
    textValue: ""
  },
  methods: {
    onTextValue(value) {
      if (value !== "") {
        this.isPristine = false;
      }
      this.textValue = value;
    }
  }
});

Nel file app.js importiamo i due componenti QrDisplay e QrTextInput che registriamo localmente in una nuova istanza di tipo Vue. Quest’ultima presenta due proprietà: isPristine è un valore booleano grazie al quale scegliamo quale elemento mostrare nella pagina; textValue è una stringa in cui salviamo il valore digitato nel campo di testo. Tale valore viene aggiornato attraverso il metodo onTextValue() che, attraverso la direttiva v-on, associamo all’evento personalizzato text-value all’elemento <qr-text-input>. La nuova istanza vm appena creata acquisisce il controllo sull’elemento con id pari a ‘root’ (el: "#root") e di conseguenza sugli elementi discendenti.

Vediamo ora in che modo viene emesso l’evento personalizzato text-value dal componente QrTextInput.

// file: components/QrTextInput.js
export const QrTextInput = {
  name: "qr-text-input",
  methods: {
    onClick() {
      // con this.$refs.inputText accediamo all'elemento che
      // presenta l'attributo 'ref' con valore uguale a 'inputText' 
      const inputValue = this.$refs.inputText.value;
      this.$emit("text-value", inputValue);
    }
  },
  template: `
    <form>
      <input
        type="text"
        placeholder="Inserisci del testo qui..."
        ref="inputText"
        autofocus>
      <button 
        class="btn"
        type="submit" 
        @click.prevent="onClick">
          Genera Codice Qr
      </button>
    </form>
  `
};

Nel componente QrTextInput introduciamo un nuovo concetto. Notiamo infatti che sull’elemento <input> del template è presente un attributo ref a cui assegniamo un identificativo ‘inputText’ (nome scelto a nostro piacimento). In questo modo possiamo accedere direttamente all’elemento su cui è presente ref attraverso this.$refs.inputText. Così facendo recuperiamo il valore del campo di testo quando viene premuto il pulsante ‘Genera Codice Qr’ ed emettiamo un evento personalizzato ‘text-value’ a cui associamo il valore presente nel campo di testo.

Ogni volta che un utente clicca il pulsante, viene generato un evento in seguito al quale invochiamo il metodo onTextValue() dell’istanza base che aggiorna il valore della sua proprietà textValue.

Nel file index.html tale proprietà viene associata alla ‘prop’ value del componente QrDisplay grazie alla direttiva v-bind. Per questo motivo quando textValue cambia, viene automaticamente passato il suo nuovo valore al componente QrDisplay.

// file: components: QrDisplay.js
import { QrDownloadButtons } from "./QrDownloadButtons.js";

export const QrDisplay = {
  name: "qr-display",
  components: {
    QrDownloadButtons
  },
  props: {
    value: {
      type: String,
      default: ""
    }
  },
  data() {
    return {
      hasError: false,
      imgSrc: null,
      svgSrc: null,
      dataUrlOpts: {
        errorCorrectionLevel: "H",
        type: "image/jpeg",
        quality: 0.8,
        margin: 1,
        width: 256
      },
      stringOpts: {
        type: "svg"
      }
    };
  },
  watch: {
    value: {
      immediate: true,
      handler(newVal, oldVal) {
        if (newVal) {
          QRCode.toDataURL(newVal, this.dataUrlOpts, (err, url) => {
            if (err) {
              this.hasError = true;
            } else {
              this.imgSrc = url;
            }
          });

          QRCode.toString(newVal, this.stringOpts, (err, string) => {
            if (err) {
              this.hasError = true;
            } else {
              this.svgSrc = `data:image/svg+xml;base64,${btoa(string)}`;
            }
          });
        }
      }
    }
  },
  template: `
    <div class="display-container">
      <div class="display">
        <div class="qr-code">
          <p class="error" v-if="hasError">
            <span class="error-icon">
              <!-- icona di avviso associata al messaggio di errore -->
              <svg 
                height="24"
                viewBox="0 0 24 24" 
                width="24" 
                xmlns="http://www.w3.org/2000/svg">
                  <path d="m0 0h24v24h-24z" fill="none"/>
                  <path 
                    fill="currentColor" 
                    d="m1 21h22l-11-19zm12-3h-2v-2h2zm0-4h-2v-4h2z"/>
              </svg>
            </span>
            Impossibile generare il codice QR.
            <br>Si &egrave; verificato un errore.
          </p>
          <img v-else :src="imgSrc" alt="">
        </div>
      </div>
      <QrDownloadButtons :imgSrc="imgSrc" :svgSrc="svgSrc" />
    </div>
  `
};

Nel componente QrDisplay registriamo QrDownloadButtons che usiamo nel template e al quale passiamo due ‘props’ imgSrc e svgSrc, associate tramite v-bind alle omonime proprietà di QrDisplay.

Per quest’ultimo abbiamo inoltre definito una solo ‘prop’ value che è di tipo stringa.

La funzione data() restituisce invece un oggetto di proprietà:

  • hasError consente di mostrare un messaggio di errore ed è infatti utilizzata nel template del componente insieme alla direttiva v-if per rimuovere o inserire nel DOM un elemento <p class="error">.
  • imgSrc e svgSrc vengono opportunamente impostate ogni volta che la ‘prop’ value subisce un cambiamento. Si tratta di stringhe di testo particolari che seguono il particolare schema degli identificatori di risorse i quali permettono di includere dei dati, come delle immagini, direttamente nei file HTML. La sintassi utilizzata è nella forma data:[<media type>][;charset=<character set>][;base64],<data>.
  • dataUrlOpts e stringOpts sono invece degli oggetti di opzioni che passeremo ai metodi della libreria node-qrcode per generare opportunamente l’immagine in formato JPEG e SVG.

Per la proprietà watch abbiamo poi utilizzato la forma estesa in cui possiamo dare maggiori indicazioni sulle azioni da eseguire quando la ‘prop’ value riceve un nuovo valore. Grazie all’opzione immediate: true indichiamo che il corpo del metodo handler() deve essere eseguito anche quando al componente viene passato un valore in fase di creazione.

In questo modo Vue osserva eventuali modifiche della ‘prop’ value del componente e risponde eseguendo le azioni che abbiamo espresso nel metodo handler().

QrDisplay prende infatti la stringa presente in value e la trasforma usando i metodi QRCode.toDataURL() e QRCode.toString. Così facendo creiamo le stringhe imgSrc e svgSrc che seguono lo schema descritto in precedenza.

In particolare imgSrc viene associata tramite la direttiva v-bind all’attributo src dell’immagine.

Entrambe le proprietà sono quindi inoltrate al componente discendente QrDownloadButtons che presenta due pulsanti attraverso i quali è possibile scaricare il codice QR in formato JPEG o SVG.

export const QrDownloadButtons = {
  name: "qr-download-buttons",
  props: {
    imgSrc: String,
    svgSrc: String
  },
  methods: {
    timestamp() {
      return Date.now();
    }
  },
  template: `
    <div class="download-area">
      <div class="buttons">
        <a class="btn" :href="imgSrc" :download="'qr_code_' + timestamp() + '.jpeg'">
          Download <br><small>jpeg</small>
        </a>
        <a class="btn" :href="svgSrc" :download="'qr_code_' + timestamp() + '.svg'">
          Download <br><small>svg</small>
        </a>
      </div>
    </div>
  `
};
https://vimeo.com/462643476

Sebbene siamo riusciti a realizzare la nostra prima applicazione di una certa complessità, possiamo evidenziare alcuni evidenti problematiche e difetti che vengono amplificati al crescere delle dimensioni di un progetto. Non abbiamo analizzato il contenuto, ma abbiamo usato un solo file CSS globale per l’intera applicazione. Può quindi succedere che vi sia collisione dei nomi delle classi usate e al contrario del codice HTML e Javascript, non esiste un modo per inserire delle regole CSS direttamente nella definizione di un componente. Potremmo risolvere il problema della collisione dei nomi utilizzando una metodologia come BEM per i nomi delle classi, ma sarebbe comunque più utile se ciascun componente avesse delle proprie regole CSS applicate soltanto agli elementi del suo template.

A tal proposito notiamo inoltre che affidarci alla sola proprietà template non è probabilmente la soluzione migliore per definire la struttura dei componenti. Infatti al crescere della complessità di questi ultimi, risulta sempre più difficoltoso leggere ed analizzare attentamente il contenuto della stessa proprietà template.

Senza volersi dilungare troppo, è facile intuire che, per le ragioni esposte sopra e per semplificare la fase di sviluppo di un’applicazione, servirebbe uno strumento in grado di organizzare meglio l’ambiente di lavoro e di integrare senza fatica altri strumenti utili come Eslint o framework da usare in fase di scrittura dei test di unità dell’applicazione.

Esiste una soluzione ufficiale, si chiama Vue CLI e sarà l’argomento che tratteremo nella prossima lezione.

Riepilogo

In questa lezione abbiamo visto come creare un generatore di codici QR rudimentale usando i concetti e le tecniche apprese finora. Attraverso pochi semplici componenti siamo riusciti a realizzare un’applicazione in Vue.js. Abbiamo però visto quali difficoltà presenta l’approccio usato finora e nella prossima lezione introdurremo Vue CLI che consente di configurare l’ambiente di lavoro e strutturare meglio un’applicazione in pochi semplici passaggi.

Pubblicitร