.Blog

Drupal REACTivated

Drupal REACTivated

Come creare siti decoupled con Drupal, React e JSON API

In questa serie di articoli vedremo come creare un piccolo sito web che faccia uso di Drupal come back-end e React come front-end. I due lati della nostra applicazione comunicheranno utilizzando endpoint esposti dal modulo JSON API.

Obiettivi

Il sito web che andremo a sviluppare dovrà fornire le seguenti funzionalità:

  • Permettere agli Utenti (anche quelli non registrati) di consultare l’elenco degli Articoli pubblicati.
  • Permettere di accedere al dettaglio di ogni Articolo.
  • Permettere agli Utenti di effettuare il login al sito.
  • Permettere agli Utenti registrati di aggiungere nuovi Articoli.

La serie sarà suddivisa in tre parti.
La prima (ovvero quella che state leggendo) avrà lo scopo di introdurre i concetti e le tecnologie che verranno poi utilizzate durante lo sviluppo del sito. Sarà sempre in questa prima parte che andremo a creare lo scheletro dei due progetti (back-end e front-end) e mostreremo una prima interazione fra essi.
Nella seconda parte ci focalizzeremo su come recuperare dati memorizzati nel nostro CMS per poi visualizzarli attraverso le nostre componenti React.
Nella terza e ultima parte invece andremo ad implementare le funzioni di Login verso il sito e di creazione di nuovi Articoli.

Requisiti

Questa serie ha come scopo quello di mostrare una delle possibili forme di integrazione tra Drupal e React.
Il requisito principale è dunque una conoscenza seppur minima del CMS Drupal e di React.
Prima di continuare a leggere è bene avere installati sul pc i seguenti strumenti:

Per l'installazione di tutti questi componenti si rimanda ai rispettivi siti che includono tutte le informazioni necessarie.

Coupled vs. Decoupled

Tradizionalmente i CMS, e quindi anche Drupal, hanno sempre fatto uso di un'architettura che accoppia le funzionalità di back-end con quelle di front-end. Da qualche anno però i siti web si stanno trasformando sempre più in piattaforme con cui gli utenti vogliono interagire in modo reattivo/interattivo.


L’utilizzo di framework client-side permette sicuramente di rispondere a tale esigenza ma porta con sé nuove problematiche che è necessario affrontare. Le soluzioni architetturali che utilizzano un framework client-side per il recupero e il rendering dei contenuti e un back-end per memorizzare e fornire i dati sono solitamente chiamate decoupled (disaccoppiate), questo appunto per la netta separazione tra front-end e back-end.

Setup del backend

Il nostro progetto Drupal sarà basato sul template Composer.
Per la generazione di un nuovo progetto è necessario utilizzare il comando

composer create-project drupal-composer/drupal-project:8.x-dev drupal-reactivated-backend --stability dev --no-interaction --prefer-dist

Una volta creato il progetto è possibile procedere con la normale installazione di Drupal utilizzando il profilo Standard.

REST API

La versione 8 di Drupal include già tutti i moduli e le funzionalità necessarie per lo sviluppo di siti web decoupled. Le REST API incluse nel Core permettono di esporre i nostri contenuti ad applicazioni esterne senza molto sforzi. Per recuperare un nodo l’url da utilizzare è del tipo http://localhost:8000/node/1?_format=hal_json. Analizziamo per un attimo quanto otteniamo in risposta dal nostro CMS.

