Animatsiooniline veebiarendus: Jõudlus #3.2 – Shared Web Workers

Animatsiooniline veebiarendus: Jõudlus #3.2 – Shared Web Workers

Tänases jõudluse osas võtame ette Shared Web Worker‘id.

Need on ülimalt sarnased eelmises, jõudluse #3.1 osas läbi võetud, Dedicated Web Worker‘itega. Nii et ma soovitan sul see läbi lugeda, kui sa juba seda teinud ei ole.

Erinevus Dedicated Web Worker‘itega

Iseenesest on Shared Web Worker‘i (edaspidi lihtsalt Shared Worker‘i) ülesandeks täpselt see, mis Dedicated Worker‘itelgi – viia osa aeglast või suuri resursse nõudvast JavaScriptist teistesse CPU harudesse, et see ei segaks ülejäänud veebilehe toiminguid.

Shared Worker‘id on aga tegelikult natukene paremad või more advanced kui Dedicated Web Worker‘id ja seda ainult ühe omaduse poolest – Shared Worker‘id, nagu nimigi ütleb, on jagatud.

Nimelt on Shared Worker‘id ligipääsetavad mitmetest resurssidest korraga, mis tähendab seda, et see on jagatud mitme resursi vahel.

Resurssideks on, kas brauseri aknad, <iframe> elemendid või ka teised Web Worker‘id ehk Shared Worker‘id võivad olla mitme akna/töötaja peale samad.

Dedicated Web Worker aga seda ei suuda, Dedicated Worker on ainult ligipääsetav sellest resursist, kus ta on defineeritud.

Täpsustus

Võib olla eeltoodud seletus ei ole kõige täpsem või on natukene segadust tekitav.

Nimelt kui looksime sellise olukorra, kus defineerime neli skripti:

  • dedicated-web-worker.jsDedicated Worker‘i loogika
  • main.js – Põhi JSi fail, mis asub tavalises veebi aknas/põhiaknas, ja antud failis ka defineerime (globaalselt) eeloleva Dedicated Worker‘i (const webWorker = new SharedWorker("shared-web-worker.js");)
  • helper.js – Abi JSi fail, mis asub <iframe> elemendis ja antud <iframe> emaelemendiks on põhiaken

ja siis paneme helper.js faili parent.webWorker.onmessage.

See võib olla tundub nagu tegemist oleks Web Worker‘i jagamisega ehk Shared Worker‘iga, kuna me ju pääseme Web Worker‘ile ligi teisest resursist, kuid tegelikult see nii ei ole.

Esiteks, me defineerisime Dedicated Web Worker‘i.

Teiseks, kood parent.webWorker.onmessage ei tee midagi muud, kui lihtsalt muudab põhiaknas oleva onmessage võrduma <iframe> elemendis oleva funktsiooniga.

Loomulikult oleks siin ka võimalus kasutada addEventListener(), kuid midagi erilist see ei muudaks.

Mida me aga võime siit järeldada on see, et väide, et “Dedicated Worker on ainult ligipääsetav sellest resursist, kus ta on defineeritud” on teatud mõttes vale, kuna antud juhul on Dedicated Worker defineeritud failis main.js ja me pääseme sellele skriptiga helper.js ligi.

Miks ma ütlen, et teatud mõttes vale? Sellepärast, et ligipääsetavust ei mõelda siinkohal otsese ligipääsetuvuse all.

See tähendab, et jah, ma võin pääseda Worker‘i muutujale ligi, nagu seda meie näide illustreerib, ja sellega igasuguseid imetrikke teha, kuid Dedicated Worker‘it ei saa mitte kunagi mitu resurssi (nt mitu akent) korraga ühel ja samal ajal kasutada ehk Dedicated Worker ei ole mitte kunagi mitme resursi peale sama (alati peab looma uue Worker instance‘i ning olemasolevat ei saa kasutada). Shared Worker aga saab olla mitme resursi peale sama ehk saab kasutada juba olemasolevat Worker‘it ja uut instance‘it looma ei pea.

