Przed nami ciekawe czasy w technologii WWW. Powoli wchodzi HTML5 wprowadzając całkowicie nową jakość. Póki co strony w HTML5 mają charakter bardziej pokazowy niż użyteczny. Ja jako maniak technologii musiałem zacząć się z tym wszystkim bawić. Na pierwszy ogień idzie technologia Web Workers [dalej WW].
WW to próba wprowadzenia wielowątkowości w przeglądarce WWW. W założeniu zadania, które wymagają dużej mocy obliczeniowej wrzucane są do takiego workera. Gdy sobie skończy liczyć, powiadamia przeglądarkę, a ta wyświetla wynik. Chodzi o to, że użytkownik strony niekoniecznie ma ochotę cierpieć zwolnienie pracy przeglądarki w czasie kiedy procesor jest obciążony (ładnie się to nazywa responsywność przeglądarki).
Zresztą to nie jest jakaś super nowość. Z wątkami w tle spotykacie się na co dzień przeglądając Facebooka (powiadomienia kto jest na czacie, nowe posty na tablicy bez przeładowywania itd.) Oficjalne API WW ma spore obostrzenia. Worker to jakby zamknięte środowisko. Nie mamy dostępu do np. obiektów strony macierzystej (DOM, jQuery). Więcej polecam poczytać u Marcoosa.
Dzisiaj zrobimy coś, czego nie widziałem nigdzie w Internecie póki co. Spróbujemy dynamicznie generować nowe wątki, kasować je, a nawet regulować częstotliwość ich odpowiedzi. Wszystko to będzie oczywiście ładnie okraszone biblioteką jQuery. Siedziałem nad tym pół niedzieli, więc uważajcie!
Najpierw rzućcie okiem na efekt finalny, a poniżej wyjaśnienia co ciekawszych rzeczy.
Pierwsze co robimy to sprawdzamy czy przeglądarka użytkownika w ogóle obsługuje WW. Korzystamy z biblioteki Modernizr i wyświetlamy alert informujący.
Druga sprawa to kod samego workera (timeworker.js):
var workerNo;
onmessage = function(e) {
workerNo = e.data.workerNo;
changeInterval(e.data.timer);
}
function changeInterval(interval) {
setInterval("gimmeTime()",interval);
}
function gimmeTime() {
postMessage({
time:new Date().toUTCString(),
workerNo:workerNo
});
}
- Pole workerNo zawiera identyfikator workera — liczbę od 1 w górę.
- onmessage to komunikat odebrany od przeglądarki. Zwracamy uwagę na to, że do odebranych danych dobieramy się poprzez e.data, a nie samo e!
- changeInterval to funkcja ustawiająca samowywołanie określonej funkcji cyklicznie co podany czas w ms.
- gimmeTime to funkcja wysyłające przeglądarce JSON z aktualnym czasem i swoim identyfikatorem za pomocą funkcji postMessage().
Teraz czas na kod workersdemo.html (podaję sam kod JavaScript bo szkoda miejsca).
var workers = new Array();
var licznik = 0;
var bindedWorkers = '';
if (Modernizr.webworkers) {
alert('Twoja przeglądarka wspiera Web Workers, możesz śmiało testować!');
}
else {
alert('Twoja przeglądarka NIE wspiera Web Workers, demo nie będzie działało. Zainstaluj sobie prawdziwą przeglądarkę (FF, Chrome, Opera[?])!');
}
function addNotification(workerno) {
var msg = $('<p>').appendTo('#notif');
msg.text('Worker '+workerno+' updated state');
msg.fadeOut(1500);
}
function fireUpWorker() {
++licznik;
var workerDiv = $('<div>').attr('id','workerdiv-'+licznik).appendTo('#liczniki');
var par = $('<p>').attr('id','worker-'+licznik).appendTo(workerDiv);
$('<input />').attr({
type:'submit',
value:'terminate',
onClick:'terminateWorker('+licznik+');return false;'
}).appendTo(workerDiv);
workers[licznik] = new Worker('timeworker.js');
var timer = $('#intrv').val();
workers[licznik].postMessage({timer:timer,workerNo:licznik});
bindWorkers();
}
function terminateWorker(workerNo) {
workers[workerNo].terminate();
$('#worker-'+workerNo).text('terminated!');
$('#workerdiv-'+workerNo).fadeOut(5000);
}
function bindWorkers() {
bindedWorkers += "workers["+licznik+"].onmessage = function(event) {";
bindedWorkers += "$('#worker-"+licznik+"').text('Worker '+event.data.workerNo+' date : '+event.data.time);";
bindedWorkers += "addNotification(event.data.workerNo);";
bindedWorkers += "};";
eval(bindedWorkers);
}
- Mamy 3 pola globalne: tablicę workers trzymającą wszystkich aktywnych workerów, licznik workerów i bindedWorkers zawierający brzydki kod JavaScript do wykonania poprzez eval() — jedyny znany mi sposób na zmuszenie przeglądarki żeby słuchała wszystkich aktywnych workerów, a nie tylko ostatniego dodanego.
- addNotification wyrzuca użytkownikowi komunikat o odebraniu wiadomości od workera, znika po 1,5 sek.
- fireUpWorker (nie chciało mi się wysilać na lepszą nazwę
) to główna funkcja. Najpierw tworzy nowe elementy HTML: div, w nim paragraf na komunikat z workera i przycisk zabijający. Nowo stworzonemu workerowi wysyłam komunikat w JSON co jaki czas ma się odświeżać i jaki ma numer. Na koniec wywołuję funkcję bindWorkers(). - terminateWorker najpierw zabija określonego workera poprzez worker.terminate(), a następnie ukrywa związanego z nim diva.
- Na koniec funkcja, z której jestem najmniej zadowolony: bindWorkers(). Zajmuje się ustawianiem co przeglądarka ma robić w przypadku otrzymania komunikatu od workera. Działa to analogicznie jak poprzednio. Poprzez onmessage i e.data. Do pola bindedWorkers dodajemy kod w postaci ciągu i uruchamiamy evalem.
Tutaj mam prośbę dla znawców. Dodawanie onmessage w pętli dla poszczególnych workerów osobno nie działało. Jak załatwić problem bez używania evala?
P.S.: Znalazłem chyba buga w Chrome: Próby dodania atrybutu onclick poprzez jQuery nic nie dają. Należy używać onClick
