Le nuove specifiche HTML 5 hanno portato con loro notevoli migliorie non solo dal punto di vista del markup semantico o della possibilità di personalizzare l'aspetto grafico della pagina. Il WHATWG, l'ente che si sta occupando della definizione appunto di questi standard, ha previsto anche notevoli migliorie e implementazioni dal punto di vista di JavaScript.
I browser moderni, infatti, dovranno supportare una serie di API con diversi scopi che faciliteranno il compito degli sviluppatori e permetteranno di avere applicazioni web ancora più performanti e vicine come usabilità a quelle desktop. Una delle novità più interessanti da questo punto di vista sono i WebWorkers. Grazie ad essi è possibile migliorare le performance e il consumo di risorse di una web application. Ciascun WebWorker rappresenta un entità virtuale predisposta a eseguire una porzione di codice in maniera autonoma dal resto dell'applicazione permettendo una miglior separazione delle attività tra i vari componenti.
Compatibilità
Ecco l'elenco dei principali browser e dei relativi rendering engine con la specifica relativa all'implementazione dello standard WebWorker:
- Internet Explorer/Trident: non supporta i WebWorkers
- Firefox/Gecko: supporta i WebWorkers a partire dalla versione 3.6
- Safari/WebKit: supporta i WebWorkers a partire dalla versione 4.0
- Chrome/WebKit: supporta i WebWorkers a partire dalla versione 3.0
- Opera/Presto: supporta i WebWorkers a partire dalla versione 10.6
WebWorkers all'opera
Prima di analizzare il codice necessario per realizzare ed avviare un WebWorker è necessario introdurre alcuni aspetti teorici su queste nuove API.
Innanzitutto ciascun WebWorker viene identificato da un particolare file che implementa le sue funzionalità. Questi sotto-script verranno eseguiti in una sorta di scope particolare per i WebWorker all'interno del quale non sarà possibile accedere agli elementi della pagina proprio per il fatto che essi devono agire indipendentemente dalla pagina e sono predisposti per eseguire operazioni complesse e tediose senza però bloccare l'interfaccia utente come potrebbe accadere in architetture che non sfruttano questa potenzialità.
Comunicazione unidirezionale
I WebWorkers possono comunicare con l'ambiente esterno (che può essere la pagina HTML o altri WebWorkers) tramite una struttura orientata agli eventi e in particolare grazie al metodo postMessage
che permette appunto di inviare messaggi. Questi messaggi dovranno essere gestiti asincronamente grazie alla proprietà onmessage
. Oltre a questa proprietà abbiamo a disposizione anche la proprietà onerror
che verrà utilizzata in caso appunto di errore.
Vediamo ora il primo esempio di worker che si occuperà semplicemente di incrementare di 1 ad un intervallo regolare una variabile esponendone il suo valore ad ogni iterazione. Ovviamente gli script da analizzare saranno 2, la pagina HTML e lo script del WebWorker:
<html>
<head>
<script>
var worker = new Worker("worker.js");
worker.onmessage = function(event) {
document.getElementById("log").innerHTML += event.data+" ";
}
</script>
</head>
<body>
<div id="log"></div>
</body>
</html>
Il codice della pagina è abbastanza banale: viene istanziato un oggetto di tipo Worker
comunicando il nome del file da utilizzare come sotto-script e viene valorizzata la proprietà onmessage
con una funzione che viene invocata automaticamente nel momento in cui il worker avrà qualcosa da comunicare alla pagina (sfruttando la funzione postMessage
).
Ecco il codice del WebWorker:
i = 0;
f = function() {
postMessage(i++);
setTimeout(f, 300);
}
f();
Anche in questo caso niente di complesso. Viene istanziata una variabile globale (i
) che viene incrementata ogni 300 millisecondi grazie a setTimeout
. Il comportamento fondamentale è l'invocazione della funzione postMessage
che permette di comunicare allo script esterno (nel nostro caso la pagina html) il valore del contatore.
Comunicazione bidirezionale
Le API esposte dalle specifiche WHATWG comunque non finiscono qua. Gli stessi membri (postMessage
e onmessage
) possono essere utilizzati in maniera opposta rispetto all'esempio precedente per permettere allo script esterno di passare informazioni al WebWorker.
L'esempio seguente è simile al precedente ed aggiunge la possibilità all'utente di modificare il valore del contatore durante il conteggio:
<html>
<head>
<script>
var worker = new Worker("worker.js");
worker.onmessage = function(event) {
document.getElementById("log").innerHTML += event.data+" ";
};
var skip = function() {
var value = document.getElementById("input").value;
worker.postMessage(value);
}
</script>
</head>
<body>
<input type="text" id="input"/>
<button onclick="skip()">SKIP</button>
<div id="log"></div>
</body>
</html>
Ecco ora il WebWorker:
i = 0;
onmessage = function(event) {
i = event.data;
} f = function() {
postMessage(i++);
setTimeout(f, 300);
}
f();
Notiamo subito che la funzione postMessage
e la proprietà onmessage
in questo esempio vengono utilizzate sia all'interno dello script esterno che all'interno del WebWorker per permettere una comunicazione bidirezionale. Il WebWorker una volta istanziato inizierà il conteggio dei numeri (partendo da 0) ma lo script esterno potrà modificarne il valore di partenza offrendo all'utente un'esperienza più interattiva rispetto all'esempio di prima.
Delegazione
I WebWorker non solo possono eseguire porzioni di codice in un contesto separato rispetto alla pagina originaria ma hanno la possibilità di istanziare e di utilizzare altri WebWorker e comunicare con loro con le stesse API utilizzate nell'esempio di prima. Questa tecnica di delegazione da un lato permette di ottimizzare ancora di più i nostri script sfruttando al meglio i processori multicore ma dall'altro presenta notevoli difficoltà dal punto di vista dello sviluppo e dell'integrazione tra i diversi componenti.
In questo esempio abbiamo creato un'applicazione in grado di calcolare il fattoriale di un numero delegando i calcoli ad un WebWorker che a sua volta sfrutterà dei suoi sottoposti per suddividere il calcolo in diversi sotto problemi. In questo caso i file analizzati saranno 3: oltre allo script presente nella pagina HTML avremo il WebWorker principale (chiamato master) che fungerà da aggregatore e i WebWorker secondari (slave) che si occuperanno direttamente di eseguire i calcoli.
La pagina HTML non necessita di ulteriori spiegazioni:
<html>
<head>
<script>
var worker = new Worker("master.js");
worker.onmessage = function(event) {
alert("Master says " + event.data);
};
</script>
</head>
<body>
</body>
</html>
Ecco ora il codice di master.js che rappresenta un WebWorker che inizializza un insieme di sottoposti (slave) e li inizializza ognuno con una task differente rispetto agli altri. Una volta che essi termineranno il loro calcolo, questo oggetto si occuperà di aggregare i risultati e li comunicherà allo script esterno sfruttando sempre la funzione postMessage
:
var product_to = 10; //numero di partenza
var slave_number = 100; //numero di slave da istanziare
var completed_slave = 0; //numero di slave che hanno completato la loro task
var result = 1; //risultato parziale
var num_per_slave = product_to/slave_number; //numero di slave da istanziare
var on_slave_message = function(event) {
result *= event.data;
completed_slave++; if(completed_slave == slave_number) postMessage(result);
}
for(var i = 0; i<slave_number; i++) {
var slave = new Worker("slave.js");
slave.onmessage = on_slave_message;
var from = i == 0 ? 1 : i*num_per_slave;
var to = (i+1)*num_per_slave;
slave.postMessage({
from: from,
to: to
});
}
Il WebWorker istanzierà un numero di sottoposti e ad ognuno di essi associerà la funzione on_slave_message
come listener. Grazie alla funzione postMessage
a ciascuno di loro verranno comunicati i valori di inizio e di fine per il calcolo del fattoriale. Una volta che avranno risposto tutti i WebWorker sottoposti, il master comunicherà allo script esterno il risultato totale contenuto nella variabile result
.
L'ultimo script analizzato riguarda slave.js ovvero la classe che si occuperà di effettuare direttamente i calcoli (partendo dal valore from
ed arrivando al valore to
) e comunicarli al WebWorker master:
var from, to;
onmessage = function(event) {
from = event.data.from;
to = event.data.to;
start(); //avvio l'algoritmo per il calcolo del fattoriale
}
start = function() {
var sub_result = 1;
for(var i = from; i<to; i++) {
sub_result *= i;
}
postMessage(sub_result);
}
Ulteriori API a disposizione
Le specifiche definiscono alcuni componenti importanti che possono essere richiamati all'interno dello scope di esecuzione di ciascun WebWorker.
Innanzitutto è possibile sfruttare la funzione nativa importScripts
la quale permette, partendo da una o più stringhe rappresentanti gli url, di includere all'interno del WebWorker alcuni script esterni favorendo quindi la modularità del codice. È importante ricordare che gli script inclusi in questo modo nella nostra applicazione esisteranno esclusivamente all'interno del WebWorker che li ha inclusi.
L'oggetto globale navigator
permette di avere a disposizione del Worker informazioni base sul client che sta eseguendo l'applicazione web (userAgent, sistema operativo...) mentre l'oggetto location contiene informazioni sulla pagina aperta. Questi oggetti rappresentano gli oggetti navigator
e location
presenti all'interno di ciascuna pagina web e messi a disposizione dal browser.
L'ultima precisazione riguarda invece l'oggetto event
che viene passato come parametro delle varie funzioni di callback associate agli eventi onmessage
. Questo oggetto, oltre a contenere all'interno della variabile data un riferimento all'oggetto passato come parametro a postMessage
, presenta ulteriori proprietà utili come ad esempio il riferimento al Worker che ha scatenato l'evento (event.worker
).
Per approfondire queste ultime tematiche comunque di raro utilizzo o per osservare dal vivo altri
esempi, il mio consiglio rimane quello di leggere le specifiche ufficiali redatte dal Web Hypertext Application Technology Working Group (WHATWG) a questo indirizzo: http://www.whatwg.org/specs/web-workers/current-work.
Alla prossima.