10 Aprile, 2019 | Di

Drupal 8 Decoupled con JSON:API e Vue.JS - parte 2

Drupal 8 Decoupled con JSON:API e Vue.JS - parte 2

Previously on Wellnet...

Nella prima parte di questo articolo ci siamo soffermati sulle specifiche di JSON:API e su come in Drupal, mediante l'apposito modulo del core, queste vengano implementate.

L'intento del tutorial è quello di far emergere le possibilità che offre l'approccio "decoupled" abbinato all'utilizzo di un formato specifico con cui le API vengono integrate (nel nostro caso JSON:API). Infatti una volta definito un formato, che ci obbliga a effettuare richieste solo secondo determinati modelli, non avremo limiti nello sviluppo del frontend: da framework e librerie frontend javascript fino ad arrivare a qualsiasi software in grado di effettuare chiamate HTTP.

Nel nostro caso realizzeremo un semplice frontend utilizzando Vue.js 2.x abbinato a Vue Bootstrap e Axios (come client HTTP). 

Il codice finale è disponibile al seguente repository: https://github.com/ninomarrazzo/vued8-jsonapi. Per eseguirlo sono sufficienti queste operazioni:

 

Requisiti necessari

Ai fini di questo articolo, diamo per scontato che tu sia già a conoscenza di Vue.js, o quantomeno della sua esistenza come framework, e che tu abbia le basi minime per poter interpretare la logica che sottostà a una componente (file composto da HTML + CSS + JS inerenti a un'unico elemento logico della nostra Web App).

Rimane da fare un'ultima precisazione: è probabile che nel tentare di replicare quanto appreso nella prima parte sia stato creato un cosiddetto ambiente locale. Se è così, sarà necessario abilitare il supporto CORS su Drupal nel seguente modo:

  • Crea una copia del file /sites/default/default.services.yml rinominata in /sites/default/services.yml
  • Modifica il file appena copiato alla voce cors.config come segue:
  cors.config:
    enabled: true
    # Specify allowed headers, like 'x-allowed-header'.
    allowedHeaders: ['x-csrf-token','authorization','content-type','accept','origin','x-requested-with', 'access-control-allow-origin','x-allowed-header']
    # Specify allowed request methods, specify ['*'] to allow all possible ones.
    allowedMethods: ['*']
    # Configure requests allowed from specific origins.
    allowedOrigins: ['*']
    # Sets the Access-Control-Expose-Headers header.
    exposedHeaders: false
    # Sets the Access-Control-Max-Age header.
    maxAge: false
    # Sets the Access-Control-Allow-Credentials header.
    supportsCredentials: false

Vue.js 2.x

Vue.js dispone anche di un proprio client che permette di inizializzare e gestire un progetto basato su vue. Installarlo è molto semplice, però è necessario avere installato anche node.js poichè con esso è fornito npm - client per node.js che permette di installare e gestire dipendenze javascript.

Per installare il client di Vue.js basta eseguire, da terminale, il comando: npm install -g @vue/cli

Dopodichè sarà possibile spostarsi nella cartella di progetto ed eseguire il comando: vue init webpack webapp

"webapp" è la cartella all'interno della quale vue-cli inizializzerà il tuo progetto.

Il comando appena eseguito ti chiederà di interagire inserendo alcuni valori. Nello specifico

  • ? Project name nome-progetto

  • ? Project description descrizione-progetto

  • ? Author mail-autore

  • ? Vue build standalone

  • ? Install vue-router? Yes

  • ? Use ESLint to lint your code? No

  • ? Set up unit tests No

  • ? Setup e2e tests with Nightwatch? No

  • ? Should we run `npm install` for you after the project has been created? (recommended) npm

Spostati ora all'interno della cartella webapp con cd webapp e installa il client HTTP axios con il seguente comando: npm install axios

Installa infine Vue Bootstrap con il comando: npm install bootstrap-vue bootstrap

Per rendere disponibile in tutta la web app gli elementi bootstrap sarà necessario aggiungere il seguente codice nel file src/main.js

// ...
import BootstrapVue from 'bootstrap-vue'
 
Vue.use(BootstrapVue)
// ...
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

Iniziamo!

Apri la cartella di progetto creata da vue-cli con il tuo editor/IDE preferito. Dovresti vedere qualcosa del genere:

Vue.js project directory tree

Salta subito all'occhio la cartella src che, come suggerisce il nome stesso, contiene il codice sorgente della tua web app. Senza soffermarci troppo sui singoli file, creerai due componenti Vue all'interno di src/components. Una chiamata LayoutHeader.vue contenente, appunto, l'header della web app.  L'altra chiamata PageHome.vue che si occuperà di interrogare Drupal per richiedere la lista degli articoli da renderizzare.

Le componenti verranno assemblate nel file componente src/App.vue, a breve vedremo come.

Componente LayoutHeader.vue

Per realizzare l'header viene sfruttato l'elemento Jumbotron di Bootstrap. Il codice della componente LayoutHeader.vue è:

<template>
  <b-jumbotron header="D8 JSON:API Web App" lead="Decoupling Drupal 8 using VueJS framework and JSON:API specification"></b-jumbotron>
</template>

Molto semplice! Una componente Vue è composta essenzialmente da 3 parti: template, stile e logica javascript. Rispettivamente rispondenti ai seguenti tag:

<template></template>, <style></style> e <script></script>. Il fatto che in questo caso ci sia solo il template non comporta nulla di sbagliato, semplicemente non vogliamo personalizzare ulteriormente (più di quanto non faccia già bootstrap) l'elemento jumbotron.

Ora che la componente è pronta, puoi importarla e posizionarla in alto, nella componente principale App.vue.

Vue-cli si è occupato di inizializzare al tuo posto il progetto a partire da un repository. Infatti, noterai che nella componente src/App.vue vi è già un <template> che altro non fa che aggiungere un <div id="app"> e, al suo interno, un richiamo a un'altra componente <router-view />

Router-view è una componente messa a disposizione dal plugin vue-router (installato al momento della risposta Y alla domanda "Install vue-router?") che si occupa, come suggerito dal nome, di gestire il routing. Quindi router-view sarà rimpiazzato dalla componente che risponde al path appena visitato. La logica del routing la trovi nel file ./router/index.js. Vediamo com'è fatto:

import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
 
Vue.use(Router)
 
export default new Router({
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld
    }
  ]
})

