Elaborazione in background con Node.js


Molte app devono eseguire l'elaborazione in background al di fuori del contesto di una richiesta web. Questo tutorial crea un'app web che consente agli utenti di inserire testo per la traduzione e quindi mostra un elenco di traduzioni precedenti. La traduzione viene eseguita in background per evitare di bloccare la richiesta dell'utente.

Il seguente diagramma illustra la procedura di richiesta di traduzione.

Diagramma dell'architettura.

Ecco la sequenza degli eventi per il funzionamento dell'app:

  1. Visita la pagina web per visualizzare un elenco delle traduzioni precedenti, archiviate in Firestore.
  2. Richiedi una traduzione del testo inserendo un modulo HTML.
  3. La richiesta di traduzione è pubblicata in Pub/Sub.
  4. Viene attivata una funzione Cloud Functions collegata a quell'argomento Pub/Sub.
  5. La funzione Cloud Functions utilizza Cloud Translation per tradurre il testo.
  6. La funzione Cloud Functions archivia il risultato in Firestore.

Questo tutorial è destinato a chiunque sia interessato a conoscere l'elaborazione in background con Google Cloud. Non è richiesta alcuna esperienza precedente con Pub/Sub, Firestore, App Engine o Cloud Functions. Tuttavia, per comprendere tutto il codice, è utile avere un po' di esperienza con Node.js, JavaScript e HTML.

Obiettivi

  • Capire ed eseguire il deployment di una funzione Cloud Functions.
  • Capire ed eseguire il deployment di un'app App Engine.
  • Prova l'app.

Costi

In questo documento utilizzi i seguenti componenti fatturabili di Google Cloud:

Per generare una stima dei costi in base all'utilizzo previsto, utilizza il Calcolatore prezzi. I nuovi utenti di Google Cloud potrebbero essere idonei per una prova gratuita.

Al termine delle attività descritte in questo documento, puoi evitare la fatturazione continua eliminando le risorse che hai creato. Per ulteriori informazioni, consulta la sezione Pulizia.

Prima di iniziare

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  3. Make sure that billing is enabled for your Google Cloud project.

  4. Enable the Firestore, Cloud Functions, Pub/Sub, and Cloud Translation APIs.

    Enable the APIs

  5. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  6. Make sure that billing is enabled for your Google Cloud project.

  7. Enable the Firestore, Cloud Functions, Pub/Sub, and Cloud Translation APIs.

    Enable the APIs

  8. Nella console Google Cloud, apri l'app in Cloud Shell.

    Vai a Cloud Shell

    Cloud Shell fornisce l'accesso tramite riga di comando alle tue risorse cloud direttamente dal browser. Apri Cloud Shell nel browser e fai clic su Continua per scaricare il codice di esempio e passare alla directory dell'app.

  9. In Cloud Shell, configura lo strumento gcloud in modo da utilizzare il tuo progetto Google Cloud:
    # Configure gcloud for your project
    gcloud config set project YOUR_PROJECT_ID

Informazioni sulla funzione Cloud Functions

  • La funzione inizia importando diverse dipendenze come Firestore e Translation. I client Firestore e Translation globali sono inizializzati, in modo da poter essere riutilizzati tra le chiamate delle funzioni. In questo modo, non dovrai inizializzare nuovi client per ogni chiamata a una funzione, il che rallenterebbe l'esecuzione.

    const Firestore = require('@google-cloud/firestore');
    const {Translate} = require('@google-cloud/translate').v2;
    
    const firestore = new Firestore();
    const translate = new Translate();
  • L'API Translation traduce la stringa nella lingua selezionata.

    const [
      translated,
      {
        data: {translations},
      },
    ] = await translate.translate(original, language);
    const originalLanguage = translations[0].detectedSourceLanguage;
    console.log(
      `Translated ${original} in ${originalLanguage} to ${translated} in ${language}.`
    );
  • La funzione Cloud Functions analizza il messaggio Pub/Sub per ottenere il testo da tradurre e la lingua di destinazione desiderata.

    L'app richiede una traduzione e memorizza il risultato in Firestore.

    // translate translates the given message and stores the result in Firestore.
    // Triggered by Pub/Sub message.
    exports.translate = async (pubSubEvent) => {
      const {language, original} = JSON.parse(
        Buffer.from(pubSubEvent.data, 'base64').toString()
      );
    
      const [
        translated,
        {
          data: {translations},
        },
      ] = await translate.translate(original, language);
      const originalLanguage = translations[0].detectedSourceLanguage;
      console.log(
        `Translated ${original} in ${originalLanguage} to ${translated} in ${language}.`
      );
    
      // Store translation in firestore.
      await firestore.collection('translations').doc().set({
        language,
        original,
        translated,
        originalLanguage,
      });
    };

