Nhóm các tài nguyên không phải JavaScript

Tìm hiểu cách nhập và nhóm nhiều loại thành phần từ JavaScript.

Tiếng Ingvar Stepanyan
Tiếng Ingvar Stepanyan

Giả sử bạn đang xử lý một ứng dụng web. Trong trường hợp đó, bạn có thể không chỉ phải xử lý các mô-đun JavaScript mà còn phải xử lý tất cả các loại tài nguyên khác – Web Worker (cũng là JavaScript, nhưng không phải là một phần của biểu đồ mô-đun thông thường), hình ảnh, biểu định kiểu, phông chữ, mô-đun WebAssembly và những tài nguyên khác.

Có thể bao gồm trực tiếp các tham chiếu đến một số tài nguyên đó trong HTML, nhưng chúng thường được kết hợp một cách hợp lý với các thành phần có thể sử dụng lại. Ví dụ: biểu định kiểu cho một trình đơn thả xuống tuỳ chỉnh gắn với phần JavaScript, hình ảnh biểu tượng gắn với thành phần thanh công cụ hoặc mô-đun WebAssembly được liên kết với keo JavaScript. Trong các trường hợp đó, sẽ thuận tiện hơn nếu bạn tham chiếu các tài nguyên trực tiếp từ các mô-đun JavaScript của chúng và tải chúng một cách linh động khi (hoặc nếu) thành phần tương ứng được tải.

Biểu đồ trực quan hoá nhiều loại thành phần kết hợp được nhập vào JS.

Tuy nhiên, hầu hết các dự án lớn đều có hệ thống xây dựng thực hiện việc tối ưu hoá thêm và sắp xếp lại nội dung, ví dụ: nhóm và giảm thiểu. Họ không thể thực thi mã và dự đoán kết quả thực thi cũng như không thể truyền tải mọi giá trị cố định kiểu chuỗi có thể có trong JavaScript và đoán xem đó có phải là URL tài nguyên hay không. Vậy làm cách nào để bạn có thể khiến chúng "thấy" những thành phần động được tải bởi thành phần JavaScript và đưa chúng vào bản dựng?

Nhập tuỳ chỉnh trong trình đóng gói

Một phương pháp phổ biến là sử dụng lại cú pháp nhập tĩnh. Trong một số trình đóng gói, tính năng này có thể tự động phát hiện định dạng theo đuôi tệp, trong khi một số trình bổ trợ khác cho phép trình bổ trợ sử dụng lược đồ URL tuỳ chỉnh như trong ví dụ sau:

// regular JavaScript import
import { loadImg } from './utils.js';

// special "URL imports" for assets
import imageUrl from 'asset-url:./image.png';
import wasmUrl from 'asset-url:./module.wasm';
import workerUrl from 'js-url:./worker.js';

loadImg(imageUrl);
WebAssembly.instantiateStreaming(fetch(wasmUrl));
new Worker(workerUrl);

Khi tìm thấy một lượt nhập có tiện ích mà trình bổ trợ nhận dạng được hoặc một lược đồ tuỳ chỉnh rõ ràng (trong ví dụ trên là asset-url:js-url:), trình bổ trợ sẽ thêm thành phần được tham chiếu vào biểu đồ bản dựng, sao chép thành phần đó vào đích đến cuối cùng, thực hiện các hoạt động tối ưu hoá áp dụng cho loại thành phần và trả về URL cuối cùng sẽ được sử dụng trong thời gian chạy.

Lợi ích của phương pháp này: việc sử dụng lại cú pháp nhập JavaScript đảm bảo rằng tất cả URL đều tĩnh và có liên quan đến tệp hiện tại, giúp hệ thống xây dựng dễ dàng định vị các phần phụ thuộc đó.

Tuy nhiên, mã này có một hạn chế đáng kể: mã như vậy không thể hoạt động trực tiếp trong trình duyệt, vì trình duyệt không biết cách xử lý các lược đồ hoặc tiện ích nhập tuỳ chỉnh đó. Điều này có thể tốt nếu bạn kiểm soát tất cả mã và vẫn dựa vào trình đóng gói để phát triển, nhưng việc sử dụng các mô-đun JavaScript trực tiếp trong trình duyệt, ít nhất là trong quá trình phát triển, để giảm sự phiền hà ngày càng phổ biến. Những người xử lý bản minh hoạ nhỏ có thể thậm chí không cần đến bộ gói, ngay cả trong quá trình sản xuất.

Mẫu chung cho trình duyệt và trình đóng gói

Nếu đang làm việc trên một thành phần có thể sử dụng lại, bạn sẽ muốn thành phần đó hoạt động trong một trong hai môi trường, cho dù thành phần đó được dùng trực tiếp trong trình duyệt hay được tạo sẵn trong một ứng dụng lớn hơn. Hầu hết các trình đóng gói hiện đại đều cho phép điều này bằng cách chấp nhận mẫu sau trong các mô-đun JavaScript:

new URL('./relative-path', import.meta.url)

Mẫu này có thể được phát hiện tĩnh bằng công cụ, gần như thể đó là một cú pháp đặc biệt, tuy nhiên nó cũng là một biểu thức JavaScript hợp lệ hoạt động trực tiếp trong trình duyệt.

Khi sử dụng mẫu này, bạn có thể viết lại ví dụ trên thành:

// regular JavaScript import
import { loadImg } from './utils.js';

loadImg(new URL('./image.png', import.meta.url));
WebAssembly.instantiateStreaming(
  fetch(new URL('./module.wasm', import.meta.url)),
  { /* … */ }
);
new Worker(new URL('./worker.js', import.meta.url));

Cách thức hoạt động Chúng ta kết thúc nhé. Hàm khởi tạo new URL(...) lấy URL tương đối làm đối số đầu tiên và phân giải URL này dựa trên URL tuyệt đối được cung cấp làm đối số thứ hai. Trong trường hợp của chúng ta, đối số thứ hai là import.meta.url. Đối số này cung cấp URL của mô-đun JavaScript hiện tại, vì vậy, đối số đầu tiên có thể là bất kỳ đường dẫn nào liên quan đến mô-đun đó.

Tính năng này có ưu điểm tương tự như tính năng nhập động. Mặc dù có thể sử dụng import(...) với các biểu thức tuỳ ý như import(someUrl), nhưng các trình đóng gói vẫn xử lý đặc biệt mẫu có URL tĩnh import('./some-static-url.js') như một cách để xử lý trước một phần phụ thuộc được biết đến tại thời điểm biên dịch, nhưng tách phần phụ thuộc đó thành một phần riêng được tải động.

Tương tự, bạn có thể sử dụng new URL(...) với các biểu thức tuỳ ý như new URL(relativeUrl, customAbsoluteBase), nhưng mẫu new URL('...', import.meta.url) là một tín hiệu rõ ràng để các trình đóng gói xử lý trước và đưa phần phụ thuộc vào cùng với JavaScript chính.

URL tương đối không rõ ràng

Bạn có thể thắc mắc rằng tại sao các trình đóng gói không thể phát hiện các mẫu phổ biến khác, chẳng hạn như fetch('./module.wasm') nếu không có trình bao bọc new URL?

Lý do là, không giống như các câu lệnh nhập, mọi yêu cầu động đều được giải quyết tương đối với chính tài liệu chứ không phải cho tệp JavaScript hiện tại. Giả sử bạn có cấu trúc như sau:

  • index.html:
    html <script src="src/main.js" type="module"></script>
  • src/
    • main.js
    • module.wasm

Nếu muốn tải module.wasm từ main.js, bạn có thể sử dụng đường dẫn tương đối như fetch('./module.wasm').

Tuy nhiên, fetch không biết URL của tệp JavaScript được thực thi, thay vào đó sẽ phân giải các URL tương ứng với tài liệu. Do đó, fetch('./module.wasm') sẽ cố gắng tải http://example.com/module.wasm thay vì http://example.com/src/module.wasm như dự định và không thành công (hoặc tệ hơn là tự tải một tài nguyên khác với dự định của bạn).

Bằng việc gói URL tương đối vào new URL('...', import.meta.url), bạn có thể tránh được vấn đề này và đảm bảo rằng mọi URL đã cung cấp đều được giải quyết tương ứng với URL của mô-đun JavaScript hiện tại (import.meta.url) trước khi nó được truyền cho bất kỳ trình tải nào.

Thay thế fetch('./module.wasm') bằng fetch(new URL('./module.wasm', import.meta.url)) và nó sẽ tải thành công mô-đun WebAssembly dự kiến, đồng thời cung cấp cho các trình đóng gói cách tìm các đường dẫn tương đối đó trong thời gian tạo bản dựng.

Hỗ trợ công cụ

Bundler

Các trình đóng gói sau đây đã hỗ trợ lược đồ new URL:

WebAssembly

Khi làm việc với WebAssembly, bạn thường sẽ không tải mô-đun Wasm theo cách thủ công mà thay vào đó hãy nhập keo JavaScript do chuỗi công cụ phát ra. Các chuỗi công cụ sau đây có thể phát ra mẫu new URL(...) được mô tả giúp bạn.

C/C++ qua Emscripten

Khi sử dụng Emscripten, bạn có thể yêu cầu nó phát ra keo JavaScript dưới dạng mô-đun ES6 thay vì một tập lệnh thông thường thông qua một trong các tuỳ chọn sau:

$ emcc input.cpp -o output.mjs
## or, if you don't want to use .mjs extension
$ emcc input.cpp -o output.js -s EXPORT_ES6