L'oggetto Router viene instanziato passandogli un'oggetto contenente l'array con chiave routes all'interno del quale verranno definiti gli oggetti di configurazione delle varie "rotte".

Quello che farai sarà creare una nuova componente chiamata PageHome.vue che importerai nel file ./router/index.js per poi collegarla al path '/' al posto della componente HelloWorld.

La componente PageHome.vue sarà quella che effettuerà la chiamata GET a /jsonapi/node/article. Il tutto verrà mostrato all'interno di una lista di box rettangolari. Il risultato desiderato è il seguente:

Web App Completa

Quanto visibile nell'immagine, come dicevamo, viene assemblato insieme nel file App.vue, il quale ci mostra in alto la componente LayoutHeader.vue e in basso PageHome.vue. Vediamo quindi come è realizzata la componente PageHome.vue per poi passare al resto:

<template>
  <b-container class="bv-example-row">
    <b-row class="row article" v-for="(article, index) in articles" :key="index">
      <b-col sm="12" lg="3" md="3"><b-img :src="'http://d8jsonapiapp.lndo.site' + included[index].attributes.uri.url" fluid alt="Responsive image"></b-img></b-col>
      <b-col sm="12" md="9" lg="9">
        <h1 class="title"> {{ article.attributes.title }} </h1>
        <div class="body" v-html="article.attributes.body.value">
        </div>
      </b-col>
    </b-row>
  </b-container>
</template>
<script>
import axios from 'axios'
 
export default {
  name: 'HelloWorld',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App',
      articles: {},
      included: {}
    }
  },
  mounted () {
    const inst = this
    axios.get('http://d8jsonapiapp.lndo.site/jsonapi/node/article?include=field_image', {
      headers: {
        'Accept': 'application/vnd.api+json'
      }
    }).then(function (result) {
      inst.articles = result.data.data
      inst.included = result.data.included
    })
  },
  methods: {}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.row.article {
  transition: all 0.2s;
  margin-bottom: 1em;
  box-shadow: 0px 0px 10px lightblue;
  padding: 1em;
}
.row.article:hover {
  background: lightblue;
  color: white;
}
.row.article:hover img {
  transform: rotate3d(0, 20, 1, 12deg);
}
</style>

