Web Workers API

28 marzo 2011

I Web Workers nascono per consentire l’esecuzione di porzioni di codice Javascript in modo asincrono, senza intaccare le performance della pagina web in visualizzazione. I Web Workers, nient’altro che file Javascript, possono essere comparati a dei thread che la pagina web può lanciare e con i quali può dialogare attraverso semplici metodi. Ogni WebWorker può eseguire a sua volta altri WebWorkers ed ognuno di essi può effettuare operazioni di I/O, calcoli complessi e quant’altro.

Le API in questione prevedono che un WebWorker possa essere generato come oggetto della classe Worker o SharedWorker: nel primo caso la sua esecuzione sarà limitata alla specifica sessione di navigazione all’interno della finestra (o tab) del browser che l’ha invocato; nel secondo invece ogni sessione di navigazione che condivide la stessa origine (lo stesso dominio) potrà connettersi e scambiare messaggi con il medesimo worker. In questo modo lo SharedWorker assume il ruolo di coordinatore, ottimo per, ad esempio, propagare su tutte le finestre del browser puntate su di un particolare dominio un messaggio ricevuto dal server.

Le specifiche

Per creare un Worker l’istruzione da eseguire è decisamente semplice:

// pagina principale 
bot_noioso = new Worker('bot_noioso.js');

Lo script invocato, ‘bot_noioso.js’, verrà eseguito in asincrono e potrà inviare messaggi verso la pagina principale attraverso la funzione postMessage:

// bot_noioso.js 
postMessage('Perch´ la terra è tonda?');

Per ricevere questi messaggi è necessario registrare una funzione all’handler ‘onmessage’ del worker contenuto nella variabile bot_noioso: chiaramente utilizzando la stessa variabile è possibile inviare, sempre tramite postMessage messaggi al worker.

// pagina principale 
bot_noioso = new Worker('bot_noioso.js'); 
bot_noioso.onmessage = function(event){
  bot_noioso.postMessage(prompt(event.data));
}

Allo stesso modo è possibile registrare l’handler ‘onmessage’ anche all’interno del file del worker:

// bot_noioso.js 
postMessage('Perch´ la terra è tonda?'); 
onmessage = function(event){
  if(event.data != null){ 
    postMessage('Perch´:' + event.data + ' ?');
  }
}

Ora, se lanciamo Chromium potremo sincerarci del buon funzionamento dello script (figura 1):

Figura 1 – Testo

screenshot

Le API dello SharedWorker differiscono in modo sensibile rispetto a queste, anche se ovviamente mantengono immutato il funzionamento di base; per poterle trattare con la dovuta cura costruiamo un piccolo esempio che le utilizzi.

Ecco la demo.

Un esempio

Continuiamo con il progetto guida; l’obiettivo di questo capitolo è beneficiare delle feature di uno SharedWorker per implementare una dashboard di controllo, che amministri le sessioni di Fiveboard aperte contemporaneamente in un browser. Per raggiungere questo (ambizioso) bersaglio creeremo un file ‘js/hub.js’, presso il quale tutte le finestre attive del progetto dovranno registrarsi, che convoglierà le informazioni da e verso una console ‘dashboard.html’. Ecco uno schema dell’architettura (figura 2):

Figura 2 (click per ingrandire)

screenshot

Le comunicazioni da e verso lo SharedWorker transitano attraverso oggetti chiamati MessagePort, nell’immagine qui sopra sono esplicitate 4 MessagePort. Una MessagePort è utilizzata implicitamente anche dall’oggetto Worker del capitolo precedente e quindi non ci si deve sorprendere nell’apprendere che il meccanismo di funzionamento di uno SharedWorker è esattamente lo stesso, fatto salvo che in questo caso è necessario specificare la porta attraverso la quale si vuole inviare o ricevere il messaggio.

// Invio di un messaggio ad uno SharedWorker 
worker.port.postMessage('...');

// Ricezione di un messaggio da uno SharedWorker 
worker.port.onmessage = function(evento){ ... }

// Dall’interno di uno SharedWorker: invio di un messaggio 
messageport_del_destinatario.postMessage('...');

// Dall’interno di uno SharedWorker: ricezione di un messaggio 
messageport_del_mittente.onmessage = function(event){ ... }

Bene, ecco in breve come dovranno comportarsi i 3 componenti che andremo a definire:

  • Index.html
    • Connettersi allo SharedWorker;
    • Presentarsi come Client e specificare il proprio nome (il titolo del documento);
  • Dashboard.html
    • Connettersi allo SharedWorker;
    • Presentarsi come dashboard e ricevere informazioni su tutti i Client già registrati;
    • Essere notificata ad ogni nuova registrazione di un Client;
    • Mantenere a video un elenco dei Client registrati;
  • Hub.js (SharedWorker)
    • Registrare ogni Client e notificare, se presente, la dashboard;
    • Registrare la dashboard ed inviarle tutti i Client registrati fino a quel momento.