Deployment della funzione Cloud Functions

  • Nella stessa directory del file translate.js, esegui il deployment della funzione Cloud Functions con un trigger Pub/Sub:

    gcloud functions deploy translate --runtime nodejs10 --trigger-topic translate

Comprendere l'app

I componenti principali dell'app web sono due:

  • Un server HTTP Node.js per gestire le richieste web. Il server ha i seguenti due endpoint:
    • /: elenca tutte le traduzioni esistenti e mostra un modulo che gli utenti possono inviare per richiedere nuove traduzioni.
    • /request-translation: gli invii di moduli vengono inviati a questo endpoint, il che pubblica la richiesta a Pub/Sub da tradurre in modo asincrono.
  • Un modello HTML compilato con le traduzioni esistenti del server Node.js.

Il server HTTP

  • Nella directory server, app.js inizia configurando l'app e registrando i gestori HTTP:

    
    // This app is an HTTP app that displays all previous translations
    // (stored in Firestore) and has a form to request new translations. On form
    // submission, the request is sent to Pub/Sub to be processed in the background.
    
    // TOPIC_NAME is the Pub/Sub topic to publish requests to. The Cloud Function to
    // process translation requests should be subscribed to this topic.
    const TOPIC_NAME = 'translate';
    
    const express = require('express');
    const bodyParser = require('body-parser');
    const {PubSub} = require('@google-cloud/pubsub');
    const {Firestore} = require('@google-cloud/firestore');
    
    const app = express();
    const port = process.env.PORT || 8080;
    
    const firestore = new Firestore();
    
    const pubsub = new PubSub();
    const topic = pubsub.topic(TOPIC_NAME);
    
    // Use handlebars.js for templating.
    app.set('views', __dirname);
    app.set('view engine', 'html');
    app.engine('html', require('hbs').__express);
    
    app.use(bodyParser.urlencoded({extended: true}));
    
    app.get('/', index);
    app.post('/request-translation', requestTranslation);
    app.listen(port, () => console.log(`Listening on port ${port}!`));
    
  • Il gestore di indici (/) riceve tutte le traduzioni esistenti da Firestore e compila un modello HTML con l'elenco:

    
    // index lists the current translations.
    async function index(req, res) {
      const translations = [];
      const querySnapshot = await firestore.collection('translations').get();
      querySnapshot.forEach((doc) => {
        console.log(doc.id, ' => ', doc.data());
        translations.push(doc.data());
      });
    
      res.render('index', {translations});
    }
    
  • Le nuove traduzioni vengono richieste inviando un modulo HTML. Il gestore di traduzione delle richieste, registrato presso /request-translation, analizza l'invio del modulo, convalida la richiesta e pubblica un messaggio in Pub/Sub:

    
    // requestTranslation parses the request, validates it, and sends it to Pub/Sub.
    function requestTranslation(req, res) {
      const language = req.body.lang;
      const original = req.body.v;
    
      const acceptableLanguages = ['de', 'en', 'es', 'fr', 'ja', 'sw'];
      if (!acceptableLanguages.includes(language)) {
        throw new Error(`Invalid language ${language}`);
      }
    
      console.log(`Translation requested: ${original} -> ${language}`);
    
      const buffer = Buffer.from(JSON.stringify({language, original}));
      topic.publish(buffer);
      res.sendStatus(200);
    }

Il modello HTML

