Web İşçileriyle İlgili Temel Bilgiler

Sorun: JavaScript eşzamanlılığı

İlginç uygulamaların (örneğin, çok fazla sunucu kullanan uygulamalardan) istemci taraflı JavaScript'e taşınmasını engelleyen birkaç darboğaz vardır. Bu özelliklerden bazıları tarayıcı uyumluluğu, statik yazma, erişilebilirlik ve performanstır. Tarayıcı satıcıları JavaScript motorlarının hızını hızla artırdığı için neyse ki ikincisi hızla geçmişte kalmıştır.

JavaScript'i engelleyen bir diğer şey aslında dilin kendisi. JavaScript, tek iş parçacıklı bir ortamdır. Diğer bir deyişle, birden çok komut dosyası aynı anda çalışamaz. Örneğin, kullanıcı arayüzü etkinliklerini işlemesi, büyük miktarlardaki API verisini sorgulayıp işlemesi ve DOM'u değiştirmesi gereken bir site düşünün. Çok yaygın, değil mi? Tarayıcıların JavaScript çalışma zamanındaki sınırlamalar nedeniyle bunların tümü eşzamanlı olarak gerçekleştirilemez. Komut dosyası yürütme işlemi, tek bir iş parçacığı içinde gerçekleşir.

Geliştiriciler setTimeout(), setInterval(), XMLHttpRequest gibi teknikler ve etkinlik işleyiciler kullanarak "eşzamanlılığı" taklit ederler. Evet, bu özelliklerin tümü eşzamansız olarak çalışır ancak uygulamanın engellenmemesi, her zaman eşzamanlılık anlamına gelmez. Eşzamansız etkinlikler, mevcut yürütme komut dosyası üretildikten sonra işlenir. Neyse ki HTML5 bize bu saldırılardan daha iyi bir şey sunuyor!

Web İşçileri ile Tanışın: JavaScript'e iş parçacığı getirme

Web Çalışanları spesifikasyonu, web uygulamanızda arka plan komut dosyaları üretmek için bir API tanımlar. Web İşçileri, hesaplama açısından yoğun görevleri yürütmek için uzun süreli komut dosyalarını çalıştırmak gibi işlemler yapmanıza olanak tanır. Ancak bunu yaparken kullanıcı etkileşimlerini yönetmek için kullanıcı arayüzünü veya diğer komut dosyalarını engellemezsiniz. Hepimizin çok sevdiği o berbat "yanıt vermeyen komut dosyası" diyaloğunu sonlandırmaya yardımcı olacaklar.

Yanıt vermeyen komut dosyası iletişim kutusu
Genel yanıt vermeyen komut dosyası iletişim kutusu.

Çalışanlar, paralellik sağlamak için ileti dizisi benzeri ileti iletmeden yararlanır. Bunlar, kullanıcı arayüzünün yenilenmesini, yüksek performansını ve kullanıcı duyarlılığını korumak için mükemmeldir.

Web İşçilerinin Türleri

Spesifikasyonda iki tür Web İşçisi ele alınmıştır: Özel Çalışanlar ve Ortak Çalışanlar. Bu makale yalnızca özel çalışanları kapsamaktadır. Süreç boyunca onlara 'web çalışanları' veya 'çalışanlar' diyeceğim.

Başlarken

Web İşçileri izole bir iş parçacığında çalışır. Sonuç olarak, çalıştırdıkları kodun ayrı bir dosyada bulunması gerekir. Ancak bunu yapmadan önce yapmanız gereken ilk şey ana sayfanızda yeni bir Worker nesnesi oluşturmaktır. Kurucu, çalışan komut dosyasının adını alır:

var worker = new Worker('task.js');

Belirtilen dosya mevcutsa tarayıcı eşzamansız olarak indirilen yeni bir çalışan iş parçacığı oluşturur. Çalışan, dosya tamamen indirilip çalıştırılıncaya kadar başlamaz. Çalışanın yolu 404 hatası döndürürse, çalışan sessiz bir şekilde başarısız olur.

Çalışanı oluşturduktan sonra postMessage() yöntemini çağırarak başlatın:

worker.postMessage(); // Start the worker.

Bir çalışanla mesaj iletme yoluyla iletişim