{
    "_links": {
        "self": {
            "href": "http://localhost:8000/node/1?_format=hal_json"
        },
        "type": {
            "href": "http://localhost:8000/rest/type/node/article"
        },
        "http://localhost:8000/rest/relation/node/article/uid": [
            {
                "href": "http://localhost:8000/user/1?_format=hal_json",
                "lang": "en"
            }
        ],
        "http://localhost:8000/rest/relation/node/article/revision_uid": [
            {
                "href": "http://localhost:8000/user/1?_format=hal_json"
            }
        ],
        "http://localhost:8000/rest/relation/node/article/field_tags": [
            {
                "href": "http://localhost:8000/taxonomy/term/1?_format=hal_json",
                "lang": "en"
            },
            {
                "href": "http://localhost:8000/taxonomy/term/2?_format=hal_json",
                "lang": "en"
            }
        ]
    },
    "nid": [
        {
            "value": 1
        }
    ],
    "uuid": [
        {
            "value": "7ea3205d-88aa-495f-88c8-726d86a503af"
        }
    ],
    "vid": [
        {
            "value": 2
        }
    ],
    "langcode": [
        {
            "value": "en",
            "lang": "en"
        }
    ],
    "type": [
        {
            "target_id": "article"
        }
    ],
    "status": [
        {
            "value": true,
            "lang": "en"
        }
    ],
    "title": [
        {
            "value": "Drupal REACTivated ",
            "lang": "en"
        }
    ],
    "_embedded": {
        "http://localhost:8000/rest/relation/node/article/uid": [
            {
                "_links": {
                    "self": {
                        "href": "http://localhost:8000/user/1?_format=hal_json"
                    },
                    "type": {
                        "href": "http://localhost:8000/rest/type/user/user"
                    }
                },
                "uuid": [
                    {
                        "value": "0ae74614-6ff2-483b-82bb-f3e609f609c7"
                    }
                ],
                "lang": "en"
            }
        ],
        "http://localhost:8000/rest/relation/node/article/revision_uid": [
            {
                "_links": {
                    "self": {
                        "href": "http://localhost:8000/user/1?_format=hal_json"
                    },
                    "type": {
                        "href": "http://localhost:8000/rest/type/user/user"
                    }
                },
                "uuid": [
                    {
                        "value": "0ae74614-6ff2-483b-82bb-f3e609f609c7"
                    }
                ]
            }
        ],
        "http://localhost:8000/rest/relation/node/article/field_tags": [
            {
                "_links": {
                    "self": {
                        "href": "http://localhost:8000/taxonomy/term/1?_format=hal_json"
                    },
                    "type": {
                        "href": "http://localhost:8000/rest/type/taxonomy_term/tags"
                    }
                },
                "uuid": [
                    {
                        "value": "38974d84-f90b-4169-8217-6acd9bb5d7dd"
                    }
                ],
                "lang": "en"
            },
            {
                "_links": {
                    "self": {
                        "href": "http://localhost:8000/taxonomy/term/2?_format=hal_json"
                    },
                    "type": {
                        "href": "http://localhost:8000/rest/type/taxonomy_term/tags"
                    }
                },
                "uuid": [
                    {
                        "value": "f4c35900-8d34-4165-b842-ac19b7f3e5d8"
                    }
                ],
                "lang": "en"
            }
        ]
    },
    "created": [
        {
            "value": 1501681133,
            "lang": "en"
        }
    ],
    "changed": [
        {
            "value": 1501681184,
            "lang": "en"
        }
    ],
    "promote": [
        {
            "value": true,
            "lang": "en"
        }
    ],
    "sticky": [
        {
            "value": false,
            "lang": "en"
        }
    ],
    "revision_timestamp": [
        {
            "value": 1501681184
        }
    ],
    "revision_translation_affected": [
        {
            "value": true,
            "lang": "en"
        }
    ],
    "default_langcode": [
        {
            "value": true,
            "lang": "en"
        }
    ],
    "body": [
        {
            "value": "<p>My article body.</p>\r\n",
            "format": "basic_html",
            "summary": "",
            "lang": "en"
        }
    ],
    "comment": [
        {
            "status": 2,
            "cid": 0,
            "last_comment_timestamp": 1501681172,
            "last_comment_name": null,
            "last_comment_uid": 1,
            "comment_count": 0,
            "lang": "en"
        }
    ]
}

La risposta ottenuta è sicuramente molto completa ma allo stesso molto complessa. Nella nostra applicazione abbiamo quindi deciso di non utilizzare le REST API.

JSON API

Una valida alternativa alle REST API è fornita dal modulo JSON API. Lo scopo di tale modulo è di implementare lato CMS gli endpoint necessari a rendere le risorse presenti sul sito disponibili secondo lo standard {json:api}. L’installazione del modulo è molto semplice. Dopo avere effettuato il download utilizzando composer

composer require drupal/jsonapi

è sufficiente abilitare il modulo affinché gli endpoint siano da subito attivi. JSON API non dipende in alcun modo dal modulo Rest, la sua unica dipendenza è Serialization.

 