Lo scambio di messaggi tra le varie parti avverrà tramite stringhe di testo nel formato “chiave:valore”, come ad esempio “registra_client:Documento di prova”; questa scelta non è dettata dalle specifiche, che sono abbastanza lasche in tal senso, ma da una semplice convenzione adottata anche da alcuni esempi del W3C.

Bene, possiamo partire; iniziamo dal file ‘application.js’ che dovrà assumere questo aspetto:

salvaIlDato = function(info_da_salvare){ 
  localStorage.setItem("fb_" + titolo_fiveboard,info_da_salvare); 
  alert("Memorizzazione effettuata");
};
recuperaIlDato = function(elemento){ 
  if(confirm("Sostituire il contenuto attuale con l'ultimo pensiero memorizzato?")){
    elemento.value = localStorage.getItem("fb_" + titolo_fiveboard);
  }
};
var titolo_fiveboard = null; 
window.onload = function(){
  var worker = new SharedWorker('js/hub.js'); 
  worker.port.onmessage = function(evento){
    nome_comando	= evento.data.split(":")[0] 
    valore_comando = evento.data.substr(nome_comando.length + 1); 
    console.log("Ricevuto comando: " + nome_comando); 
    switch (nome_comando){
      case 'pronto': 
      titolo_fiveboard = prompt("Seleziona il titolo per questa FiveBoard"); 
      document.title = "FB: " + titolo_fiveboard; 
      worker.port.postMessage("registra_client:" + titolo_fiveboard);
    break;
    }
  }
}

Al caricamento della pagina viene attivata la funzione collegata a window.onload: questa crea il collegamento con lo SharedWorker (se il worker non è già presente verrà caricato in memoria e lanciato) e definisce una funzione di ‘ascolto’ nella quale, per ogni messaggio ricevuto, esegue particolari azioni a seconda della chiave estratta. In questo momento la funzione reagisce alla sola chiave ‘pronto’ chiedendo all’utente un titolo del documento ed inviando il risultato al worker con la chiave ‘registra_client’.

Il titolo del documento viene anche utilizzato nelle funzioni di salvataggio e di recupero del dato, per differenziare fra di loro le variabili memorizzate su LocalStorage in modo da evitare collisioni durante l’apertura contemporanea di più index.html.

Ora definiamo hub.js:

var fiveboards_registrate = new Array(); 
var dashboard = null;

processa_il_messaggio = function(evento){ 
  nome_comando	= evento.data.split(":")[0] 
  valore_comando = evento.data.substr(nome_comando.length + 1); 
  switch (nome_comando){
    case 'registra_client': 
      fiveboards_registrate[valore_comando]=evento.target; 
      if(dashboard != null){
        dashboard.postMessage("nuova_fiveboard:" + valore_comando);
    }
    break; 
    case 'registra_dashboard':
      dashboard = evento.target; 
      for(fiveboard in fiveboards_registrate){
        evento.target.postMessage("nuova_fiveboard:" + fiveboard);
    }
break;
  }
}
onconnect = function(nuova_finestra){ 
  var port = nuova_finestra.ports[0]; 
  port.onmessage = processa_il_messaggio; 
  port.postMessage("pronto");
}

L’handler onconnect viene invocato ogni qualvolta una pagina tenta di aprire una connessione verso il worker; nella funzione si delega al metodo ‘processa_il_messaggio’ la gestione dei futuri scambi tra in worker e la pagina; a quest’ultima è inoltre segnalato il concludersi delle operazioni con un messaggio ‘pronto’.

La funzione processa_il_messaggio interpreta e gestisce le due chiavi registra_client e registra_dashboard: nel primo caso aggiunge la MessagePort di comunicazione con il client ad una collezione fiveboards_registrate e comunica alla dashboard, se presente, la nuova aggiunta tramite un messaggio con chiave nuova_fiveboard. Nel secondo caso registra la MessagePort della dashboard (attraverso evento.target, che corrisponde ad evento.ports[0]) e comunica alla stessa tutti client finora memorizzati attraverso un ciclo sulla collezione fiveboards_registrate e ad un messaggio con la già nota chiave nuova_fiveboard.

Perfetto, ora non ci resta che definire markup e Javascript di ‘dashboard.html’:

<html lang='it'> 
<head>
  <meta charset="utf-8"> 
  <title>Five(Dash)Board: tutto sotto controllo!.</title> 
  <script> 
  var worker = null;

  getInfo = function(title){ 
    // to be defined...
  }

  init = function(){ 
    worker = new SharedWorker('js/hub.js'); 
    worker.port.onmessage = function(evento){
      nome_comando	= evento.data.split(":")[0] 
      valore_comando = evento.data.substr(nome_comando.length + 1); 
      console.log("Ricevuto comando: " + nome_comando); 
      switch (nome_comando){
        case 'pronto': 
          worker.port.postMessage("registra_dashboard")
        break; 
        case 'nuova_fiveboard':
          document.getElementById("elenco_fiveboard").insertAdjacentHTML('beforeend', 
            "<li>" +
            "Titolo: " + valore_comando + " " + 
            "(<a href='javascript:getInfo("" + valore_comando +"");'>" +
            "più informazioni" + "</a>)" +
            "</li>"); 
        break;
        }
    }
  }
  </script> 
