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

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

Giả sử bạn đang làm việc trên một ứng dụng web. Trong trường hợp đó, có thể bạn 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 – Trình chạy web (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, tệp kiểu, phông chữ, mô-đun WebAssembly và các tài nguyên khác.

Bạn có thể đưa các tệp tham chiếu đến một số tài nguyên đó trực tiếp vào HTML, nhưng thường thì các tệp tham chiếu này được ghép nối hợp lý với các thành phần có thể sử dụng lại. Ví dụ: một tệp kiểu cho trình đơn thả xuống tuỳ chỉnh liên kết với phần JavaScript, hình ảnh biểu tượng liên kết với thành phần thanh công cụ hoặc mô-đun WebAssembly liên kết với phần kết dính JavaScript. Trong những trường hợp đó, bạn nên tham chiếu trực tiếp các tài nguyên từ mô-đun JavaScript và tải các tài nguyên đó 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 tài sản đượ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 các hoạt động tối ưu hoá và sắp xếp lại nội dung bổ sung, ví dụ: gói và rút gọn. Các trình duyệt không thể thực thi mã và dự đoán kết quả thực thi, cũng không thể duyệt qua mọi giá trị cố định dạng 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 các thành phần này "nhìn thấy" các thành phần động do các thành phần JavaScript tải và đưa các thành phần đó vào bản dựng?

Lệnh nhập tuỳ chỉnh trong trình tạo 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 tạo gói, trình tạo gói có thể tự động phát hiện định dạng theo đuôi tệp, trong khi một số trình tạo gói khác cho phép trình bổ trợ sử dụng giao thứ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 một trình bổ trợ trình tạo gói tìm thấy một tệp nhập có một tiện ích mà trình bổ trợ đó nhận dạng hoặc một giao thức tuỳ chỉnh rõ ràng như vậy (asset-url:js-url: trong ví dụ ở trên), 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 của thành phần và trả về URL cuối cùng để 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à tương ứng với tệp hiện tại, giúp hệ thống xây dựng dễ dàng xác định các phần phụ thuộc đó.

Tuy nhiên, phương thức 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ể ổn nếu bạn kiểm soát tất cả mã và dựa vào trình tạo gói để phát triển. Tuy nhiên, việc sử dụng các mô-đun JavaScript trực tiếp trong trình duyệt ngày càng phổ biến, ít nhất là trong quá trình phát triển, để giảm sự cố. Những người làm việc trên một bản minh hoạ nhỏ có thể thậm chí không cần trình tạo gói, ngay cả khi phát hành công khai.

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 sử dụng trực tiếp trong trình duyệt hay được tạo sẵn như một phần của một ứng dụng lớn hơn. Hầu hết các trình tạo 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)

Các công cụ có thể phát hiện mẫu này một cách tĩnh, gần như thể đó là một cú pháp đặc biệt, nhưng đây 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 như sau:

// 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 Hãy cùng phân tích. Hàm khởi tạo new URL(...) lấy một URL tương đối làm đối số đầu tiên và phân giải URL đó theo một 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, 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 tương ứng với mô-đun đó.

Phương thức này có những đánh đổi tương tự như lệnh 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 trình đóng gói sẽ xử lý đặc biệt một mẫu có URL tĩnh import('./some-static-url.js') để xử lý trước một phần phụ thuộc đã biết tại thời điểm biên dịch, nhưng chia 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 để trình tạo gói xử lý trước và đưa một 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 tại sao trình đóng gói không thể phát hiện các mẫu phổ biến khác, ví dụ: fetch('./module.wasm') không có trình bao bọc new URL?

Lý do là không giống như câu lệnh nhập, mọi yêu cầu động đều được phân giải tương ứng với chính tài liệu chứ không phải với tệp JavaScript hiện tại. Giả sử bạn có cấu trúc 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 mà nó được thực thi, thay vào đó, fetch sẽ phân giải 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 dự kiến và không thành công (hoặc tệ hơn là âm thầm tải một tài nguyên khác với dự kiến).

Bằng cách 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 được cung cấp đều được phân giải tương ứng với URL của mô-đun JavaScript hiện tại (import.meta.url) trước khi được chuyển đến trình tải.

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

Hỗ trợ công cụ

Trình tạo gói

Các trình đóng gói sau đây đã hỗ trợ giao thức new URL:

WebAssembly

Khi làm việc với WebAssembly, bạn thường không tải mô-đun Wasm theo cách thủ công mà thay vào đó, hãy nhập chất kết dính 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ả trong phần nội dung.

C/C++ thông qua Emscripten

Khi sử dụng Emscripten, bạn có thể yêu cầu công cụ này phát hành chất kết dính JavaScript dưới dạng mô-đun ES6 thay vì 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, đầu ra sẽ sử dụng mẫu new URL(..., import.meta.url) trong nền để trình tạo gói có thể tự động tìm thấy tệp Wasm được liên kết.

Bạn cũng có thể sử 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, trình chạy web được tạo sẽ được đưa vào theo cùng một cách và trình đóng gói cũng như trình duyệt cũng có thể tìm thấy trình chạy web đó.

Rust thông 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, công cụ này sẽ phát ra một mô-đun JavaScript dựa trên đề xuất tích hợp WebAssembly ESM. Tại thời điểm viết bài, đề xuất này vẫn đang trong giai đoạn thử nghiệm và kết quả chỉ hoạt động khi được đóng gói bằng Webpack.

Thay vào đó, bạn có thể yêu cầu wasm-pack phát hành một mô-đun ES6 tương thích với trình duyệt thông 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ả và trình tạo gói cũng sẽ tự động phát hiện tệp Wasm.

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

Tóm lại, bạn không thể sử dụng 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 để API 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(...), vì vậy, các Worker cũng sẽ được phát hiện và đưa vào bởi trình kết hợp.

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

import.meta.resolve

Lệnh gọi import.meta.resolve(...) chuyên dụng là một điểm cải tiến tiềm năng trong tương lai. Điều này cho phép phân giải các chỉ định tương ứng với 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('...')

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

import.meta.resolve đã được triển khai dưới dạng một thử nghiệm trong Node.js nhưng vẫn còn một số câu hỏi chưa được giải đáp về cách hoạt động của import.meta.resolve 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 ngoài mô-đun ECMAScript. Hiện tại, các tệp này chỉ giới hạn ở 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 loại này và thay thế các trường hợp sử dụng hiện được bao phủ bởi mẫu new URL, nhưng các loại trong câu nhận định nhập sẽ được thêm vào theo từng trường hợp. Hiện tại, các thành phần này chỉ bao gồm JSON, với các mô-đun CSS sắp ra mắt, nhưng các loại thành phần khác vẫn sẽ cần một giải pháp chung hơn.

Hãy xem nội dung giải thích tính năng trên 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 vào web, nhưng các cách này đều có nhiều hạn chế và không hoạt động trên nhiều chuỗi công cụ. Các đề xuất trong tương lai có thể cho phép chúng ta nhập các thành phần đó bằng cú pháp chuyên biệt, nhưng chúng ta chưa thực hiện được điều đó.

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 đã hoạt động trong các trình duyệt, nhiều trình kết hợp và chuỗi công cụ WebAssembly.