Proviamo a recuperare gli stessi dati che avevamo ottenuto utilizzando le REST API. Questa volta l’url da utilizzare è http://localhost:8000/jsonapi/node/article/b4b9f5e7-f3fe-41ea-9c9e-c1c4b....
Notiamo subito due grandi differenze.
Per prima cosa è necessario specificare oltre al nome/tipo dell’entità (node) anche il bundle specifico (article) questo per evitare problemi dovuti al fatto che bundle differenti possono avere campi differenti.
La seconda grossa differenza consiste nel fatto che non si lavora più con i nid ma con i uuid (b4b9f5e7-f3fe-41ea-9c9e-c1c4b46c15a7).
Passiamo all’analisi della risposta ottenuta.

{
    "data": {
        "type": "node--article",
        "id": "7ea3205d-88aa-495f-88c8-726d86a503af",
        "attributes": {
            "nid": 1,
            "uuid": "7ea3205d-88aa-495f-88c8-726d86a503af",
            "vid": 2,
            "langcode": "en",
            "status": true,
            "title": "Drupal REACTivated ",
            "created": 1501681133,
            "changed": 1501681184,
            "promote": true,
            "sticky": false,
            "revision_timestamp": 1501681184,
            "revision_log": null,
            "revision_translation_affected": true,
            "default_langcode": true,
            "path": null,
            "body": {
                "value": "<p>My article body.</p>\r\n",
                "format": "basic_html",
                "summary": ""
            },
            "comment": {
                "status": 2,
                "cid": 0,
                "last_comment_timestamp": 1501681172,
                "last_comment_name": null,
                "last_comment_uid": 1,
                "comment_count": 0
            }
        },
        "relationships": {
            "type": {
                "data": {
                    "type": "node_type--node_type",
                    "id": "b1126548-d96d-419d-be7a-c494e3be4e90"
                },
                "links": {
                    "self": "http://localhost:8000/jsonapi/node/article/7ea3205d-88aa-495f-88c8-726d8...,
                    "related": "http://localhost:8000/jsonapi/node/article/7ea3205d-88aa-495f-88c8-726d8...
                }
            },
            "uid": {
                "data": {
                    "type": "user--user",
                    "id": "0ae74614-6ff2-483b-82bb-f3e609f609c7"
                },
                "links": {
                    "self": "http://localhost:8000/jsonapi/node/article/7ea3205d-88aa-495f-88c8-726d8...,
                    "related": "http://localhost:8000/jsonapi/node/article/7ea3205d-88aa-495f-88c8-726d8...
                }
            },
            "revision_uid": {
                "data": {
                    "type": "user--user",
                    "id": "0ae74614-6ff2-483b-82bb-f3e609f609c7"
                },
                "links": {
                    "self": "http://localhost:8000/jsonapi/node/article/7ea3205d-88aa-495f-88c8-726d8...,
                    "related": "http://localhost:8000/jsonapi/node/article/7ea3205d-88aa-495f-88c8-726d8...
                }
            },
            "field_image": {
                "data": null
            },
            "field_tags": {
                "data": [
                    {
                        "type": "taxonomy_term--tags",
                        "id": "38974d84-f90b-4169-8217-6acd9bb5d7dd"
                    },
                    {
                        "type": "taxonomy_term--tags",
                        "id": "f4c35900-8d34-4165-b842-ac19b7f3e5d8"
                    }
                ],
                "links": {
                    "self": "http://localhost:8000/jsonapi/node/article/7ea3205d-88aa-495f-88c8-726d8...,
                    "related": "http://localhost:8000/jsonapi/node/article/7ea3205d-88aa-495f-88c8-726d8...
                }
            }
        },
        "links": {
            "self": "http://localhost:8000/jsonapi/node/article/7ea3205d-88aa-495f-88c8-726d8...
        }
    },
    "links": {
        "self": "http://localhost:8000/jsonapi/node/article/7ea3205d-88aa-495f-88c8-726d8...
    }
}

Il json inviato dal server include tutte le informazioni che erano presenti nella versione precedente ma in modo molto più conciso. Il modulo JSON API utilizzando la definizione dei campi associati al nostro bundle riesce ad evitare l’inserimento di inutili array e oggetti contenenti il solo attributo “value”.

CORS

