WebAssembly trong TypeScript
Bài viết này giải thích về WebAssembly trong TypeScript.
Chúng tôi sẽ giải thích các phương pháp tích hợp TypeScript và WebAssembly một cách thiết thực và dễ hiểu.
YouTube Video
WebAssembly trong TypeScript
WebAssembly (Wasm) là một môi trường thực thi định dạng nhị phân, hoạt động với tốc độ gần như bản địa trong trình duyệt. Bằng cách gọi Wasm từ TypeScript, bạn có thể tận dụng hiệu quả các quá trình tính toán phức tạp và các thư viện gốc đã viết bằng C/C++ hoặc Rust.
Luồng thực thi cơ bản
Ở đây, chúng tôi sẽ giải thích luồng thực thi cơ bản của Wasm. TypeScript (hoặc trình duyệt) lấy tệp .wasm, khởi tạo nó và gọi các hàm đã xuất.
-
- Tạo tệp nhị phân .wasm bằng AssemblyScript, Rust, C++ hoặc chuẩn bị một tệp có sẵn.
-
- Lấy tệp .wasm trong TypeScript (hoặc trình duyệt) và khởi tạo nó một cách đồng bộ hoặc bất đồng bộ.
-
- Gọi các hàm đã xuất và chia sẻ bộ nhớ bằng
WebAssembly.Memorynếu cần thiết.
- Gọi các hàm đã xuất và chia sẻ bộ nhớ bằng
WebAssembly.instantiateStreaming
Tiếp theo, chúng tôi sẽ trình bày một ví dụ cơ bản về cách tải tệp Wasm và gọi một hàm đã xuất. Trình duyệt cần hỗ trợ instantiateStreaming.
Đoạn mã sau đây là một ví dụ về việc lấy simple.wasm từ máy chủ và gọi hàm add.
1// TypeScript: load-and-call.ts
2// Fetch and instantiate a wasm module and call its exported `add` function.
3async function run() {
4 const response = await fetch('http://localhost:3000/simple.wasm');
5 // Use instantiateStreaming when available for efficiency.
6 const { instance } = await WebAssembly.instantiateStreaming(response, {});
7 // @ts-ignore
8 const result = instance.exports.add(2, 3);
9 console.log('2 + 3 =', result);
10}
11run().catch(console.error);- Các hàm bên trong Wasm được lưu trong
instance.exports. - Vì TypeScript không nhận được thông tin kiểu dữ liệu, bạn cần sử dụng
@ts-ignorehoặc tự tạo định nghĩa kiểu dữ liệu.
Quy trình làm việc với AssemblyScript
AssemblyScript cho phép bạn viết Wasm với cú pháp giống TypeScript, là lựa chọn gần gũi cho các nhà phát triển TypeScript. Ở đây, chúng ta chuẩn bị một hàm đơn giản, biên dịch nó thành .wasm và .d.ts, sau đó gọi nó từ TypeScript.
1// assembly/index.ts (AssemblyScript)
2// npm install --save-dev assemblyscript
3
4// Export a simple function to add two integers.
5export function add(a: i32, b: i32): i32 {
6 return a + b;
7}- Bằng cách sử dụng
asc(trình biên dịchAssemblyScript), bạn có thể tạo ra tệp.wasmvà tùy chọn tạo thêm tệp định nghĩa kiểu.d.ts. Để thử cục bộ, hãy cài đặtassemblyscriptbằng npm và biên dịch nó.
1# build commands
2# npm install --save-dev assemblyscript
3npx asc assembly/index.ts -o build/simple.wasm -t build/simple.wat --bindings esm --exportTable --sourceMap
4
5# optionally generate d.ts with --exportRuntime or use as-bind / loader toolsDưới đây là ví dụ về cách lấy và gọi từ phía TypeScript.
1// ts client that loads AssemblyScript-generated wasm
2async function runAssemblyScript() {
3 const res = await fetch('http://localhost:3000/build/simple.wasm');
4 const { instance } = await WebAssembly.instantiateStreaming(res, {});
5 // @ts-ignore
6 console.log('AssemblyScript add:', instance.exports.add(10, 7));
7}
8runAssemblyScript().catch(console.error);- AssemblyScript cần chú ý khi quản lý bộ nhớ và xâu ký tự, nhưng rất dễ sử dụng cho các phép tính số học cơ bản.
Rust + wasm-bindgen (Một lựa chọn mạnh mẽ và phổ biến)
Phần này giải thích quy trình làm việc khi viết Wasm bằng Rust và kết nối nó với JavaScript hoặc TypeScript bằng cách sử dụng wasm-bindgen. Ở đây, chúng tôi sử dụng ví dụ hàm Fibonacci đơn giản để minh họa cách import mô-đun đã tạo như một mô-đun ES.
Xuất các hàm từ phía Rust bằng cách sử dụng wasm-bindgen.
1// src/lib.rs (Rust)
2// install wasm-pack from https://drager.github.io/wasm-pack/installer/
3use wasm_bindgen::prelude::*;
4
5// Export a function to JavaScript using wasm-bindgen.
6#[wasm_bindgen]
7pub fn fib(n: u32) -> u32 {
8 if n <= 1 { return n; }
9 let mut a = 0;
10 let mut b = 1;
11 for _ in 2..=n {
12 let tmp = a + b;
13 a = b;
14 b = tmp;
15 }
16 b
17}- Khi bạn biên dịch với
wasm-packhoặcwasm-bindgenCLI, các định nghĩa kiểu cho TypeScript và JS wrapper sẽ được tạo, cho phép bạn import trực tiếp dưới dạng ESM.
1# build with wasm-pack
2# install wasm-pack from https://drager.github.io/wasm-pack/installer/
3wasm-pack build --target nodejs --out-dir pkgỞ phía TypeScript, import và sử dụng mô-đun ES từ pkg.
1// Node.js: import WASM module built with --target web
2// import init, { fib } from '../pkg/my_wasm_module.js';
3// Node.js: import WASM module built with --target nodejs
4import wasm from '../pkg/my_wasm_module.js';
5
6async function run() {
7 //await init(); // --target web
8 console.log('fib(10)=', wasm.fib(10));
9}
10
11run().catch(console.error);wasm-packtạo ra các wrapper JavaScript và định nghĩa kiểu.d.ts, giúp dễ dàng sử dụng từ TypeScript. Lưu ý rằng khi bạn chỉ địnhwebcho tùy chọn--targetcủa lệnhwasm-pack, việc khởi tạo bất đồng bộ là cần thiết.
Ví dụ thực tế về chia sẻ bộ nhớ: Truyền và xử lý mảng (cấp thấp)
Khi trao đổi dữ liệu lớn với Wasm, chia sẻ ArrayBuffer để trao đổi dữ liệu hiệu quả là rất quan trọng. Ở đây chúng tôi trình bày ví dụ với AssemblyScript, nhưng nguyên lý tương tự cũng áp dụng với wasm-bindgen của Rust.
Ở phía AssemblyScript, chuẩn bị một hàm xuất để ghi vào bộ nhớ. Ví dụ, một hàm để bình phương từng phần tử của một mảng sẽ như sau.
1// assembly/array_ops.ts (AssemblyScript)
2// Square values in place in the wasm linear memory starting at `ptr` for `len` elements.
3export function square_in_place(ptr: usize, len: i32): void {
4 // Treat memory as a pointer to 32-bit integers.
5 for (let i = 0; i < len; i++) {
6 let offset = ptr + (i << 2); // i * 4 bytes
7 let value = load<i32>(offset);
8 store<i32>(offset, value * value);
9 }
10}Để chỉ định các thiết lập bộ nhớ được sử dụng bởi AssemblyScript, hãy chuẩn bị tệp asconfig.json sau.
1{
2 "options": {
3 "memoryBase": 0,
4 "importMemory": false,
5 "initialMemory": 1,
6 "maximumMemory": 10
7 }
8}1 npx asc assembly/array_ops.ts -o build/array_ops.wasm -t build/array_ops.wat --bindings esm --exportTable --sourceMap- Để gọi hàm này, bạn cần sao chép
ArrayBuffervào không gian bộ nhớ của Wasm và truyền con trỏ.
Dưới đây là ví dụ sử dụng WebAssembly.Memory trong TypeScript để sao chép dữ liệu và gọi hàm.
1// TypeScript: use memory to pass array to wasm
2async function runArrayOps() {
3 const res = await fetch('http://localhost:3000/build/array_ops.wasm');
4 const { instance } = await WebAssembly.instantiateStreaming(res, {});
5 // @ts-ignore
6 const memory: WebAssembly.Memory = instance.exports.memory;
7 // Create a view into wasm memory.
8 const i32View = new Int32Array(memory.buffer);
9
10 // Example data
11 const input = new Int32Array([1, 2, 3, 4]);
12 // Choose an offset (in i32 elements) to copy data to (simple example: at index 0).
13 const offset = 0;
14 i32View.set(input, offset);
15
16 // Call wasm function: ptr in bytes, len in elements
17 // @ts-ignore
18 instance.exports.square_in_place(offset * 4, input.length);
19
20 // Read back result
21 const result = i32View.slice(offset, offset + input.length);
22 console.log('squared:', result);
23}
24runArrayOps().catch(console.error);memory.bufferlà bộ nhớ tuyến tính được chia sẻ; giảm thiểu việc sao chép giúp tăng tốc độ xử lý tối đa. Cũng lưu ý rằng con trỏ tham chiếu đến vị trí theo byte, cònTypedArrayđược quản lý theo số phần tử, hãy cẩn thận không nhầm lẫn sự khác biệt này.
Xử lý an toàn kiểu dữ liệu: Chuẩn bị định nghĩa kiểu TypeScript
Các hàm Wasm xuất ra là đối tượng JavaScript, vì vậy cung cấp định nghĩa kiểu ở phía TypeScript sẽ giúp phát triển dễ dàng hơn. Dưới đây là một ví dụ đơn giản về tệp định nghĩa kiểu dữ liệu.
Dưới đây là định nghĩa kiểu tối thiểu bạn có thể tạo thủ công dưới tên simple.d.ts.
1// simple.d.ts
2export function add(a: number, b: number): number;
3export const memory: WebAssembly.Memory;- Đặt tệp này vào phần
typeRootstrongtsconfig.jsoncủa bạn hoặc sử dụngdeclare modulesẽ kích hoạt kiểm tra kiểu dữ liệu.wasm-packtự động tạo ra các tệp.d.tsrất tiện lợi, vì vậy hãy tận dụng chúng.
Các kiểu khởi tạo khi chạy: Đồng bộ vs Bất đồng bộ
Vì các mô-đun Wasm cần thao tác I/O (tải tệp) và biên dịch, nên việc khởi tạo bất đồng bộ là phổ biến. Tuy nhiên, cũng có trường hợp bạn lưu sẵn WebAssembly.Module và khởi tạo đồng bộ.
Dưới đây là cấu trúc mã cơ bản để khởi tạo WebAssembly bất đồng bộ. Trong các dự án thực tế, nên sử dụng kiểu này.
1// async init pattern
2async function initWasm(url: string) {
3 const res = await fetch(url);
4 const { instance, module } = await WebAssembly.instantiateStreaming(res, {});
5 return instance;
6}- Việc khởi tạo bất đồng bộ giúp dễ dàng bổ sung xử lý lỗi và lazy loading một cách linh hoạt, rất tiện lợi cho phát triển thực tế. Thêm nữa, mã sinh ra từ
wasm-packthường cung cấp APIinit()để khởi tạo, nên làm quen với luồng này sẽ giúp công việc của bạn thuận lợi hơn.
Những lưu ý thực tế về hiệu năng
Dưới đây là một số điểm cần chú ý để cải thiện hiệu năng đáng kể. Hãy tham khảo các mẹo tối ưu này khi kết hợp TypeScript và WebAssembly.
- Khi gọi hàm rất thường xuyên, chi phí chuyển đổi giữa JavaScript và Wasm có thể trở thành điểm nghẽn. Chúng tôi khuyên bạn nên gom dữ liệu và xử lý cùng lúc càng nhiều càng tốt.
- Việc cấp phát và sao chép bộ nhớ làm tăng gánh nặng xử lý. Nên sử dụng bộ đệm và con trỏ chia sẻ để giảm thiểu các thao tác này.
- Hãy cẩn thận khi xử lý số thực. Trong TypeScript, chúng trở thành kiểu
number, nhưng bạn có thể xử lý chính xác hơn bằng cách định kiểu tương ứng ở phía Wasm.
Tóm tắt
Bằng cách kết hợp TypeScript và WebAssembly, bạn có thể đạt được hiệu năng gần như bản địa trên trình duyệt. Điều này đặc biệt hiệu quả với các tác vụ đòi hỏi tính toán cao hoặc khi bạn muốn tận dụng các tài sản mã nguồn gốc hiện có. Sự kết hợp này là một lựa chọn rất mạnh mẽ khi bạn muốn nâng cao hiệu năng cho ứng dụng web của mình.
Bạn có thể làm theo bài viết trên bằng cách sử dụng Visual Studio Code trên kênh YouTube của chúng tôi. Vui lòng ghé thăm kênh YouTube.