Khi sử dụng tuỳ chọn này, dữ liệu đầu ra sẽ sử dụng mẫu new URL(..., import.meta.url) nâng cao để các trình đóng gói có thể tự động tìm thấy tệp Wasm được liên kết.

Bạn cũng có thể dùng tuỳ chọn này với luồng WebAssembly bằng cách thêm cờ -pthread:

$ emcc input.cpp -o output.mjs -pthread
## or, if you don't want to use .mjs extension
$ emcc input.cpp -o output.js -s EXPORT_ES6 -pthread

Trong trường hợp này, Web Worker được tạo sẽ được đưa vào cùng một kiểu và cũng sẽ có thể được phát hiện bởi trình đóng gói cũng như trình duyệt.

Gỉ sét qua wasm-pack / wasm-bindgen

wasm-pack (chuỗi công cụ Rust chính cho WebAssembly) cũng có một số chế độ đầu ra.

Theo mặc định, nó sẽ phát ra mô-đun JavaScript dựa trên đề xuất tích hợp ESM WebAssembly. Tại thời điểm viết, đề xuất này vẫn đang trong giai đoạn thử nghiệm và kết quả sẽ chỉ hoạt động khi được đóng gói với Webpack.

Thay vào đó, bạn có thể yêu cầu wasm-pack phát ra mô-đun ES6 tương thích với trình duyệt qua --target web:

$ wasm-pack build --target web

Kết quả sẽ sử dụng mẫu new URL(..., import.meta.url) được mô tả, đồng thời tệp Wasm cũng sẽ được các trình đóng gói tự động phát hiện.

Nếu bạn muốn sử dụng các luồng WebAssembly với Rust, thì câu chuyện sẽ phức tạp hơn một chút. Vui lòng xem phần tương ứng của hướng dẫn để tìm hiểu thêm.

Phiên bản ngắn là bạn không thể sử dụng các API luồng tuỳ ý, nhưng nếu sử dụng Rayon, bạn có thể kết hợp API này với bộ chuyển đổi wasm-bindgen-rayon để có thể tạo Worker trên web. Keo JavaScript mà wasm-bindgen-rayon sử dụng cũng bao gồm mẫu new URL(...) nâng cao, vì vậy, các trình đóng gói cũng sẽ có thể phát hiện và đưa Worker này vào.

Các tính năng trong tương lai

import.meta.resolve

Lệnh gọi import.meta.resolve(...) chuyên biệt là một điểm cải tiến tiềm năng trong tương lai. Việc này sẽ cho phép phân giải các thông số kỹ thuật tương ứng cho mô-đun hiện tại theo cách đơn giản hơn mà không cần thêm tham số:

new URL('...', import.meta.url)
await import.meta.resolve('...')

Thư viện này cũng tích hợp tốt hơn với bản đồ nhập và trình phân giải tuỳ chỉnh vì nó truyền qua cùng một hệ thống phân giải mô-đun như import. Đây cũng sẽ là tín hiệu mạnh mẽ hơn cho các trình đóng gói vì đó là cú pháp tĩnh không phụ thuộc vào API thời gian chạy như URL.

import.meta.resolve đã được triển khai dưới dạng thử nghiệm trong Node.js nhưng vẫn còn một số câu hỏi chưa được giải quyết về cách hoạt động của thử nghiệm trên web.

Nhập câu nhận định

Xác nhận nhập là một tính năng mới cho phép nhập các loại khác với mô-đun ECMAScript. Hiện tại, các định dạng này chỉ giới hạn ở định dạng JSON:

foo.json:

{ "answer": 42 }

main.mjs:

import json from './foo.json' assert { type: 'json' };
console.log(json.answer); // 42

Các trình đóng gói cũng có thể sử dụng các hàm này và thay thế các trường hợp sử dụng hiện có trong mẫu new URL, nhưng các loại trong câu nhận định nhập được thêm vào theo từng trường hợp. Hiện tại, các mô-đun này chỉ bao gồm JSON. Trong khi đó, các mô-đun CSS sẽ sớm ra mắt, nhưng các loại tài sản khác vẫn cần một giải pháp tổng quát.

Hãy xem phần giải thích về tính năng v8.dev để tìm hiểu thêm về tính năng này.

Kết luận

Như bạn có thể thấy, có nhiều cách để đưa các tài nguyên không phải JavaScript lên web. Tuy nhiên, chúng có nhiều hạn chế và không hoạt động trên các chuỗi công cụ khác nhau. Các đề xuất trong tương lai có thể cho phép chúng tôi nhập những tài sản như vậy với cú pháp chuyên biệt, nhưng chúng tôi chưa hoàn tất.

Cho đến thời điểm đó, mẫu new URL(..., import.meta.url) là giải pháp hứa hẹn nhất hiện nay đã hoạt động trong các trình duyệt, nhiều trình đóng gói và chuỗi công cụ WebAssembly hiện nay.