Quando un’applicazione effettua una richiesta per accedere ad una risorsa che risiede su un dominio, protocollo o porta differente si genera quella che viene definita cross-origin HTTP request. Per questioni di sicurezza, per cui non entriamo nel dettaglio, i browser bloccano preventivamente le richieste di tipo cross-origin generate da script presenti nella pagine web. La nostra applicazione decoupled farà proprio utilizzo di chiamate cross-origin in quanto sarà il nostro client Javascript ad interrogare Drupal attraverso uno degli endpoint forniti. Il front-end React sarà un’applicazione web completamente differente dal nostro CMS Drupal.
Potrà essere deployata su un server diverso e anche quando saranno entrambi in locale faranno uso di porte differenti. È necessario quindi trovare un modo per permettere al nostro client Javascript di effettuare liberamente le richieste senza che il browser le blocchi.
Nella versione 2.0 delle Web API è stato introdotto il supporto alle richieste cross-origin a cui è stato attribuito il nome CORS (Cross-Origin Resource Sharing). Seguendo le specifiche CORS, i Web Server hanno la possibilità di controllare l’accesso alle richieste cross-origin.
La versione 8 di Drupal permette di gestire la configurazione di CORS andando a modificare alcuni valori nel file services.yml presente nella directory ‘sites/default’. Nel caso in cui il file non risulti presente è possibile copiare e rinominare il file default.services.yml presente nella directory stessa directory.

cp default.services.yml services.yml

I valori contenuti nel file sono i seguenti:

   # Configure Cross-Site HTTP requests (CORS).
   # Read https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
   # for more information about the topic in general.
   # Note: By default the configuration is disabled.
  cors.config:
    enabled: false
    # Specify allowed headers, like 'x-allowed-header'.
    allowedHeaders: []
    # 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

quindi di default CORS è disabilitato (enabled: false). Per realizzare la nostra applicazione sarà necessario cambiare la configurazione nel modo seguente

   # Configure Cross-Site HTTP requests (CORS).
   # Read https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
   # for more information about the topic in general.
   # Note: By default the configuration is disabled.
  cors.config:
    enabled: true
    # Specify allowed headers, like 'x-allowed-header'.
    allowedHeaders: ['*']
    # Specify allowed request methods, specify ['*'] to allow all possible ones.
    allowedMethods: ['*']
    # Configure requests allowed from specific origins.
    allowedOrigins: ['http://localhost:3000']
    # 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

in particolare oltre all’attivazione di CORS (enabled: true) permetteremo l’esecuzione di richieste cross-origin solo dall’indirizzo 'http://localhost:3000' che come vedremo sarà quello utilizzato dalla nostra applicazione React. Le impostazioni vanno ovviamente modificate nel caso in cui si decida di provare l'applicazione React ospitandola su un server remoto.

Setup del front-end

Come libreria javascript per lo sviluppo del nostro front-end abbiamo scelto React. Per creare lo scheletro della nostra applicazione utilizzeremo il tool ufficiale sviluppato da Facebook chiamato create-react-app. Per installarlo digitare il seguente comando

npm install -g create-react-app

Una volta installato creiamo la nostra applicazione utilizzando

create-react-app drupal-reactivated-frontend

Se è la prima volta che utilizzate questo tool è utile leggere la documentazione di React presente sul repository.

SuperAgent

All’interno della nostra applicazione React per interrogare gli endpoint del modulo JSON API in alternativa alle Fetch API abbiamo scelto di utilizzare una libreria chiamata SuperAgent. La scelta di tale libreria, seppur arbitraria, è dovuta alla disponibilità di un plugin dal nome superagent-jsonapify che consente una migliore integrazione con il risultato delle interrogazioni verso server che implementano le specifiche {json:api}.
Per utilizzare le due librerie all’interno della nostra applicazione react aggiungiamole alle dipendenze utilizzando Yarn

yarn add superagent superagent-jsonapify

Applicazione React

Dopo aver configurato correttamente back-end e front-end possiamo partire col lo sviluppo della prima versione del nosto sito web.
In questa prima parte il risultato finale sarà quello di visualizzare il numero di Articoli presenti nel nostro CMS Drupal.

Per recuperare la lista degli Articoli, l’url da interrogare è il seguente http://localhost:8000/jsonapi/node/article. Di default il modulo JSON API applica una paginazione che permette di ottenere dal server solo 50 articoli per volta (vedremo come è possibile ridefinire questo parametro).
Rispetto a quanto generato automaticamente da create-react-app l’unico file su cui abbiamo apportato delle modifiche è src/App.js.
Andiamo ad analizzare il suo contenuto dopo le modifiche.

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import superagent from 'superagent';
import superagentJsonapify from 'superagent-jsonapify';

superagentJsonapify(superagent);

const BASE_URL = 'http://localhost:8000'