Bir iş ile üst sayfası arasındaki iletişim, bir etkinlik modeli ve postMessage() yöntemi kullanılarak yapılır. postMessage(), tarayıcınıza/sürümünüze bağlı olarak bir dizeyi veya JSON nesnesini tek bağımsız değişkeni olarak kabul edebilir. Modern tarayıcıların en son sürümleri JSON nesnesinin geçirilmesini destekler.

Aşağıda, doWork.js dosyasında bir çalışana "Hello World"ü aktarmak için dize kullanımına bir örnek verilmiştir. Çalışan, kendisine aktarılan mesajı geri döndürür.

Ana alfabe:

var worker = new Worker('doWork.js');

worker.addEventListener('message', function(e) {
console.log('Worker said: ', e.data);
}, false);

worker.postMessage('Hello World'); // Send data to our worker.

doWork.js (çalışan):

self.addEventListener('message', function(e) {
self.postMessage(e.data);
}, false);

Ana sayfadan postMessage() çağrıldığında, çalışanımız bu mesajı message etkinliği için bir onmessage işleyici tanımlayarak işler. Mesaj yüküne (bu örnekte "Hello World") Event.data üzerinden erişilebilir. Bu örnek çok heyecan verici olmasa da, postMessage() yönteminin verileri ana iş parçacığına geri iletmek için de bir araç olduğunu göstermektedir. Kolay!

Ana sayfa ile çalışanlar arasında iletilen iletiler kopyalanır, paylaşılmaz. Örneğin, sonraki örnekte JSON mesajının "msg" özelliğine her iki konumdan da erişilebilir. Nesnenin ayrı, ayrılmış bir alanda çalışmasına rağmen doğrudan çalışana aktarılmakta olduğu görülüyor. Aslında, nesne çalışana verilirken serileştiriliyor ve ardından, diğer tarafta da seri durumdan çıkarılıyor. Sayfa ve çalışan aynı örneği paylaşmaz. Bu nedenle, her geçişte bir kopya oluşturulur. Çoğu tarayıcı, bu özelliği her iki tarafta da otomatik olarak JSON kodlaması yaparak/değerin kodunu çözerek uygular.

Aşağıda, JSON nesnelerini kullanarak mesaj ileten daha karmaşık bir örnek verilmiştir.

Ana alfabe:

<button onclick="sayHI()">Say HI</button>
<button onclick="unknownCmd()">Send unknown command</button>
<button onclick="stop()">Stop worker</button>
<output id="result"></output>

<script>
function sayHI() {
worker.postMessage({'cmd': 'start', 'msg': 'Hi'});
}

function stop() {
// worker.terminate() from this script would also stop the worker.
worker.postMessage({'cmd': 'stop', 'msg': 'Bye'});
}

function unknownCmd() {
worker.postMessage({'cmd': 'foobard', 'msg': '???'});
}

var worker = new Worker('doWork2.js');

worker.addEventListener('message', function(e) {
document.getElementById('result').textContent = e.data;
}, false);
</script>

doWork2.js:

self.addEventListener('message', function(e) {
var data = e.data;
switch (data.cmd) {
case 'start':
    self.postMessage('WORKER STARTED: ' + data.msg);
    break;
case 'stop':
    self.postMessage('WORKER STOPPED: ' + data.msg +
                    '. (buttons will no longer work)');
    self.close(); // Terminates the worker.
    break;
default:
    self.postMessage('Unknown command: ' + data.msg);
};
}, false);

Aktarılabilir nesneler

Çoğu tarayıcı, File, Blob, ArrayBuffer ve JSON nesneleri gibi daha karmaşık türleri Çalışanlar içine/dışına aktarmanıza olanak tanıyan yapılandırılmış klonlama algoritmasını uygular. Ancak bu tür veriler postMessage() kullanılarak geçirilirken bir kopya oluşturulur. Bu nedenle, örneğin 50 MB'lık büyük bir dosya geçiriyorsanız, dosyayı çalışan ve ana iş parçacığı arasında alırken gözle görülür bir ek yük oluşur.

Yapılandırılmış klonlama harikadır ancak bir kopyanın oluşturulması yüzlerce milisaniye sürebilir. Performans isabetiyle mücadele etmek için Aktarılabilir Nesneler'i kullanabilirsiniz.