Üleval olev näide Dedicated Worker’i jagamisest on tegelikult näide muutuja jagamisest või õigemini näide, kuidas võib <iframe> elemendiga emaakna globaalsete muutujatele ligi pääseda.

Kui oled tuttav sellise mõistega nagu static variable, siis see on sarnane Shared Web Worker‘i põhimõttega. See on kõikidel klassidel ühine ehk võib ka öelda, et jagatud.

Kui kellegil jäi asi veel segaseks, siis liigu praegu edasi – näited kindlasti aitavad sul paremini asjadest aru saada.

Pean ise ka tunnistama, et kui antud teemat õppisin, siis ma ei mõistnud kohe, miks Shared Worker’eid üldse vaja on – enamus asju suudab ära teha Dedicated Worker.

Tehniline pool

Tehnilise poole pealt on Shared Worker‘id ülimalt sarnased Dedicated Worker‘itega, kuid mõningaid erinevusi siiski leidub.

Kõige suuremaks erinevuseks on kindlasti see, et Shared Worker‘id kasutavad suhtlemiseks omadust nimega port.

Nimelt kuna Shared Worker on jagatud mitme resursi vahel, siis on tal olemas erinevad pordid/ühendused ja Shared Worker suudab nendest järge pidada.

Ehk kui tahame põhi JavaScripti failist saata sõnumi Shared Worker‘ile, siis peame tegema järgmist:

sharedWorker.port.postMessage("Tere");

Ja kui tahame Shared Worker‘ilt ise sõnumeid saada, siis peame seda jällegi tegema läbi omaduse port:

sharedWorker.port.onmessage = function(event){
    //Kood
};

Samaks aga jääb siiski error‘ite saamine Worker‘ilt:

sharedWorker.onerror = function(event){
    //Kood
}

 

Natukene keerukamaks võib olla läheb Worker ise.

Esiteks, peame ära kasutama sellist event‘i nagu onconnect, mis on Worker‘i stardipunktiks.

Nimelt onconnect funktsiooni event objektist saame ligi port objektile, mis aitab meil põhiskriptiga suhelda – suhtlemiseks peame alati kasutama port objekti.

Reaalselt näeb asi välja midagi sellist:

onconnect = function(event){
    const port = event.ports[0];

<pre><code>port.onmessage = function(event){
    port.postMessage(event.data + " Maailm!");
};
</code></pre>

};

Nagu näha võid, siis saame port objekti event.ports massiivist, mis sisaldab endas põhi JS failist saadetud MessagePort interface‘e.

Võib olla osad arvavad, et massiiv event.ports näitab, kui mitu ühendust meil on, kuid tegelikult see nii ei ole.

Kui natukene porte debug‘ida, siis näeme, et enamustel kordadel on event.ports massiivil ainult üks liige.

event.ports massiivis saab olla rohkem kui üks liige siis, kui me saadame põhifailist porte/ühendusi juurde, kuid seda tuleb harva ette.

See, et üldse selline massiiv on olemas, on tegelikult tingitud MessagePort interface‘st

Ühenduste üles loendamiseks aga, tuleb luua selleks eraldi muutuja (allpool on ka sellest näiteid).

 

Kui kasutame onmessage asemel addEventListener‘it, siis peame me manuaalselt pordi avama.

onmessage avab pordi automaatselt ja me ei pea midagi eraldi ise tegema.

See ei ole tegelikult keeruline.

Põhi JS failis peame me lihtsalt kutsuma välja worker.port.start() meetodi.

Ja kui me tahame Worker‘ist sõnumeid saata põhifaili, siis peame seda tegema ka seal – port.start().

Meeles aga tuleb pidada, et port peab olema avatud enne sõnumite saatmist ehk enne postMessage‘it.

Näide:

index.html

<!DOCTYPE html>
<html>
<head>
    <title>Shared workers with addEventListener</title>
</head>
<body>
    <script type="text/javascript" src="main.js"></script>
</body>
</html>

main.js (põhifail)

if(window.SharedWorker){
    const sharedWorker = new SharedWorker("shared-worker.js");

<pre><code>sharedWorker.port.addEventListener("message", (e) => {
    console.log(e.data);
});

sharedWorker.port.start();
sharedWorker.port.postMessage("Tere");
</code></pre>

}