class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      articles: null
    }
  }

  render() {
    const { error, errorMessage, articles, more } = this.state

    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h2>Welcome to Drupal REACTivated</h2>
        </div>
        <p className="App-intro">
          <button onClick={this.handleCountClick}>Count Articles</button>
        </p>
        {
          error &&
          <h2>{errorMessage}</h2>
        }
        {
          articles &&
          <h2>You have {more ? `more than ${articles.length}` : articles.length} articles on your Drupal backend.</h2>
        }
      </div>
    );
  }

  handleCountClick = () => {
    superagent.get(`${BASE_URL}/jsonapi/node/article`)
      .then(response => {
        const body = response.body
        const articles = body.data
        const more = body.links.hasOwnProperty('next')
        this.setState({
          error: false,
          articles: articles,
          more: more
        })
      })
      .catch((error) => {
        this.setState({
          error: true,
          errorMessage: `Error fetching articles: '${error.message}'`
        })
      })
  }
}

export default App;

Nel costruttore del nostro component App viene inizializzato lo stato impostando

    this.state = {
      articles: null
    }

Come vedremo all’interno di questa variabile sarà memorizzato l’elenco degli Articoli recuperati dal server.
La funzione render() svolge principalmente due funzioni. La prima è quella di mostrare un pulsante ‘Count Articles’ con un handler onClick che avvia la richiesta verso le JSON API.
La seconda è quella di mostrare il risultato della richiesta, memorizzato nello stato del componente, sia in caso che tutto vada a buon fine che in caso di errore.
Il cuore della nostra applicazione risiede per il momento nel metodo handleCountClick().
Analizziamone il comportamento

handleCountClick = () => {
    superagent.get(`${BASE_URL}/jsonapi/node/article`)
      .then(response => {
        const body = response.body
        const articles = body.data
        const more = body.links.hasOwnProperty('next')
        this.setState({
          error: false,
          articles: articles,
          more: more
        })
      })
      .catch((error) => {
        this.setState({
          error: true,
          errorMessage: `Error fetching articles: '${error.message}'`
        })
      })
  }

Attraverso la libreria superagent andiamo ad effettuare una richiesta verso l’url ‘${BASE_URL}/jsonapi/node/article’ di tipo GET. La risposta ottenuta contiene due grandi informazioni

{
data: [...]
links: {...}
}

nell’attributo ‘data’ viene memorizzato l’array degli Articoli secondo un formato che andremo ad analizzare meglio nella prossima puntata quando ci occuperemo di visualizzare gli Articoli. L’attributo ‘links’ è invece un oggetto contenente un insieme di link utili tra l’altro per la paginazione. La nostra funzione si occupa per prima cosa di memorizzare l’array ‘data’ ricevuto in risposta all’interno dell’attributo ‘articles’ dello stato. Come seconda operazione la funzione controlla la presenza tra i links dell’attributo “next”. La presenza di tale attributo indica che sono presenti ulteriori Articoli e quindi il conteggio viene presentato aggiungendo la stringa ‘more than’. In caso di errore viene semplicemente impostato l’attributo error a true e viene memorizzato il messaggio ricevuto dal server.
Per far partire l’applicazione basta digitare il comando

yarn start

Automaticamente viene aperta una nuova finestra del browser all'indirizzo http://localhost:3000 che mostra la nostra applicazione React. Se tutto funziona correttamente utilizzando il pulsante ‘Count Articles’ dovreste vedere qualcosa di simile a questo

Provate ad aggiungere nuovi Articoli utilizzando il back-end Drupal e aggiornate la pagina dell'applicazione React per controllare che tutto funzioni correttamente.

Conclusioni

In questo primo articolo abbiamo effettuato il setup del back-end e del front-end. Siamo poi andati a modificare il componente principale della nostra applicazione React al fine di mostrare una prima interazione tra le due parti.
Nella seconda parte cominceremo il vero e proprio sviluppo del sito web andando ad implementare la pagina che mostra l’elenco degli Articoli e la pagina di dettaglio di ogni Articolo.
Il codice della nostra applicazione React è disponibile su github. Per chi volesse partire da un sito Drupal già configurato e con tutti i moduli necessari abbiamo messo a disposizione anche il codice del progetto Composer. Dopo aver clonato il repository è necessario installare Drupal utilizzando il profilo Configuration installer per importare tutti i file di configurazione presenti nella directory config/sync.