Il modello HTML è la base della pagina HTML mostrata all'utente in modo che possa visualizzare le traduzioni precedenti e richiederne di nuove. Il modello viene compilato dal server HTTP con l'elenco delle traduzioni esistenti.

  • L'elemento <head> del modello HTML include metadati, fogli di stile e JavaScript per la pagina:
    <html>
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Translations</title>
    
        <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
        <link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.indigo-pink.min.css">
        <script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
        <script>
            $(document).ready(function() {
                $("#translate-form").submit(function(e) {
                    e.preventDefault();
                    // Get value, make sure it's not empty.
                    if ($("#v").val() == "") {
                        return;
                    }
                    $.ajax({
                        type: "POST",
                        url: "/request-translation",
                        data: $(this).serialize(),
                        success: function(data) {
                            // Show snackbar.
                            console.log(data);
                            var notification = document.querySelector('.mdl-js-snackbar');
                            $("#snackbar").removeClass("mdl-color--red-100");
                            $("#snackbar").addClass("mdl-color--green-100");
                            notification.MaterialSnackbar.showSnackbar({
                                message: 'Translation requested'
                            });
                        },
                        error: function(data) {
                            // Show snackbar.
                            console.log("Error requesting translation");
                            var notification = document.querySelector('.mdl-js-snackbar');
                            $("#snackbar").removeClass("mdl-color--green-100");
                            $("#snackbar").addClass("mdl-color--red-100");
                            notification.MaterialSnackbar.showSnackbar({
                                message: 'Translation request failed'
                            });
                        }
                    });
                });
            });
        </script>
        <style>
            .lang {
                width: 50px;
            }
            .translate-form {
                display: inline;
            }
        </style>
    </head>

    Nella pagina sono presenti gli asset CSS e JavaScript Material Design Lite (MDL). MDL ti consente di aggiungere un'aspetto di Material Design ai tuoi siti web.

    La pagina utilizza JQuery per attendere che il documento termini il caricamento e impostare un gestore per l'invio del modulo. Ogni volta che viene inviato il modulo di richiesta di traduzione, la pagina esegue una convalida minima del modulo per verificare che il valore non sia vuoto, quindi invia una richiesta asincrona all'endpoint /request-translation.

    Infine, viene visualizzato uno snackbar MDL per indicare se la richiesta è andata a buon fine o se ha riscontrato un errore.

  • Il corpo HTML della pagina utilizza un layout MDL e diversi componenti MDL per visualizzare un elenco di traduzioni e un modulo per richiedere traduzioni aggiuntive:
    <body>
        <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header">
            <header class="mdl-layout__header">
                <div class="mdl-layout__header-row">
                    <!-- Title -->
                    <span class="mdl-layout-title">Translate with Background Processing</span>
                </div>
            </header>
            <main class="mdl-layout__content">
                <div class="page-content">
                    <div class="mdl-grid">
                    <div class="mdl-cell mdl-cell--1-col"></div>
                        <div class="mdl-cell mdl-cell--3-col">
                            <form id="translate-form" class="translate-form">
                                <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
                                    <input class="mdl-textfield__input" type="text" id="v" name="v">
                                    <label class="mdl-textfield__label" for="v">Text to translate...</label>
                                </div>
                                <select class="mdl-textfield__input lang" name="lang">
                                    <option value="de">de</option>
                                    <option value="en">en</option>
                                    <option value="es">es</option>
                                    <option value="fr">fr</option>
                                    <option value="ja">ja</option>
                                    <option value="sw">sw</option>
                                </select>
                                <button class="mdl-button mdl-js-button mdl-button--raised mdl-button--accent" type="submit"
                                    name="submit">Submit</button>
                            </form>
                        </div>
                        <div class="mdl-cell mdl-cell--8-col">
                            <table class="mdl-data-table mdl-js-data-table mdl-shadow--2dp">
                                <thead>
                                    <tr>
                                        <th class="mdl-data-table__cell--non-numeric"><strong>Original</strong></th>
                                        <th class="mdl-data-table__cell--non-numeric"><strong>Translation</strong></th>
                                    </tr>
                                </thead>
                                <tbody>
                                    {{#each translations}}
                                    <tr>
                                        <td class="mdl-data-table__cell--non-numeric">
                                            <span class="mdl-chip mdl-color--primary">
                                                <span class="mdl-chip__text mdl-color-text--white">{{ originalLanguage }} </span>
                                            </span>
                                        {{ original }}
                                        </td>
                                        <td class="mdl-data-table__cell--non-numeric">
                                            <span class="mdl-chip mdl-color--accent">
                                                <span class="mdl-chip__text mdl-color-text--white">{{ language }} </span>
                                            </span>
                                            {{ translated }}
                                        </td>
                                    </tr>
                                    {{/each}}
                                </tbody>
                            </table>
                            <br/>
                            <button class="mdl-button mdl-js-button mdl-button--raised" type="button" onClick="window.location.reload();">
                                Refresh
                            </button>
                        </div>
                    </div>
                </div>
                <div aria-live="assertive" aria-atomic="true" aria-relevant="text" class="mdl-snackbar mdl-js-snackbar" id="snackbar">
                    <div class="mdl-snackbar__text mdl-color-text--black"></div>
                    <button type="button" class="mdl-snackbar__action"></button>
                </div>
            </main>
        </div>
    </body>
    
    </html>

Eseguire il deployment dell'app web

Puoi utilizzare l'ambiente standard di App Engine per creare ed eseguire il deployment di un'app che viene eseguita in modo affidabile con un carico elevato e con grandi quantità di dati.

Questo tutorial utilizza l'ambiente standard di App Engine per il deployment del frontend HTTP.

app.yaml configura l'app App Engine:

runtime: nodejs10
  • Dalla stessa directory del file app.yaml, esegui il deployment della tua applicazione nell'ambiente standard di App Engine:
    gcloud app deploy

Test dell'app

Dopo aver eseguito il deployment della funzione Cloud Functions e dell'app App Engine, prova a richiedere una traduzione.

  1. Per visualizzare l'app nel browser,inserisci il seguente URL:

    https://PROJECT_ID.REGION_ID.r.appspot.com

    Sostituisci quanto segue:

    Una pagina con l'elenco delle traduzioni vuoto e un modulo per richiedere nuove traduzioni.

  2. Nel campo Testo da tradurre, inserisci del testo da tradurre, ad esempio Hello, World.
  3. Seleziona dall'elenco a discesa una lingua in cui vuoi tradurre il testo.
  4. Fai clic su Invia.
  5. Per aggiornare la pagina, fai clic su Aggiorna . Nell'elenco di traduzioni è presente una nuova riga. Se non vedi una traduzione, attendi qualche secondo e riprova. Se non vedi ancora la traduzione, consulta la prossima sezione sul debug dell'app.

Eseguire il debug dell'app

Se non riesci a connetterti all'app App Engine o non vedi nuove traduzioni, controlla quanto segue:

  1. Verifica che i comandi di deployment di gcloud siano stati completati correttamente e non abbiano restituito errori. Se si sono verificati errori, correggili e riprova a eseguire il deployment della funzione Cloud Functions e dell'app App Engine.
  2. Nella console Google Cloud, vai alla pagina Visualizzatore log.

    Vai alla pagina Visualizzatore log
    1. Nell'elenco a discesa Risorse selezionate di recente, fai clic su Applicazione GAE, quindi fai clic su All_module_All. Viene visualizzato un elenco di richieste da quando hai visitato la tua app. Se non viene visualizzato un elenco di richieste, conferma di aver selezionato All module_id dall'elenco a discesa. Se vengono visualizzati messaggi di errore nella console Google Cloud, verifica che il codice dell'app corrisponda al codice nella sezione Informazioni sull'app.
    2. Nell'elenco a discesa Risorse selezionate di recente, fai clic su Funzione Cloud Functions e quindi su Nome di tutte le funzioni. Viene visualizzata una funzione per ogni traduzione richiesta. In caso contrario, verifica che la funzione Cloud Functions e l'app App Engine utilizzino lo stesso argomento Pub/Sub:

Esegui la pulizia

Per evitare che al tuo Account Google Cloud vengano addebitati costi relativi alle risorse utilizzate in questo tutorial, elimina il progetto che contiene le risorse oppure mantieni il progetto ed elimina le singole risorse.

Elimina il progetto Cloud

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

Elimina l'istanza di App Engine

  1. In the Google Cloud console, go to the Versions page for App Engine.

    Go to Versions

  2. Select the checkbox for the non-default app version that you want to delete.
  3. Per eliminare la versione dell'app, fai clic su Elimina.

Elimina la funzione Cloud Functions

  • Elimina la funzione Cloud Functions creata in questo tutorial:
    gcloud functions delete Translate

Passaggi successivi