shared-worker.js

onconnect = (e) => {
    const port = e.ports[0];

<pre><code>port.addEventListener("message", (e) => {
    port.postMessage(e.data + " Maailm!");
});

port.start();
</code></pre>

};

Live näite võid leida siit. Vaata veebi konsooli!

onconnect‘i asemel võime ka kasutada addEventListener‘i, kuid midagi see ei muudaks.

 

Eelviimane väikene erinevus Shared Worker‘ite ja Dedicated Worker‘ite vahel on error handling.

Nagu juba tead, siis tavaliste vigade puhul erinevus puudub – süntaks ja muid JSi error‘eid saab ikka kätte läbi worker.onerror‘i, kuid erinev on onmessageerror.

Nimelt kui Dedicated Worker‘il sai sõnumi vea kätte läbi worker.onmessageerror‘i, siis nüüd peab kasutaama worker.port.onmessageerror‘it, kuna suhtlus ju käib läbi objekti port.

 

Viimaseks erinevuseks jääb Worker‘i töö lõpetamine, kuigi Shared Worker‘it lõpetatakse manuaalselt väga harva.

Shared Worker automaatselt eemaldatakse alles siis, kui kõik resursid (aknad) on kinni pandud, kuid kui peaks juhtuma, et sa pead manuaalselt Shared Worker‘i lõpetama, siis seda saab teha kahte moodi, kuid erinevalt Dedicated Worker‘itest puudub Shared Worker‘il terminate().

Üheks võimaluseks on kutsuda Shared Worker‘is endas välja lihtsalt close(), mis lõpetab ja kustutab Shared Worker‘i igast resursist.

Antud viis on nii-öelda väga julm, kuna kõik ühendused ja Worker ise eemaldatakse.

Kui aga kutsuda põhi JS failist välja worker.port.close() või ka Worker‘is endas port.close(), siis lõpetatakse Worker‘iga suhtlus (ei saa enam sõnumeid saata), kuid Worker‘it enne ei eemaldata, kui kõik resursid on kinni pandud.

 

Rohkem erinevusi Dedicated Worker‘ite ja Shared Worker‘ite vahel ei olegi.

