Nguon: Microsoft Learn · .NET 8.0

.NET trên Web Workers

Nguồn: .NET on Web Workers

Các ứng dụng web hiện đại thường yêu cầu các tác vụ tính toán nặng có thể chặn luồng UI chính, dẫn đến trải nghiệm người dùng kém. Web Workers cung cấp giải pháp cho vấn đề này bằng cách cho phép JavaScript (JS) chạy trên các luồng riêng biệt. Với .NET WebAssembly (Wasm), bạn có thể chạy code C# trong Web Workers, kết hợp lợi ích hiệu năng của code đã biên dịch với mô hình thực thi không chặn của các luồng nền.

Phương pháp này đặc biệt có giá trị khi bạn cần thực hiện các phép tính phức tạp, xử lý dữ liệu, hoặc logic nghiệp vụ mà không yêu cầu thao tác DOM trực tiếp. Thay vì viết lại các thuật toán trong JS, bạn có thể duy trì codebase .NET hiện có và thực thi nó hiệu quả trong nền trong khi frontend React.js vẫn phản hồi tốt.

Lưu ý: Với .NET 8/9/10, hãy làm theo các bước wasmbrowser thủ công trong bài viết này. Để xem hướng dẫn dành riêng cho Blazor, xem ASP.NET Core Blazor with .NET on Web Workers.

Bài viết này trình bày phương pháp React sử dụng dự án .NET WebAssembly độc lập.

Ứng dụng mẫu

Khám phá triển khai hoàn chỉnh trong kho mẫu Blazor trên GitHub. Mẫu dành cho .NET 10 trở lên và có tên DotNetOnWebWorkersReact.

Điều kiện tiên quyết và cài đặt

Trước khi bắt đầu triển khai, hãy đảm bảo các công cụ cần thiết đã được cài đặt.

.NET SDK 8.0 trở lên là bắt buộc. Nếu WebAssembly build tools chưa được cài đặt, hãy chạy:

bash
dotnet workload install wasm-tools
dotnet workload install wasm-experimental

Đối với frontend React.js, Node.jsnpm phải được cài đặt.

Tạo một ứng dụng React mới:

bash
npx create-react-app react-app
cd react-app

Tạo dự án .NET WebAssembly

Tạo một dự án WebAssembly browser mới để đóng vai trò là Web Worker:

bash
dotnet new wasmbrowser -o WebWorkersOnReact
cd WebWorkersOnReact
dotnet add package QRCoder

Chỉnh sửa file Program.cs để thiết lập điểm vào (entry point) Web Worker và xử lý tin nhắn:

csharp
using System;
using System.Runtime.InteropServices.JavaScript;
using QRCoder;
using System.Linq;

public partial class QRGenerator
{
    private static readonly int MAX_QR_SIZE = 20;

    [JSExport]
    internal static byte[] Generate(string text, int qrSize)
    {
        if (qrSize >= MAX_QR_SIZE)
        {
            throw new Exception(
                $"QR code size must be less than {MAX_QR_SIZE}. Try again.");
        }
        QRCodeGenerator qrGenerator = new QRCodeGenerator();
        QRCodeData qrCodeData = qrGenerator.CreateQrCode(
            text, QRCodeGenerator.ECCLevel.Q);
        BitmapByteQRCode qrCode = new BitmapByteQRCode(qrCodeData);
        return qrCode.GetGraphic(qrSize);
    }
}

Thêm file wwwroot/worker.js với code interop (tương tác) giữa C# và JS:

javascript
import { dotnet } from './_framework/dotnet.js'

let assemblyExports = null;
let startupError = undefined;

try {
  const { getAssemblyExports, getConfig } = await dotnet.create();
  const config = getConfig();
  assemblyExports = await getAssemblyExports(config.mainAssemblyName);
}
catch (err) {
  startupError = err.message;
}

self.addEventListener('message', async function(e) {
  try {
    if (!assemblyExports) {
      throw new Error(startupError || "worker exports not loaded");
    }
    let result = null;
    switch (e.data.command) {
      case "generateQR":
        const size = Number(e.data.size);
        const text = e.data.text;
        if (size === undefined || text === undefined)
          new Error("Inner error, got empty QR generation data from React");
        result = assemblyExports.QRGenerator.Generate(text, size);
        break;
      default:
          throw new Error("Unknown command: " + e.data.command);
    }
    self.postMessage({
      command: "response",
      requestId: e.data.requestId,
      result,
    });
  }
  catch (err) {
    self.postMessage({
      command: "response",
      requestId: e.data.requestId,
      error: err.message,
    });
  }
}, false);

Build dự án worker:

bash
dotnet build

Thiết lập ứng dụng React

Để kiểm tra nhanh, hãy sao chép output của worker vào các file tĩnh của ứng dụng React thủ công. Với ứng dụng thực tế, hãy tự động hóa các bước sao chép này bằng npm script hoặc bước build khác.

Tạo một Web Worker file client.js để nhận tin nhắn từ dotnet:

javascript
const dotnetWorker = new Worker('../../qr/wwwroot/worker.js', { type: "module" } );

dotnetWorker.addEventListener('message', async function (e) {
  switch (e.data.command) {
    case "response":
      if (!e.data.requestId) {
        console.error("No requestId in response from worker");
      }
      const request = pendingRequests[e.data.requestId];
      delete pendingRequests[e.data.requestId];
      if (e.data.error) {
        request.reject(new Error(e.data.error));
      }
      request.resolve(e.data.result);
      break;
    default:
      console.log('Worker said: ', e.data);
      break;
  }
}, false);

Kết nối chức năng này với UI và thêm nút kích hoạt generateQR:

javascript
export async function generateQR(text, size) {
  const response = await sendRequestToWorker({
    command: "generateQR",
    text: text,
    size: size
  });
  const blob = new Blob([response], { type: 'image/png' });
  return URL.createObjectURL(blob);
}

function sendRequestToWorker(request) {
  pendingRequestId++;
  const promise = new Promise((resolve, reject) => {
    pendingRequests[pendingRequestId] = { resolve, reject };
  });
  dotnetWorker.postMessage({
    ...request,
    requestId: pendingRequestId
  });
  return promise;
}

Cân nhắc về hiệu năng và tối ưu hóa

Khi làm việc với .NET trên Web Workers, hãy cân nhắc các chiến lược tối ưu hóa chính sau:

Xem ứng dụng mẫu để xem minh họa các khái niệm trên.