Il template di questa componente non fa altro che sfruttare gli elementi CSS della griglia di Bootstrap (container, righe e colonne). La griglia è composta apparentemente da una riga. Dico apparentemente perchè in realtà la componente b-row è gestita da una direttiva Vue: v-for che, in effetti, esegue un loop sulla variabile articles. Come avrai capito, quest'ultima contiene i nodi di tipo article prelevati dalla tua installazione Drupal. Per capire come viene valorizzata articles, devi concentrarti sul metodo mounted (quest'ultimo appartiene al cosiddetto Vue.js Lifecycle e viene eseguito quando l'istanza della componente è "montata"). È in questo metodo che in effetti eseguiamo, tramite axios, la chiamata GET all'endpoint:

http://d8jsonapiapp.lndo.site/jsonapi/node/article?include=field_image

Questo URL ti risulterà familiare se hai già letto la prima parte di questo tutorial.

C'è un solo elemento "sconosciuto": il parametro include valorizzato con field_image. Questo parametro indica al server che implementa JSON:API che, oltre alla risorsa richiesta, deve essere fornito anche il resource object ad essa relazionato, nel nostro caso il field_image, ovvero l'immagine dell'articolo.  In questo modo abbiamo ridotto le chiamate da fare da due a una! Come risposta ci verrà fornito un document composto dall'array dei resuorce objects e l'array degli included, ovvero i campi immagine, uno per ogni resource object, relativi ai contenuti richiesti. Un'altra cosa stabilita durante l'esposizione della prima parte di questo tutorial è la presenza dell'header Accept, da impostare col media type application/vnd.api+json.

I risultati ottenuti dalla chiamata vengono poi passati alle variabili articlesincluded così da poterle utilizzare nel template.

 

App.vue

Ora, vediamo il codice della componente App.vue nella quale, come già detto, avviene l'assemblaggio della nostra web app vue. Lo script di questa si occupa di importare la componente LayoutHeader.vue e posizionarla prima della componente router-view così da apparire sempre in alto rispetto al contenuto iniettato dal router.

<template>
  <div id="app">
    <layout-header></layout-header>
    <router-view/>
  </div>
</template>
 
<script>
import LayoutHeader from './components/Layout/LayoutHeader'
export default {
  name: 'App',
  components: {
    LayoutHeader
  }
}
</script>

Alcune considerazioni

Come hai potuto notare, formalizzare delle specifiche per chiamate/risposte HTTP comporta una serie di vantaggi non indifferenti. Intanto, se la forma della richiesta che il server si aspetta di ricevere è astratta o formalizzata, secondo specifiche regole, questo fa sì che un client qualsiasi, che sia esso la nostra web app o un'app nativa (poco importa), saprà sempre ottenere le informazioni di cui necessita poiché saprà di doversi interfacciare secondo il formato, nel nostro caso, imposto da JSON:API. Inoltre, abbiamo visto come JSON:API permette di relazionare logicamente le risorse riducendo il numero di richieste necessarie e quindi il peso e la velocità complessiva della nostra Web App. Cosa che, nel caso di applicazioni più complesse, farà sicuramente la sua bella differenza. Un altro vantaggio conseguente all'adozione di JSON:API risiede nella documentazione delle API. Cosa voglio intendere? È risaputo che una delle parti più importanti di una API è la sua documentazione quindi, ogni qualvolta ci si ritrova a doverne implementare una, si è costretti a impiegare elevate quantità di tempo per documentare quanto più dettagliatamente le varie possibilità offerte dalle nostre API. Nel nostro caso, quello di JSON:API, buona parte del lavoro è stata già fatta, appunto, dai suoi sviluppatori i quali hanno dovuto documentare ogni singolo aspetto delle specifiche del formato.

 

Potrebbe interessarti: