Tại sao bạn cần một Web Worker ? Web Worker là một thành phần mã cho một ứng dụng web. Nó cho phép nhà phát triển tạo một luồng thực thi mới cho tác vụ JavaScript để không làm gián đoạn quá trình thực thi của ứng dụng chính.
Thoạt nhìn, có vẻ như các trình duyệt vốn đã hỗ trợ phân luồng và nhà phát triển không cần phải làm bất cứ điều gì đặc biệt. Thật không may, đó không phải là trường hợp. Web Workers giải quyết một vấn đề tương tranh thực sự.
Web Workers là một phần của các tiêu chuẩn chức năng dự kiến của trình duyệt web và các thông số kỹ thuật dành cho chúng đã được viết tại W3C . Khung Angular đã gói gọn Web Workers cho chúng ta và chúng ta có thể dễ dàng thêm chúng vào ứng dụng của mình bằng Giao diện dòng lệnh góc (CLI).
Trong bài viết này, trước tiên chúng ta sẽ xem xét một số quan niệm sai lầm về tính đồng thời của luồng với JavaScript trong trình duyệt. Sau đó, chúng ta sẽ tạo một ví dụ chức năng minh họa việc triển khai Web Workers với Angular dễ dàng như thế nào, tính năng này cho phép tạo luồng đồng thời trên một trang web.
Một số nhà phát triển tin rằng JavaScript vốn đã đồng thời trong trình duyệt vì khi trình duyệt kết nối với một trang web và truy xuất HTML cho một trang, nó có thể mở nhiều kết nối (khoảng sáu) và lấy tài nguyên (hình ảnh, tệp CSS được liên kết, tệp JavaScript được liên kết, v.v.) đồng thời. Có vẻ như trình duyệt thực thi đồng thời một số luồng và nhiều tác vụ (thông qua chuyển đổi ngữ cảnh).
Đối với nhà phát triển web chưa quen, điều này dường như cho thấy rằng trình duyệt có thể thực hiện công việc đồng thời. Tuy nhiên, khi nói đến JavaScript, trình duyệt thực sự chỉ thực thi một tiến trình tại một thời điểm.
Hầu hết các trang web hiện đại, Ứng dụng trang đơn (SPA) và Ứng dụng web lũy tiến (PWA) hiện đại hơn đều phụ thuộc vào JavaScript và thường chứa nhiều mô-đun JavaScript. Tuy nhiên, bất cứ lúc nào ứng dụng web đang chạy JavaScript, trình duyệt sẽ bị giới hạn ở một luồng hoạt động duy nhất. Không có JavaScript nào chạy đồng thời trong các trường hợp thông thường.
Điều đó có nghĩa là nếu chúng tôi xác định một tác vụ kéo dài hoặc tốn nhiều quy trình trong một trong các mô-đun JavaScript của mình, thì người dùng có thể gặp phải tình trạng ứng dụng bị giật hoặc dường như bị treo. Đồng thời, trình duyệt chờ quá trình hoàn tất trước khi cập nhật giao diện người dùng (UI). Kiểu hành vi này khiến người dùng mất niềm tin vào ứng dụng web hoặc SPA của chúng tôi và không ai trong chúng tôi muốn điều đó.
Chúng tôi sẽ tạo một trang ví dụ có hai ngăn để cho phép ứng dụng web của chúng tôi chạy các luồng JavaScript đồng thời. Trong một khung, giao diện người dùng của ứng dụng của chúng tôi được biểu thị bằng một lưới có các vòng tròn, lưới này liên tục cập nhật hình ảnh và phản ứng với các cú nhấp chuột. Khung thứ hai sẽ lưu trữ một quy trình chạy dài, quy trình này thường chặn luồng giao diện người dùng và ngăn giao diện người dùng thực hiện công việc của nó.
Để làm cho giao diện người dùng phản hồi nhanh, chúng tôi sẽ chạy quy trình dài của mình trong Web Worker, quy trình này sẽ thực thi quy trình đó trên một luồng riêng biệt và sẽ không chặn việc thực thi logic giao diện người dùng theo cách này. Chúng ta sẽ sử dụng Angular làm khung để xây dựng ứng dụng vì nó khiến việc xây dựng Web Workers trở thành một tác vụ một lệnh đơn giản.
Để sử dụng Angular CLI, chúng ta cần cài đặt Node.js và NPM (Trình quản lý gói nút). Khi chúng tôi đã đảm bảo rằng Node và NPM đã được cài đặt, hãy mở cửa sổ bảng điều khiển, sau đó cài đặt Angular CLI qua NPM (đây chỉ là việc một lần):
npm install -g @angular/cli
Thay đổi thư mục thành thư mục đích nơi chúng tôi muốn tạo mẫu ứng dụng mới. Bây giờ, chúng ta đã sẵn sàng để tạo ứng dụng của mình. Chúng tôi sử dụng lệnh “ng new” để làm điều đó. Chúng tôi sẽ đặt tên cho dự án của mình là NgWebWorker:
ng new NgWebWorker --no-standalone
Trình hướng dẫn dự án hỏi xem chúng tôi có muốn đưa định tuyến vào dự án của mình không. Chúng tôi không cần định tuyến cho ví dụ này, vì vậy hãy nhập n.
Sau đó nó sẽ hỏi loại định dạng biểu định kiểu mà chúng tôi muốn sử dụng. Angular hỗ trợ sử dụng các bộ xử lý biểu định kiểu như Sass và Less, nhưng trong trường hợp này, chúng tôi sẽ sử dụng CSS đơn giản, vì vậy chỉ cần nhấn Enter để làm mặc định.
Sau đó, chúng ta sẽ thấy một số thông báo TẠO khi NPM lấy các gói cần thiết và CLI tạo dự án mẫu. Cuối cùng, khi hoàn tất, chúng ta lại nhận được con trỏ nhấp nháy ở dòng lệnh.
Tại thời điểm này, Angular CLI đã tạo một dự án và đặt nó vào thư mục NgWebWorker. Thay đổi thư mục thành NgWebWorker. Angular CLI đã tạo mọi thứ chúng tôi cần để thực hiện dự án Angular, bao gồm cả việc cài đặt máy chủ Node HTTP. Điều đó có nghĩa là tất cả những gì chúng ta phải làm để bắt đầu ứng dụng mẫu là như sau:
ng serve
Angular CLI biên dịch dự án của bạn và khởi động máy chủ Node HTTP. Bây giờ, bạn có thể tải ứng dụng trong trình duyệt của mình bằng cách trỏ ứng dụng đó vào URL <a href="http://localhost:4200"target="_blank"> .
Khi tải trang, chúng ta sẽ thấy mẫu cơ bản có tên dự án ở trên cùng.
Lợi ích của việc chạy “ng phục vụ” là bất kỳ thay đổi nào được thực hiện đối với mã sẽ tự động khiến trang web được làm mới trong trình duyệt, giúp bạn dễ dàng thấy các thay đổi có hiệu lực hơn nhiều.
Hầu hết mã chúng ta sẽ tập trung vào đều nằm trong thư mục /src/app.
app.comComponent.html chứa HTML cho một thành phần hiện được sử dụng để hiển thị trang chính. Mã thành phần được thể hiện trong tệp app.comComponent.ts (TypeScript).
Chúng tôi sẽ xóa nội dung của app.comComponent.html và thêm bố cục của riêng mình. Chúng tôi sẽ tạo một trang chia nhỏ hiển thị các giá trị quy trình chạy dài của chúng tôi ở phía bên trái và vẽ một số vòng tròn ngẫu nhiên ở phía bên phải. Điều này sẽ cho phép trình duyệt của chúng tôi chạy hai luồng Web Worker bổ sung sẽ hoạt động độc lập để bạn có thể thấy Angular Web Workers đang hoạt động.
Tất cả mã cho phần còn lại của bài viết có thể được lấy từ kho GitHub.
Đây là trang cuối cùng sẽ trông như thế nào khi vẽ các vòng tròn ngẫu nhiên (trước khi quá trình chạy dài bắt đầu).
Thay thế mã trong app.comComponent.html bằng mã sau:
<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>
Quá trình tải xuống mã cũng bao gồm một số ID và lớp CSS cũng như các kiểu liên quan trong styles.css , được sử dụng để định dạng giao diện người dùng rất đơn giản, vì vậy chúng tôi có hai phần (trái và phải) và các kiểu dáng cơ bản khác.
.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; }
Điều quan trọng cần lưu ý ở đây là chúng tôi đã thêm liên kết sự kiện Angular (nhấp chuột) vào nút. Khi người dùng nhấp vào nút, quá trình dài sẽ được bắt đầu bằng cách gọi phương thức longLoop được tìm thấy trong tệp TypeScript thành phần, app.comComponent.ts .
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); } } }
Điều này chạy 10 tỷ lần lặp ghi vào biến thành viên của thành phần của chúng tôi, longProcessOutput.
Vì chúng tôi đã ràng buộc biến thành viên đó trong app.comComponent.html (trên phần tử textarea), nên giao diện người dùng sẽ phản ánh bản cập nhật mỗi khi biến được cập nhật. Giá trị chúng tôi đặt trong HTML là nơi chúng tôi liên kết biến thành viên.
<textarea rows="20" [value]="longProcessOutput"></textarea>
Chạy nó. Chúng ta sẽ thấy rằng không có gì nhiều xảy ra khi chúng ta nhấp vào nút và đột nhiên, vùng văn bản được cập nhật với một loạt giá trị. Nếu mở bảng điều khiển, chúng ta sẽ thấy các giá trị được ghi ở đó khi mã chạy.
Tiếp theo, chúng ta sẽ thêm thành phần “vòng tròn” để vẽ các vòng tròn ngẫu nhiên. Chúng ta có thể làm điều đó bằng cách sử dụng Angular CLI bằng lệnh sau:
ng generate component circle
Lệnh đã tạo một thư mục mới có tên là vòng tròn và tạo bốn tệp mới:
vòng tròn.thành phần.html
Circle.comComponent.spec.ts (kiểm tra đơn vị)
Circle.comComponent.ts (mã TypeScript)
Circle.comComponent.css (các kiểu sẽ chỉ được áp dụng cho HTML được liên kết cho thành phần này)
HTML rất đơn giản. Chúng tôi chỉ cần HTML sẽ đại diện cho thành phần của chúng tôi. Trong trường hợp của chúng tôi, đây là thành phần ở bên phải trang, sẽ hiển thị lưới màu xanh nhạt và vẽ các vòng tròn. Bản vẽ này được thực hiện thông qua phần tử HTML Canvas.
<div id="second"> <canvas #mainCanvas (mousedown)="toggleTimer()"></canvas> </div>
Chúng tôi bắt đầu và dừng việc vẽ các vòng tròn bằng cách thêm ràng buộc sự kiện Angular để lấy sự kiện di chuột xuống. Nếu người dùng nhấp vào bên trong khu vực Canvas ở bất kỳ đâu, các vòng tròn sẽ bắt đầu vẽ nếu quá trình này chưa bắt đầu. Nếu quá trình này đã được bắt đầu thì phương thức TooggleTimer (có trong Circle.comComponent.ts ) sẽ xóa lưới và dừng việc vẽ các vòng tròn.
chuyển đổiTimer chỉ cần sử dụng setInterval để vẽ một vòng tròn ở một vị trí ngẫu nhiên với màu được chọn ngẫu nhiên cứ sau 100 mili giây (10 vòng tròn/giây).
toggleTimer(){ if (CircleComponent.IntervalHandle === null){ CircleComponent.IntervalHandle = setInterval(this.drawRandomCircles,50); } else{ clearInterval(CircleComponent.IntervalHandle); CircleComponent.IntervalHandle = null; this.drawGrid(); } }
Có nhiều mã hơn trong Circle.comComponent.ts để thiết lập phần tử Canvas, khởi tạo các biến thành viên và thực hiện việc vẽ. Khi được thêm vào, mã của bạn sẽ trông như thế này:
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(); } }
Đừng quên thêm thành phần vòng tròn vào tệp index.html :
<body> <app-root></app-root> <app-circle></app-circle> </body>
Khi tải trang, các vòng tròn sẽ bắt đầu vẽ. Khi chúng ta nhấp vào nút [Bắt đầu quá trình dài], chúng ta sẽ thấy bản vẽ tạm dừng. Đó là bởi vì tất cả công việc đang được thực hiện trên cùng một chủ đề.
Hãy khắc phục vấn đề đó bằng cách thêm Web Worker.
Để thêm Web Worker mới bằng CLI, chúng ta chỉ cần đi tới thư mục dự án của mình và thực hiện lệnh sau:
ng generate web-worker app
Tham số cuối cùng (ứng dụng) đó là tên của thành phần chứa quy trình chạy dài của chúng tôi mà chúng tôi sẽ muốn đặt vào Web Worker của mình.
Angular sẽ thêm một số mã vào app.comComponent.ts trông giống như sau:
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. }
Mã mới làm gì? Chúng ta có thể thấy rằng mã này tham chiếu Thành phần app.worker mới mà lệnh cũng đã thêm vào. Tại thời điểm này, mã:
Đây là toàn bộ nội dung của app.worker.ts :
/// <reference lib="webworker" /> addEventListener('message', ({ data }) => { const response = `worker response to ${data}`; postMessage(response); });
Chúng ta sẽ thấy các thông báo trong bảng điều khiển là kết quả của các bước này, trông giống như dòng cuối cùng trong đầu ra của bảng điều khiển sau:
Đó là console.log xảy ra trong EventHandler được tạo trên đối tượng Worker ban đầu:
worker.onmessage = ({ data }) => { console.log(`page got message: ${data}`); };
Điều đó cho chúng ta biết rằng app.comComponent đã đăng một tin nhắn lên app.worker và app.worker đã trả lời bằng một tin nhắn của chính nó.
Chúng tôi muốn sử dụng Worker để chạy quy trình Chạy dài của mình trên một luồng khác để mã vẽ vòng tròn của chúng tôi không bị gián đoạn.
Trước tiên, hãy di chuyển mã liên quan đến các thành phần giao diện người dùng của chúng ta sang hàm tạo của lớp app.comComponent.
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. } }
Điều này cho phép chúng ta tham chiếu biến longProcessOutput ngay bây giờ. Với điều đó, chúng ta có thể truy cập vào biến đó; chúng ta có worker.onmessage, bổ sung dữ liệu cơ bản vào vùng văn bản thay vì ghi vào bảng điều khiển giống như thử nghiệm ban đầu.
Bạn có thể thấy đoạn văn bản được đánh dấu bên trái là tin nhắn đã nhận được.
Chúng ta vẫn cần di chuyển vòng lặp dài hạn của mình sang Web Worker để đảm bảo rằng khi vòng lặp chạy, nó sẽ chạy trên luồng của chính nó.
Đây là phần lớn mã mà chúng ta sẽ có trong app.comComponent.ts cuối cùng:
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); } } }
Chúng tôi đã chuyển biến công nhân vào lớp, hiện là biến thành viên. Bằng cách đó, chúng ta có thể dễ dàng tham chiếu nó ở bất kỳ đâu trong lớp AppComponent của mình.
Tiếp theo, hãy xem xét kỹ hơn cách chúng ta xác định trình xử lý sự kiện tin nhắn trên đối tượng worker bằng mã trong hàm tạo:
this.worker.onmessage = ({ data }) => { this.longProcessOutput += `${data}` + "\n"; };
Mã đó sẽ chạy khi lớp Web Worker (tìm thấy trong app.worker.ts ) gọi postMessage(data). Mỗi khi phương thức postMessage được gọi, longProcessOutput (mô hình được liên kết với vùng văn bản) sẽ được cập nhật với dữ liệu cộng với dấu xuống dòng (“\n”), đơn giản là mỗi giá trị sẽ được ghi trên một dòng riêng trong phần tử vùng văn bản
Đây là tất cả mã được tìm thấy trong Web Worker thực tế ( app.worker.ts ):
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); } } });
Trình xử lý sự kiện này được kích hoạt khi người dùng nhấp vào nút [Chạy quy trình dài]. Tất cả mã được tìm thấy trong Web Worker ( app.worker.ts ) chạy trên một luồng mới. Đó là giá trị của Web Worker; mã của nó chạy trên một luồng riêng biệt. Đó là lý do tại sao nó không còn ảnh hưởng đến luồng chính của ứng dụng web nữa.
Mã Web Worker được kích hoạt khi người dùng nhấp vào nút vì hiện tại chúng tôi có đoạn mã sau trong phương thức longLoop của mình.
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..."); }
Khi thông báo được đăng lên nhân viên, EventListener sẽ kích hoạt và chạy mã từ longLoop ban đầu mà chúng tôi đã đặt ở đó.
Khi chạy ứng dụng, bạn sẽ thấy rằng việc nhấp vào nút [Bắt đầu quá trình dài] không còn khiến quá trình vẽ vòng tròn bị tạm dừng nữa. Bạn cũng sẽ có thể tương tác trực tiếp với thành phần Canvas vẽ vòng tròn để nếu bạn nhấp vào nó trong khi longLoop vẫn đang chạy, Canvas sẽ được vẽ lại ngay lập tức. Trước đây, ứng dụng hoạt động như thể bị đóng băng nếu bạn làm điều này.
Giờ đây, bạn có thể thêm các quy trình chạy dài của mình vào Angular Web Worker và tận dụng tất cả lợi ích của một ứng dụng không bị đóng băng.
Nếu bạn muốn xem một ví dụ được giải quyết bằng JavaScript Web Workers, bạn có thể xem nó trên Plunker .
Bạn đang tìm kiếm các thành phần giao diện người dùng không phụ thuộc vào khung? MESCIUS có một bộ hoàn chỉnh các thành phần UI JavaScript, bao gồm lưới dữ liệu, biểu đồ, thước đo và điều khiển đầu vào . Chúng tôi cũng cung cấp các thành phần bảng tính mạnh mẽ, kiểm soát báo cáo và chế độ xem bản trình bày nâng cao .
Chúng tôi có sự hỗ trợ sâu sắc cho Angular (cũng như React và Vue) và tận tâm mở rộng các thành phần của mình để sử dụng trong các khung JavaScript hiện đại.