Neden bir Web Çalışanına ihtiyacınız var? Web Çalışanı, bir web uygulaması için bir kod bileşenidir. Geliştiricinin, ana uygulamanın yürütülmesini kesintiye uğratmaması için bir JavaScript görevi için yeni bir yürütme iş parçacığı oluşturmasına olanak tanır.
İlk bakışta, tarayıcıların doğası gereği iş parçacığını desteklediği ve geliştiricinin özel bir şey yapması gerekmediği görünebilir. Ne yazık ki durum böyle değil. Web Çalışanları gerçek bir eşzamanlılık sorununu çözer.
Web Çalışanları, web tarayıcılarından beklenen işlevsel standartların bir parçasıdır ve bunların özellikleri W3C'de yazılmıştır. Angular çerçevesi bizim için Web Çalışanlarını tamamladı ve bunları Angular Komut Satırı Arayüzünü (CLI) kullanarak uygulamamıza kolayca ekleyebiliriz.
Bu makalede öncelikle tarayıcıdaki JavaScript ile iş parçacığı eşzamanlılığıyla ilgili bazı yanlış anlamaları inceleyeceğiz. Ardından, bir web sitesinde eş zamanlı iş parçacığı oluşturmayı sağlayan Angular ile Web Çalışanlarını uygulamanın ne kadar kolay olduğunu gösteren işlevsel bir örnek oluşturacağız.
Bazı geliştiriciler, tarayıcı bir web sitesine bağlanıp bir sayfanın HTML'sini aldığında, birden fazla bağlantı (altı civarında) açabilir ve kaynakları (resimler, bağlı CSS dosyaları, bağlantılı JavaScript dosyaları, vb.) çekebildiğinden, JavaScript'in doğası gereği tarayıcıda eşzamanlı olduğuna inanır. vb.) aynı anda. Görünüşe göre tarayıcı birden fazla iş parçacığını ve çok sayıda görevi aynı anda yürütüyor (bağlam değiştirme yoluyla).
Deneyimsiz web geliştiricisine göre bu, tarayıcının eşzamanlı çalışma yapabileceğini gösteriyor gibi görünüyor. Ancak konu JavaScript olduğunda tarayıcı aslında aynı anda yalnızca bir işlemi yürütür.
Çoğu modern web sitesi, Tek Sayfa Uygulamaları (SPA) ve daha modern Aşamalı Web Uygulamaları (PWA), JavaScript'e dayanır ve genellikle çok sayıda JavaScript modülü içerir. Ancak web uygulamasının JavaScript çalıştırdığı herhangi bir zamanda tarayıcı tek bir etkinlik iş parçacığıyla sınırlıdır. Normal koşullar altında hiçbir JavaScript aynı anda çalışmaz.
Bu, JavaScript modüllerimizden birinde tanımlanmış uzun süren veya yoğun işlem gerektiren bir görevimiz varsa, kullanıcının uygulamada kekemelik veya takılmış gibi görünebileceği anlamına gelir. Aynı zamanda tarayıcı, kullanıcı arayüzünün (UI) güncellenebilmesi için işlemin tamamlanmasını bekler. Bu tür davranışlar, kullanıcıların web uygulamalarımıza veya SPA'larımıza olan güvenini kaybetmesine neden olur ve hiçbirimiz bunu istemiyoruz.
Web uygulamamızın eşzamanlı JavaScript iş parçacıklarını çalıştırabilmesini sağlamak için iki bölmeli bir örnek sayfa oluşturacağız. Bir bölmede, uygulamamızın kullanıcı arayüzü, resmi sürekli güncelleyen ve fare tıklamalarına tepki veren dairelerin bulunduğu bir ızgarayla temsil edilir. İkinci bölme, normalde UI iş parçacığını engelleyen ve UI'nin işini yapmasını engelleyen, uzun süren bir işlemi barındıracaktır.
Kullanıcı arayüzümüzü duyarlı hale getirmek için, uzun sürecimizi ayrı bir iş parçacığında yürütecek ve kullanıcı arayüzü mantığının bu şekilde yürütülmesini engellemeyecek bir Web Worker'da çalıştıracağız. Uygulamayı oluşturmak için çerçeve olarak Angular'ı kullanacağız çünkü bu, Web Çalışanlarının oluşturulmasını tek komutlu basit bir görev haline getiriyor.
Angular CLI'yi kullanabilmek için Node.js ve NPM'nin (Node Paket Yöneticisi) kurulu olması gerekir. Node ve NPM'nin kurulu olduğundan emin olduktan sonra bir konsol penceresi açın ve ardından NPM aracılığıyla Angular CLI'yi kurun (bu tek seferlik bir şeydir):
npm install -g @angular/cli
Dizini, yeni uygulama şablonumuzu oluşturmak istediğimiz hedef dizine değiştirin. Artık uygulamamızı oluşturmaya hazırız. Bunun için “ng new” komutunu kullanıyoruz. Projemize NgWebWorker adını vereceğiz:
ng new NgWebWorker --no-standalone
Proje sihirbazı, projemize yönlendirmeyi dahil etmek isteyip istemediğimizi sorar. Bu örnekte yönlendirmeye ihtiyacımız yok, dolayısıyla n yazın.
Daha sonra ne tür stil sayfası formatını kullanmak istediğimizi soracaktır. Angular, Sass ve Less gibi stil sayfası işlemcilerinin kullanılmasını destekler, ancak bu durumda basit CSS kullanacağız, bu nedenle varsayılan için Enter tuşuna basmanız yeterlidir.
Daha sonra NPM gerekli paketleri çekerken ve CLI şablon projesini oluştururken bazı CREATE mesajlarını göreceğiz. Son olarak, tamamlandığında komut satırında tekrar yanıp sönen bir imleç görüyoruz.
Bu noktada Angular CLI bir proje oluşturmuş ve onu NgWebWorker klasörüne yerleştirmiştir. Dizini NgWebWorker olarak değiştirin. Angular CLI, Node HTTP sunucusunun kurulumu dahil, Angular projemiz üzerinde çalışmak için ihtiyacımız olan her şeyi yarattı. Bu, şablon uygulamasını başlatmak için yapmamız gereken tek şeyin aşağıdaki olduğu anlamına gelir:
ng serve
Angular CLI projenizi derler ve Node HTTP sunucusunu başlatır. Artık uygulamayı <a href="http://localhost:4200"target="_blank"> URL'sine işaret ederek tarayıcınıza yükleyebilirsiniz.
Sayfayı yüklediğimizde üstte proje adının yer aldığı temel şablonu görüyoruz.
"NG serve" çalıştırmanın yararı, kodda yapılan herhangi bir değişikliğin sitenin tarayıcıda otomatik olarak yenilenmesine neden olması ve değişikliklerin etkili olduğunu görmeyi çok daha kolay hale getirmesidir.
Odaklanacağımız kodun çoğu /src/app dizini altındadır.
app.component.html, şu anda ana sayfayı görüntülemek için kullanılan bir bileşen için HTML içerir. Bileşen kodu app.component.ts (TypeScript) dosyasında temsil edilir.
app.component.html içeriğini silip kendi düzenimizi ekleyeceğiz. Sol tarafta uzun süredir devam eden süreç değerlerimizi gösteren bölünmüş bir sayfa oluşturacağız ve sağ tarafa rastgele bazı daireler çizeceğiz. Bu, tarayıcımızın bağımsız olarak çalışacak iki ek Web Worker iş parçacığını çalıştırmasına izin verecektir, böylece Angular Web Worker'ları çalışırken görebilirsiniz.
Makalenin geri kalanına ilişkin tüm kodlar GitHub deposundan edinilebilir.
Rastgele daireler çizilirken (uzun süren süreç başlamadan önce) son sayfanın nasıl görüneceği aşağıda açıklanmıştır.
app.component.html dosyasındaki kodu aşağıdakiyle değiştirin:
<div id="first"> <div class="innerContainer"> <button (click)="longLoop()">Start Long Process</button> </div> <div class="innerContainer"> <textarea rows="20" [value]="(longProcessOutput)"></textarea> </div> </div>
Kod indirme işlemi aynı zamanda bazı kimlikleri ve CSS sınıflarını ve kullanıcı arayüzünün çok basit biçimlendirmesi için kullanılan stilleri.css dosyasındaki ilişkili stilleri de içerir; dolayısıyla iki bölümümüz (sol ve sağ) ve diğer temel stillerimiz vardır.
.container { width: 100%; margin: auto; padding: 1% 2% 0 1%; } .innerContainer{ padding: 1%; } #first { width: 50%; height: 405px; float:left; background-color: lightblue; color: white; } #second { width: 50%; float: right; background-color: green; color: white; }
Burada dikkat edilmesi gereken önemli nokta, düğmeye bir Angular olay bağlaması (tıklama) eklemiş olmamızdır. Kullanıcı düğmeye tıkladığında, bileşen TypeScript dosyası app.component.ts'de bulunan longLoop yöntemi çağrılarak uzun süreç başlatılır.
title = 'NgWebWorker'; longProcessOutput: string = 'Long\nprocess\noutput\nwill\nappear\nhere\n'; fibCalcStartVal: number; longLoop() { this.longProcessOutput = ''; for (var x = 1; x <= 1000000000; x++) { var y = x / 3.2; if (x % 20000000 == 0) { this.longProcessOutput += x + '\n'; console.log(x); } } }
Bu, bileşenimizin bir üye değişkeni olan longProcessOutput'a 10 milyar yineleme yazma işlemi gerçekleştirir.
Bu üye değişkeni app.component.html dosyasına (textarea öğesinde) bağladığımız için, değişken her güncellendiğinde kullanıcı arayüzü güncellemeyi yansıtacaktır. HTML'de ayarladığımız değer üye değişkenini bağladığımız yerdir.
<textarea rows="20" [value]="longProcessOutput"></textarea>
Çalıştır. Düğmeye tıkladığımızda pek bir şey olmadığını göreceğiz ve ardından aniden metin alanı bir dizi değerle güncelleniyor. Konsolu açtığımızda kod çalışırken orada yazılan değerleri görüyoruz.
Daha sonra rastgele daireler çizmek için bir “daire” bileşeni ekleyeceğiz. Bunu Angular CLI'yi kullanarak aşağıdaki komutla yapabiliriz:
ng generate component circle
Komut, Circle adında yeni bir klasör oluşturdu ve dört yeni dosya oluşturdu:
Circle.component.html
Circle.component.spec.ts (birim testleri)
Circle.component.ts (TypeScript kodu)
Circle.component.css (yalnızca bu bileşenle ilişkili HTML'ye uygulanacak stiller)
HTML basittir. Sadece bileşenimizi temsil edecek HTML'ye ihtiyacımız var. Bizim durumumuzda bu, sayfanın sağ tarafında açık yeşil ızgarayı gösterecek ve daireleri çizecek olan bileşendir. Bu çizim HTML Canvas öğesi aracılığıyla yapılır.
<div id="second"> <canvas #mainCanvas (mousedown)="toggleTimer()"></canvas> </div>
Mousedown olayını yakalamak için bir Angular olay bağlaması ekleyerek dairelerin çizimini başlatıp durduruyoruz. Kullanıcı Kanvas alanının herhangi bir yerine tıklarsa, eğer süreç henüz başlamadıysa daireler çizilmeye başlayacaktır. İşlem zaten başlatılmışsa toggleTimer yöntemi ( circle.component.ts dosyasında bulunur) ızgarayı temizler ve daire çizimini durdurur.
toggleTimer, her 100 milisaniyede bir (10 daire/saniye) rastgele seçilen renkte bir daire çizmek için setInterval'ı kullanır.
toggleTimer(){ if (CircleComponent.IntervalHandle === null){ CircleComponent.IntervalHandle = setInterval(this.drawRandomCircles,50); } else{ clearInterval(CircleComponent.IntervalHandle); CircleComponent.IntervalHandle = null; this.drawGrid(); } }
Circle.component.ts'de Canvas öğesini ayarlayan, üye değişkenlerini başlatan ve çizimi yapan daha fazla kod vardır. Eklendiğinde kodunuz şöyle görünmelidir:
import { ViewChild, Component, ElementRef, AfterViewInit } from '@angular/core'; @Component({ selector: 'app-circle', templateUrl: './circle.component.html', styleUrl: './circle.component.css', }) export class CircleComponent implements AfterViewInit { title = 'NgWebWorker'; static IntervalHandle = null; static ctx: CanvasRenderingContext2D; GRID_LINES: number = 20; lineInterval: number = 0; gridColor: string = 'lightgreen'; static CANVAS_SIZE: number = 400; @ViewChild('mainCanvas', { static: false }) mainCanvas: ElementRef; constructor() { console.log('ctor complete'); } ngAfterViewInit(): void { CircleComponent.ctx = (<HTMLCanvasElement>( this.mainCanvas.nativeElement )).getContext('2d'); this.initApp(); this.initBoard(); this.drawGrid(); this.toggleTimer(); } initApp() { CircleComponent.ctx.canvas.height = CircleComponent.CANVAS_SIZE; CircleComponent.ctx.canvas.width = CircleComponent.ctx.canvas.height; } initBoard() { console.log('initBoard...'); this.lineInterval = Math.floor( CircleComponent.ctx.canvas.width / this.GRID_LINES ); console.log(this.lineInterval); } drawGrid() { console.log('drawGrid...'); CircleComponent.ctx.globalAlpha = 1; // fill the canvas background with white CircleComponent.ctx.fillStyle = 'white'; CircleComponent.ctx.fillRect( 0, 0, CircleComponent.ctx.canvas.height, CircleComponent.ctx.canvas.width ); for (var lineCount = 0; lineCount < this.GRID_LINES; lineCount++) { CircleComponent.ctx.fillStyle = this.gridColor; CircleComponent.ctx.fillRect( 0, this.lineInterval * (lineCount + 1), CircleComponent.ctx.canvas.width, 2 ); CircleComponent.ctx.fillRect( this.lineInterval * (lineCount + 1), 0, 2, CircleComponent.ctx.canvas.width ); } } toggleTimer() { if (CircleComponent.IntervalHandle === null) { CircleComponent.IntervalHandle = setInterval(this.drawRandomCircles, 100); } else { clearInterval(CircleComponent.IntervalHandle); CircleComponent.IntervalHandle = null; this.drawGrid(); } } static generateRandomPoints() { var X = Math.floor(Math.random() * CircleComponent.CANVAS_SIZE); // gen number 0 to 649 var Y = Math.floor(Math.random() * CircleComponent.CANVAS_SIZE); // gen number 0 to 649 return { x: X, y: Y }; } drawRandomCircles() { var p = CircleComponent.generateRandomPoints(); CircleComponent.drawPoint(p); } static drawPoint(currentPoint) { var RADIUS: number = 10; var r: number = Math.floor(Math.random() * 256); var g: number = Math.floor(Math.random() * 256); var b: number = Math.floor(Math.random() * 256); var rgbComposite: string = 'rgb(' + r + ',' + g + ',' + b + ')'; CircleComponent.ctx.strokeStyle = rgbComposite; CircleComponent.ctx.fillStyle = rgbComposite; CircleComponent.ctx.beginPath(); CircleComponent.ctx.arc( currentPoint.x, currentPoint.y, RADIUS, 0, 2 * Math.PI ); // allPoints.push(currentPoint); CircleComponent.ctx.stroke(); CircleComponent.ctx.fill(); } }
Circle bileşenini index.html dosyasına eklemeyi unutmayın:
<body> <app-root></app-root> <app-circle></app-circle> </body>
Sayfa yüklendiğinde daireler çizilmeye başlayacaktır. [Uzun İşlemi Başlat] butonuna tıkladığımızda çizimin durakladığını göreceğiz. Çünkü tüm işler aynı iş parçacığı üzerinde yapılıyor.
Bir Web Çalışanı ekleyerek bu sorunu çözelim.
CLI'yi kullanarak yeni bir Web Worker eklemek için proje klasörümüze gidip aşağıdaki komutu uygulamamız yeterlidir:
ng generate web-worker app
Bu son parametre (app), Web Worker'ımıza yerleştirmek isteyeceğimiz, uzun süren sürecimizi içeren bileşenin adıdır.
Angular, app.component.ts dosyasına aşağıdaki gibi görünen bazı kodlar ekleyecektir:
if (typeof Worker !== 'undefined') { // Create a new const worker = new Worker(new URL('./app.worker', import.meta.url)); worker.onmessage = ({ data }) => { console.log(`page got message: ${data}`); }; worker.postMessage('hello'); } else { // Web Workers are not supported in this environment. // You should add a fallback so that your program still executes correctly. }
Yeni kod ne işe yarıyor? Bu kodun, komutun eklediği yeni app.worker Bileşenine referans verdiğini görebiliriz. Bu noktada kod:
İşte app.worker.ts'nin tüm içeriği:
/// <reference lib="webworker" /> addEventListener('message', ({ data }) => { const response = `worker response to ${data}`; postMessage(response); });
Bu adımların sonucunda konsolda aşağıdaki konsol çıktısında son satır gibi görünecek mesajları göreceğiz:
Bu, orijinal Worker nesnesinde oluşturulan EventHandler'da oluşan console.log dosyasıdır:
worker.onmessage = ({ data }) => { console.log(`page got message: ${data}`); };
Bu bize app.component'in app.worker'a bir mesaj gönderdiğini ve app.worker'ın da kendi mesajıyla yanıt verdiğini söyler.
Uzun Koşu sürecimizi başka bir iş parçacığı üzerinde çalıştırmak için Worker'ı kullanmak istiyoruz, böylece daire çizim kodumuz kesintiye uğramaz.
Öncelikle kullanıcı arayüzü öğelerimizle ilgili kodu app.component sınıfımızın yapıcısına taşıyalım.
constructor() { if (typeof Worker !== 'undefined') { // Create a new const worker = new Worker(new URL('./app.worker', import.meta.url)); worker.onmessage = ({ data }) => { console.log(`page got message: ${data}`); this.longProcessOutput += `page got message: ${data}` + '\n'; }; worker.postMessage('hello'); } else { // Web Workers are not supported in this environment. // You should add a fallback so that your program still executes correctly. } }
Bu, artık longProcessOutput değişkenine başvurmamızı sağlar. Bununla o değişkene erişebiliriz; İlk test olarak konsola yazmak yerine temel verileri textarea'ya ekleyen işçi.onmessage'ımız var.
Sol tarafta vurgulanan metnin alınan mesaj olduğunu görebilirsiniz.
Döngü çalıştığında kendi iş parçacığı üzerinde çalışacağından emin olmak için hala uzun süredir devam eden döngümüzü Web Worker'a taşımamız gerekiyor.
Son app.component.ts dosyamızda kullanacağımız kodun büyük kısmı şöyle:
constructor() { if (typeof Worker !== 'undefined') { // Create a new const worker = new Worker(new URL('./app.worker', import.meta.url)); worker.onmessage = ({ data }) => { console.log(`page got message: ${data}`); this.longProcessOutput += `page got message: ${data}` + '\n'; }; worker.postMessage('hello'); } else { // Web Workers are not supported in this environment. // You should add a fallback so that your program still executes correctly. } } longLoop() { this.longProcessOutput = ''; for (var x = 1; x <= 1000000000; x++) { var y = x / 3.2; if (x % 20000000 == 0) { this.longProcessOutput += x + '\n'; console.log(x); } } }
İşçi değişkenini artık üye değişkeni olan sınıfa taşıdık. Bu şekilde AppComponent sınıfımızın herhangi bir yerinde kolayca referans verebiliriz.
Şimdi, yapıcıdaki kodla, çalışan nesnesinde bir mesaj olay işleyicisini nasıl tanımladığımıza daha yakından bakalım:
this.worker.onmessage = ({ data }) => { this.longProcessOutput += `${data}` + "\n"; };
Bu kod, Web Worker sınıfı ( app.worker.ts içinde bulunur) postMessage(data) öğesini çağırdığında çalışacaktır. postMessage yöntemi her çağrıldığında, longProcessOutput (textarea'ya bağlı model), veriler artı bir satırbaşı ("\n") ile güncellenecektir; bu, her değerin metinde kendi satırına yazılması anlamına gelir. textarea öğesi.
Gerçek Web Worker'da ( app.worker.ts ) bulunan tüm kodlar şunlardır:
addEventListener('message', ({ data }) => { console.log(`in worker EventListener : ${data}`); for (var x = 1; x <=1000000000;x++){ var y = x/3.2; if ((x % 20000000) == 0){ // posts the value back to our worker.onmessage handler postMessage(x); // don't need console any more --> console.log(x); } } });
Bu olay işleyicisi, kullanıcı [Uzun İşlem Çalıştır] düğmesini tıklattığında tetiklenir. Web Worker'da ( app.worker.ts ) bulunan tüm kodlar yeni bir iş parçacığında çalışır. Web Worker'ın değeri budur; kodu ayrı bir iş parçacığında çalışır. Bu nedenle artık web uygulamasının ana iş parçacığını etkilemiyor.
Kullanıcı düğmeye tıkladığında Web Worker kodu tetiklenir çünkü artık longLoop yöntemimizde aşağıdaki koda sahibiz.
longLoop(){ this.longProcessOutput = ""; // the following line starts the long process on the Web Worker // by sending a message to the Web Worker this.worker.postMessage("start looping..."); }
Mesaj işçiye gönderildiğinde, EventListener harekete geçer ve oraya yerleştirdiğimiz orijinal longLoop'tan kodu çalıştırır.
Uygulamayı çalıştırdığınızda, [Uzun İşlemi Başlat] düğmesine tıklamanın artık daire çiziminin duraklatılmasına neden olmadığını göreceksiniz. Ayrıca daire çizme Kanvas bileşeniyle doğrudan etkileşim kurabileceksiniz, böylece longLoop hala çalışırken buna tıklarsanız Kanvas hemen yeniden çizilecektir. Önceden, bunu yaptığınızda uygulama donmuş gibi davranıyordu.
Artık uzun süredir devam eden süreçlerinizi Angular Web Worker'a ekleyebilir ve donmayan bir uygulamanın tüm avantajlarından yararlanabilirsiniz.
JavaScript Web Workers ile çözülmüş bir örnek görmek istiyorsanız Plunker'da görebilirsiniz.
Çerçeveden bağımsız kullanıcı arayüzü bileşenleri mi arıyorsunuz? MESCIUS , veri ızgaraları, grafikler, göstergeler ve giriş kontrolleri dahil olmak üzere eksiksiz bir JavaScript kullanıcı arayüzü bileşenleri setine sahiptir. Ayrıca güçlü elektronik tablo bileşenleri , raporlama kontrolleri ve gelişmiş sunum görünümleri de sunuyoruz.
Angular (aynı zamanda React ve Vue) için de derin bir desteğimiz var ve bileşenlerimizi modern JavaScript çerçevelerinde kullanılmak üzere genişletmeye kendimizi adadık.