Aktarılabilir Nesneler ile veriler bir bağlamdan diğerine aktarılır. Sıfır kopyadan oluşur ve bir Çalışana veri gönderme performansını büyük ölçüde iyileştirir. C/C++ dünyasındaysanız bunu referans olarak düşünebilirsiniz. Ancak, geçişten farklı olarak, görüşme bağlamındaki "sürüm" yeni bağlama aktarıldıktan sonra kullanılamaz. Örneğin, ana uygulamanızdan Çalışan'a bir ArrayBuffer aktarırken orijinal ArrayBuffer temizlenir ve artık kullanılamaz. İçerikleri (sessizce) Çalışan bağlamına aktarılır.

Aktarılabilir nesneleri kullanmak için biraz farklı bir postMessage() imzası kullanın:

worker.postMessage(arrayBuffer, [arrayBuffer]);
window.postMessage(arrayBuffer, targetOrigin, [arrayBuffer]);

Çalışan durumu; ilk bağımsız değişken veri, ikincisi ise aktarılması gereken öğelerin listesidir. Ayrıca, ilk bağımsız değişkenin ArrayBuffer olması gerekmez. Örneğin, bu bir JSON nesnesi olabilir:

worker.postMessage({data: int8View, moreData: anotherBuffer},
                [int8View.buffer, anotherBuffer]);

Önemli nokta, ikinci bağımsız değişkenin ArrayBuffer dizisi olması gerektiğidir. Bu, aktarılabilir öğeleriniz listenizdir.

Aktarılabilirler hakkında daha fazla bilgi için developer.chrome.com adresindeki yayınımıza bakın.

Çalışan ortamı

Çalışan kapsamı

Bir çalışan bağlamında hem self hem de this, çalışan için genel kapsama referans verir. Dolayısıyla, önceki örnek şu şekilde de yazılabilir:

addEventListener('message', function(e) {
var data = e.data;
switch (data.cmd) {
case 'start':
    postMessage('WORKER STARTED: ' + data.msg);
    break;
case 'stop':
...
}, false);

Alternatif olarak, onmessage etkinlik işleyicisini doğrudan ayarlayabilirsiniz (yine de addEventListener, JavaScript ninjaları tarafından her zaman desteklenir).

onmessage = function(e) {
var data = e.data;
...
};

Çalışanların kullanabileceği özellikler

Çok iş parçacıklı çalışma biçimleri nedeniyle, Web İşçileri JavaScript özelliklerinin yalnızca bir alt kümesine erişebilir:

  • navigator nesnesi
  • location nesnesi (salt okunur)
  • XMLHttpRequest
  • setTimeout()/clearTimeout() ve setInterval()/clearInterval()
  • Uygulama Önbelleği
  • importScripts() yöntemini kullanarak harici komut dosyalarını içe aktarma
  • Diğer web çalışanları yumurtlama

Çalışanların şunlara erişimi YOKTUR:

  • DOM (iş parçacığı güvenli değildir)
  • window nesnesi
  • document nesnesi
  • parent nesnesi

Harici komut dosyaları yükleniyor

importScripts() işleviyle, harici komut dosyalarını veya kitaplıkları bir çalışana yükleyebilirsiniz. Bu yöntem, içe aktarılacak kaynakların dosya adlarını temsil eden sıfır veya daha fazla dize alır.

Şu örnek, çalışana script1.js ve script2.js öğelerini yüklüyor:

worker.js:

importScripts('script1.js');
importScripts('script2.js');

Ayrıca, tek bir import ifadesi olarak da yazılabilir:

importScripts('script1.js', 'script2.js');

Alt çalışanlar

Çalışanlar, çocuk işçiler yaratma olanağına sahiptir. Bu, çalışma zamanında büyük görevleri daha da bölebilmek için idealdir. Ancak, alt çalışanların dikkat etmesi gereken noktalar vardır:

  • Alt çalışanlar üst sayfayla aynı kaynakta barındırılmalıdır.
  • Alt çalışanların içindeki URI'lar, ana sayfanın aksine üst çalışanın konumuna göre çözümlenir.

Çoğu tarayıcının her çalışan için ayrı işlemler oluşturduğunu unutmayın. Bir çalışan çiftliği kurmadan önce, kullanıcının sistem kaynaklarını çok fazla boşaltmamaya dikkat edin. Bunun bir nedeni, ana sayfalar ile çalışanlar arasında iletilen mesajların kopyalanması, paylaşılmamasıdır. İleti İletme Özelliğini Kullanarak Bir Çalışanla iletişim kurma konusuna bakın.

Bir alt çalışanın nasıl oluşturulacağıyla ilgili bir örnek için spesifikasyondaki örneğe bakın.

Satır içi çalışanlar

Peki, çalışan komut dosyanızı anında oluşturmak veya ayrı çalışan dosyaları oluşturmak zorunda kalmadan bağımsız bir sayfa oluşturmak isterseniz ne olur? Blob() sayesinde, çalışan kodu için bir dize olarak bir URL işleyici oluşturarak, çalışanınızı ana mantığınızla aynı HTML dosyasında "satır içi" olarak kullanabilirsiniz:

var blob = new Blob([
"onmessage = function(e) { postMessage('msg from worker'); }"]);

// Obtain a blob URL reference to our worker 'file'.
var blobURL = window.URL.createObjectURL(blob);

var worker = new Worker(blobURL);
worker.onmessage = function(e) {
// e.data == 'msg from worker'
};
worker.postMessage(); // Start the worker.

Blob URL'leri

window.URL.createObjectURL() araması da burada. Bu yöntem, bir DOM File veya Blob nesnesinde depolanan verilere referans vermek için kullanılabilecek basit bir URL dizesi oluşturur. Örneğin:

blob:http://localhost/c745ef73-ece9-46da-8f66-ebes574789b1

Blob URL'leri benzersizdir ve uygulamanızın kullanım ömrü boyunca (ör. document kaldırılıncaya kadar) kullanılabilir. Çok sayıda Blob URL'si oluşturuyorsanız artık ihtiyaç duyulmayan referansları yayınlamanız önerilir. Blob URL'lerini window.URL.revokeObjectURL() öğesine ileterek açık bir şekilde serbest bırakabilirsiniz:

window.URL.revokeObjectURL(blobURL);

Chrome'da, oluşturulan tüm blob URL'lerini görüntüleyebileceğiniz bir sayfa vardır: chrome://blob-internals/.

Tam örnek

Bunu bir adım öteye taşıyarak, çalışanın JS kodunun sayfamızın içine nasıl yerleştirileceğini akıllıca görebiliriz. Bu teknik, çalışanı tanımlamak için bir <script> etiketi kullanır:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>

<div id="log"></div>

<script id="worker1" type="javascript/worker">
// This script won't be parsed by JS engines
// because its type is javascript/worker.
self.onmessage = function(e) {
    self.postMessage('msg from worker');
};
// Rest of your worker code goes here.
</script>

<script>
function log(msg) {
    // Use a fragment: browser will only render/reflow once.
    var fragment = document.createDocumentFragment();
    fragment.appendChild(document.createTextNode(msg));
    fragment.appendChild(document.createElement('br'));

    document.querySelector("#log").appendChild(fragment);
}

var blob = new Blob([document.querySelector('#worker1').textContent]);

var worker = new Worker(window.URL.createObjectURL(blob));
worker.onmessage = function(e) {
    log("Received: " + e.data);
}
worker.postMessage(); // Start the worker.
</script>
</body>
</html>

Bana göre, bu yeni yaklaşım biraz daha net ve daha okunaklı. id="worker1" ve type='javascript/worker' ile bir komut dosyası etiketi tanımlar (böylece tarayıcı JS'yi ayrıştırmaz). Bu kod, document.querySelector('#worker1').textContent kullanılarak dize olarak ayıklanır ve dosyanın oluşturulması için Blob() işlevine geçirilir.

Harici komut dosyaları yükleniyor

Çalışan kodunuzu satır içi yapmak için bu teknikleri kullanırken, importScripts() yalnızca mutlak URI sağladığınızda çalışır. Göreli bir URI iletmeye çalışırsanız tarayıcı bir güvenlik hatası şikayet eder. Bunun nedeni şudur: Çalışan (artık bir blob URL'sinden oluşturulmuş) blob: önekiyle çözümlenirken uygulamanız farklı (muhtemelen http://) bir şemayla çalışır. Bu nedenle, başarısızlık, çapraz kaynak kısıtlamalarından kaynaklanır.

Satır içi çalışanda importScripts() kullanmanın bir yolu, ana komut dosyanızın geçerli URL'sini satır içi çalışana geçirerek ve mutlak URL'yi manuel olarak oluşturarak çalıştırıldığı geçerli URL'yi "eklemek"tir. Bu, harici komut dosyasının aynı kaynaktan içe aktarılmasını sağlar. Ana uygulamanızın http://example.com/index.html üzerinde çalıştığı varsayıldığında:

...
<script id="worker2" type="javascript/worker">
self.onmessage = function(e) {
var data = e.data;

if (data.url) {
var url = data.url.href;
var index = url.indexOf('index.html');
if (index != -1) {
    url = url.substring(0, index);
}
importScripts(url + 'engine.js');
}
...
};
</script>
<script>
var worker = new Worker(window.URL.createObjectURL(bb.getBlob()));
worker.postMessage(<b>{url: document.location}</b>);
</script>

işleme hataları

Herhangi bir JavaScript mantığında olduğu gibi, web çalışanlarınızda ortaya çıkan hataları işlemek istersiniz. Bir çalışan yürütme sırasında hata oluşursa bir ErrorEvent tetiklenir. Arayüz, neyin yanlış gittiğini anlamak için yararlı üç özellik içerir: filename (hataya neden olan çalışan komut dosyasının adı, lineno - hatanın meydana geldiği satır numarası) ve message (hatanın anlamlı bir açıklaması). Hatanın özelliklerini yazdırmak için onerror etkinlik işleyiciyi ayarlamayla ilgili bir örnek aşağıda verilmiştir:

<output id="error" style="color: red;"></output>
<output id="result"></output>

<script>
function onError(e) {
document.getElementById('error').textContent = [
    'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message
].join('');
}

function onMsg(e) {
document.getElementById('result').textContent = e.data;
}

var worker = new Worker('workerWithError.js');
worker.addEventListener('message', onMsg, false);
worker.addEventListener('error', onError, false);
worker.postMessage(); // Start worker without a message.
</script>

Örnek: workspaceWithError.js, 1/x işlemini gerçekleştirmeye çalışır. Burada x tanımlanmamıştır.

// YAPILACAKLAR: DevSite - Kod örneği, satır içi etkinlik işleyicileri kullandığı için kaldırıldı

workerWithError.js:

self.addEventListener('message', function(e) {
postMessage(1/x); // Intentional error.
};

Güvenlikle ilgili söz

Yerel erişimle ilgili kısıtlamalar

Google Chrome'un güvenlik kısıtlamaları nedeniyle, çalışanlar tarayıcının en son sürümlerinde yerel olarak (ör. file:// üzerinden) çalışmaz. Aksine, sessizce başarısız olurlar! Uygulamanızı file:// şemasından çalıştırmak için Chrome'u --allow-file-access-from-files işareti ayarlı olarak çalıştırın.

Diğer tarayıcılarda aynı kısıtlama uygulanmaz.

Aynı kaynakla ilgili dikkat edilmesi gereken noktalar

Çalışan komut dosyaları, çağrı sayfasıyla aynı şemaya sahip harici dosyalar olmalıdır. Bu nedenle, bir data: URL'sinden veya javascript: URL'sinden komut dosyası yükleyemezsiniz ve bir https: sayfası, http: URL'siyle başlayan çalışan komut dosyaları başlatamaz.

Kullanım alanları

Peki hangi tür uygulamalar web çalışanlarından yararlanır? İşte, zihninizi çalıştıracak birkaç fikir daha:

  • Daha sonra kullanmak üzere verileri önceden getirme ve/veya önbelleğe alma.
  • Kod söz dizimi vurgulama veya diğer gerçek zamanlı metin biçimlendirmeleri.
  • Yazım denetleyici.
  • Video veya ses verileri analiz ediliyor.
  • Arka plan G/Ç veya web hizmetlerinin anketleri.
  • Büyük dizileri veya hızlı JSON yanıtlarını işleme.
  • <canvas> içinde resim filtreleme.
  • Yerel web veritabanının çok sayıda satırını güncelleme.

Web Workers API'nin dahil olduğu kullanım alanları hakkında daha fazla bilgi için Çalışanlara Genel Bakış sayfasını ziyaret edin.

Demolar

Referanslar