Kehtima jäävad ka juba Dedicated Worker‘ite postituses välja toodud tõed:

  • mõistlik on kontrollida, kas brauser toetab SharedWorker interface‘i – if(window.SharedWorker);
  • Shared Worker luuakse läbi konstruktori new SharedWorker("shared-worker-file.js);
  • väliseid skripte saab lisada läbi funktsiooni importScripts();
  • tavalisi error‘eid saadetakse läbi worker.onerror;
  • jne

Näited

Alustame lihtsast näitest ja siis liigume juba keerulisema juurde.

Lihtsam näide – portide loendus

index.html

<!DOCTYPE html>
<html>
<head>
    <title>Shared Web Workers</title>
    <meta charset="utf-8">
</head>
<body>
    <h2>Output</h2>
    <div id="output" style="width: 200px; height: 100px; border: 2px solid black;"></div>
    <script src="main.js"></script>
    <h3>Teiste portide sõnumite nägemiseks vajuta <a href="" target="_blank">siia</a> või kopeeri antud veebilehe URL ja ava see uues aknas/vahelehel</h3>
    <h4>Antud veebileht ei pruugi kõige paremini toimida Firefoxis (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1438945">BUG #1438945</a>). Kasuta Chrome'i.</h4>
</body>
</html>

main.js

if(window.SharedWorker){
    const output = document.getElementById("output");
    //Loome uue Shared Worker'i
    const sharedWorker = new SharedWorker("shared-worker.js");

<pre><code>//Saadame Worker'ile sõnumi; liigu siit edasi shared-worker.js'i
sharedWorker.port.postMessage("Tere Maailm!");

//Event handler Worker'ist saadetud sõnumite jaoks 
sharedWorker.port.onmessage = (e) => {
    //Väljastame sõnumi output kasti
    output.innerHTML += e.data;
};
</code></pre>

}

shared-worker.js

//Muutuja, mis peab järge, mitu ühendust meil on olnud
let connections = 0;

onconnect = (e) => {
    //Omistame pordi, millega saame põhifailiga suhelda 
    const port = e.ports[0];

<pre><code>//Omistame ID, mis sõltub sellest, mitu ühendust meil on olnud
const id = connections;

connections++;

//Event handler põhifailist saadetud sõnumite jaoks 
port.onmessage = (e) => {
    //Saadame põhifaili sõnumi, mis koosneb põhifailist saadetud sõnumist ja kes (mitmes port/ühendus) selle sõnumi saatis.
    //Liigu siit edasi põhifaili - sharedWorker.port.onmessage event handler'i juurde
    port.postMessage("<p>" + e.data + " - port #" + id + "</p>");
};
</code></pre>

};

Live näite leiad siit.

Väikene side-note. Võib olla märkasid, et olin kirjutanud, et antud näide ei pruugi firefoxis kõige paremini toimida, siis on see sellepärast, Firefoxil oli bug seotud Shared Worker’itega. Kõige uuemas (65.0) versioonis peaks see olema parandatud, kuid mõistlik oleks siiski antud ja ka järgmist näidet vaadata Chrome brauserites. Räägin allpool ka Shared Worker’ite teotusest üldisemalt.

Keerulisem näide – lihtne message äpp erinevate akende vahel

index.html

<!DOCTYPE html>
<html>
<head>
    <title>Start Messaging</title>
    <meta charset="utf-8">
</head>
<body>
    <!--Nupp, millega saab avada uue message akna-->
    <h2>Selleks, et "erinevate kasutajatega" suhelda ava vähemalt kaks akent/vahelehte. Selleks võid kasutada alumist nuppu</h2>
    <h3>Soovitavalt võiksid kasutada Chrome'i</h3>
    <button type="button" onclick="openNewMessageWindow()">Open new message window</button>
    <script>
        const openNewMessageWindow = (e) => {
            window.open("message-window.html");
        };
    </script>
</body>
</html>

message-window.html ehk meie user interface

<!DOCTYPE html>
<html>
<head>
    <title>Message window</title>
    <meta charset="utf-8">
</head>
<body>
    <div id="message-log" style="width: 1000px; height: 500px; border: 2px solid black; overflow-y: auto;"></div>
    <b>User #<span id="id"></span></b>: <input id="message-box" type="text" name="message" />
    <button id="send-button" type="button">Send</button>
    <script type="text/javascript" src="main.js"></script>
</body>
</html>

main.js

//Antud pordile/kasutajale omane id
let id = null;
if(window.SharedWorker){
    //Loome Shared Worker'i
    const worker = new SharedWorker("message-worker.js", "Message Worker");

<pre><code>const messageBox = document.getElementById("message-box");
const sendButton = document.getElementById("send-button");
const messageLog = document.getElementById("message-log");

//Saadame koheselt Shared Worker'ile sõnumi, et saada antud pordile/kasutajale id
worker.port.postMessage("getId");

//"Send" nupu vajutamisel saadame Shared Worker'ile objekti, mis sisaldab endas kasutaja poolt sisestaud sõnumit
sendButton.onclick = (e) => {
    worker.port.postMessage({newMessage: messageBox.value});
    messageBox.value = "";
};

//Error handling
worker.onerror = (e) => {
    messageLog.innerHTML += "<p>Midagi läks valesti! Messenger on praegu maas.</p>";
};

//Event handler Worker'ist saadetud sõnumite jaoks 
worker.port.onmessage = (e) => {
    if(id === null){ //Kui pordil/kasutajal ei ole id, siis omistame selle
        id = e.data.id;
        document.title = "Message window #" + id;
        document.getElementById("id").innerHTML = id;
    }else if(e.data.messages !== ""){ //Kui saame uue sõnumi, siis lisame selle "message kasti"
        messageLog.innerHTML += e.data.message;
    }
};
</code></pre>

}

message-worker.js

//Muutuja, mis peab järge, mitu ühendust meil on olnud
let connections = 0;

//Massiiv kasutajatest, kellele tuleb uusi sõnumeid saata
let messangers = [];

onconnect = (e) => {
    //Omistame pordi, millega saame põhifailiga suhelda
    const port = e.ports[0];

<pre><code>//Omistame ID, mis sõltub sellest, mitu ühendust meil on olnud
const id = connections;

//Lisame uue kasutaja kastuajate massiivi
messangers.push(port);

connections++;

//Event handler põhifailist saadetud sõnumite jaoks
port.onmessage = (e) => {
    if(e.data === "getId"){ //Kui data väärtuseks on "getId", siis saadame põhifaili tagasi ühendustest sõltuva ID
        port.postMessage({id: id});
    }else if(e.data.newMessage){ //Kui data'l on olemas omadus nimega "newMessage"
        //Loome uue terviksõnumi
        let message = "<p><b>User #" + id + "</b>: " + e.data.newMessage + "</p>";

        //Saadame igale kasutajale/pordile uue sõnumi
        messangers.forEach(messanger => messanger.postMessage({ message: message }));
    }
};
</code></pre>

};

Näide Shared Workeritest

Live näite leiad siit.

Tehtud W3 multiviewer järgi.

Hoiatus Shared Worker‘ite kohta

Shared Web Worker‘id ei ole just kõige paremini toetatud veebi API – õigemini ei toeta seda sellised brauserid nagu Edge ja Safari (loe: toetus on väga halb) ning lisaks sellele on Shared Worker‘itega minevikus olnud ka väikest viisi poleemikat.

Esiteks, 2015. aastal tehti ettepanek (mis küll läbi ei läinud ja kohe maha laideti), et Shared Worker‘id peaks üldse ära eemaldama HTMLi spec‘ist.

Teiseks, eemaldas Safari Shared Web Worker‘id enda toetatud veebikomponentidest põhjendades seda sellega, et Shared Worker‘id panid liiga suured piirangud Safari veebimootorile. Lisaks ei ole nendel seda plaanis uuesti toetama hakata.

Kolmandaks, vaatab meile otsa see fakt, et isegi tänapäevastes brauserites (nagu näiteks Firefox 64.0, mida ma praegu kasutan) on Shared Web Worker‘id vigased.

Kõige hämmastavam on selle juures see tõsiasi, et antud tehnoloogia on juba vähemalt seitse aastat vana.

Loomulikult bug‘e võib igal hetkel juhtuda (nt mingi uuenduse käigus tekib mingi uus viga jne), nii et antud argument just kõige kuulikindlam ei ole, aga siiski on märkimisväärt.

Neljandaks, mis on rohkem minu isiklik arvamus, on see, et Shared Web Worker‘itele on väga vähe kasutusalasid.

Nimelt suudab (teatud mööndustega) Dedicated Web Worker seda sama, mida tegelikult suudab Shared Worker.

Jah, tõesti, ükski Dedicated Worker ei suuda olla jagatud mitme resursi vahel, kuid näiteks üleval oleva sõnumi äpi saab valmis teha ka ilma Shared Worker‘itega (kasutades näiteks selleks BroadcastChannel API’d).

Praegu tundub, et Shared Worker‘id on rohkem selline mugavustehnoloogia, mis vajab rohkem standardiseerimist, ning selle tõttu soovitan ma sul korralikult järele mõelda, kas Shared Worker‘eid on mõtekas kasutada ja kui on, siis kuidas toetada mitte-toetatud brausereid.

 

Sellise kurva noodiga ma Shared Worker‘id ka kokku tõmban.

Kui sulle jäi midagi arusaamatuks, siis ära karda kommenteerida ning ma annan endast parima, et arusaamatus lahendada!


Senikaua ole tugev ja kohtume juba järgmistes postitustes,

Tähelepanu eest tänades – Oliver Paljak

P.S! Kui oled huvitatud animatsioonilisest veebilehest, siis võta minuga julgelt ühendust AnsiVeebi kodulehel.
Ma ei pea ennast praegu animatsioonilise veebiarenduse eksperdiks, kuid olen sellele spetsialiseerunud ja saan Sind väga palju aidata animatsioonilise veebilehe loomisel!

Leave a Reply

Your email address will not be published. Required fields are marked *