</head>
<body onload="init();"> 
  <h1>FiveBoard:</h1> 
  <ol id="elenco_fiveboard"> 
  </ol>
</body> 
</html>

Il listato è largamente autoesplicativo sulla base di quanto già enunciato: in questo caso le chiavi gestite sono ‘pronto’ e ‘nuova_fiveboard’: mentre la prima attiva il meccanismo di registrazione (registra_dashboard) appena visto in hub.js, la seconda si incarica di popolare una lista ordinata ogniqualvolta la pagina riceve una notifica di registrazione di una nuova fiveboard.

Eseguiamo il tutto in Chromium e godiamoci il risultato dei nostri sforzi (figura 3):

Figura 3 (click per ingrandire)

screenshot

È possibile verificare il tutto in questa demo.

Più informazioni per i più ardimentosi

Vi siete chiesti a cosa debba servire il link ‘più informazioni’ e la funzione getInfo ad esso collegata? L’idea è questa: al click sul comando il sistema deve mostrare all’utente il testo inserito ed il testo memorizzato per l’istanza selezionata. Per raggiungere l’obiettivo ecco come interverremo:

  1. La dashboard chiede allo SharedWorker maggiori_informazioni per una data finestra;
  2. Lo SharedWorker mette in diretta comunicazione la finestra in oggetto e la dashboard;
  3. La finestra in oggetto comunica alla dashboard testo inserito e testo memorizzato.

Ecco uno schema delle comunicazioni (figura 4):

Figura 4 (click per ingrandire)

screenshot

Il punto 2 è decisamente il più interessante e si avvale di MessageChannel: trattasi di un semplicissimo oggetto, nato all’interno delle API di comunicazione HTML5, contenente due MessagePort ed istruito per fare in modo che ogni messaggio in ingresso alla porta1 venga recapitato alla porta2 e viceversa.

Tutto quello che hub.js deve fare è quindi creare un MessageChannel e dare i due capi del filo (le due MessagePort) rispettivamente alla dashboard ed al client interessato; per fare questo risulta molto comodo il secondo argomento del metodo postMessage, che consente appunto di specificare un array di porte da allegare ad un messaggio.

Tutto chiaro? No? Vediamo le modifiche apportate al progetto:

// file dashboard.html

  // invio della richiesta allo SharedWorker (punto 1) 
  getInfo = function(title){
    worker.port.postMessage("maggiori_informazioni:" + title);
  }
  
  // una nuova chiave da gestire. 
    case 'attendi_testo':
      evento.ports[0].onmessage = function(e){ 
        alert(e.data);
      } 
  break;

// file hub.js 

  // una nuova chiave da gestire, creazione del canale ed invio delle porte di
  // comunicazione alla dashboard ed al client interessato (punto 2) 
  case 'maggiori_informazioni':
  var channel = new MessageChannel(); 
  dashboard.postMessage("attendi_testo",[channel.port1]); 
  fiveboards_registrate[valore_comando].postMessage("richiedi_testo",
  [channel.port2]); 
  break;

// file application.js

  // una nuova chiave da gestire, invio delle informazioni richieste attraverso la 
  // MessagePort ricevuta dall’evento (non la MessagePort di comunicazione col worker) 
  // (punto 3) 
  case 'richiedi_testo':
    evento.ports[0].postMessage( 
      "testo corrente:"	+ document.forms['form_da_ricordare'].elements
      ['testo_da_ricordare'].value + "n" + 
      "testo memorizzato:" + localStorage.getItem("fb_" + titolo_fiveboard)
    ); 
    break;

Eseguiamo il tutto in Chromium ed ammiriamone il risultato (figura 5):

Figura 5 (click per ingrandire)

screenshot

Conclusioni

I Web Worker sono il classico esempio di API semplici solo in apparenza; più ci si addentra in utilizzi concreti più si scoprono possibilità e nuove metodologie di sviluppo. È importante ricordare infatti che sono previsti meccanismi di interazione tra i Web Workers, l’ApplicationCache, e i WebSocket, e che ogni WebWorker può invocarne altri.

Sono diretta conseguenza di questa intrinseca flessibilità delle API gli innumerevoli scenari di utilizzo ipotizzabili oltre a quello già mostrato, come ad esempio svolgere calcoli complessi (ogni Worker può girare su di un core differente) quali rendering o image detection in tempo reale, centralizzare le comunicazioni con il server attraverso uno SharedWorker e un WebSocket oppure monitorare processi browser-side.

Tabella del supporto sui browser

API e Web Applications Internet Explorer Firefox Safari Google Chrome Opera
WebWorkers No 3.6+ 4.0+ 2.0+ 10.6+

Tutte le lezioni

1 ... 38 39 40 ... 51

Se vuoi aggiornamenti su Web Workers API inserisci la tua e-mail nel box qui sotto:
Tags:
 
X
Se vuoi aggiornamenti su Web Workers API

inserisci la tua e-mail nel box qui sotto:

Ho letto e acconsento l'informativa sulla privacy

Acconsento al trattamento di cui al punto 3 dell'informativa sulla privacy