Dowiedz się, jak importować i łączyć różne typy zasobów z języka JavaScript.
Przyjmijmy, że pracujesz nad aplikacją internetową. W takim przypadku prawdopodobnie musisz się zająć nie tylko modułami JavaScriptu, ale również wszystkimi innymi zasobami – z narzędziami Web Worker (które są też skryptami JavaScript, ale nie są częścią zwykłego wykresu modułu), obrazami, arkuszami stylów, czcionkami, modułami WebAssembly i innymi.
Można dołączać odwołania do niektórych z tych zasobów bezpośrednio w pliku HTML, ale często są one logicznie połączone z komponentami wielokrotnego użytku. Na przykład arkusz stylów dla niestandardowego menu rozwijanego powiązanego z jego częścią JavaScriptu, obrazy ikon powiązane ze składnikiem paska narzędzi lub moduł WebAssembly powiązany z elementem JavaScript. W takich przypadkach wygodniej jest odwoływać się do zasobów bezpośrednio z ich modułów JavaScript i ładować je dynamicznie podczas wczytywania odpowiedniego komponentu.
Jednak w większości dużych projektów stosowane są systemy kompilacji, które przeprowadzają dodatkowe optymalizacje i reorganizację treści, na przykład łączenie ich w pakiety i minifikację. Nie mogą wykonać kodu ani przewidzieć wyniku jego wykonania. Nie mogą też przejrzeć wszystkich możliwych ciągów znaków w JavaScript i zgadnąć, czy jest to adres URL zasobu. Jak więc sprawić, aby „widziały” zasoby dynamiczne wczytane przez komponenty JavaScript i włączały je w kompilację?
Importy niestandardowe w programach do tworzenia pakietów
Jednym z popularnych podejść jest ponowne użycie składni importu stałego. W niektórych modułach pakietów może on automatycznie wykrywać format na podstawie rozszerzenia pliku, podczas gdy w innych wtyczkach może używać niestandardowego schematu adresu URL, jak w tym przykładzie:
// 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);
Gdy wtyczka do tworzenia pakietów znajdzie import z rozszerzeniem, które rozpoznaje, lub z wyraźnym schematem niestandardowym (asset-url:
i js-url:
w przykładzie powyżej), dodaje do grafu kompilacji zasób, do którego się odwołuje, kopiuje go do docelowego miejsca, wykonuje optymalizacje odpowiednie dla typu zasobu i zwraca końcowy adres URL, który ma być używany w czasie wykonywania.
Zaleta tego podejścia: ponowne użycie składni importu JavaScriptu gwarantuje, że wszystkie adresy URL są statyczne i względne względem bieżącego pliku, co ułatwia systemowi kompilacji znalezienie takich zależności.
Ma on jednak jedną istotną wadę: nie działa bezpośrednio w przeglądarce, ponieważ przeglądarka nie wie, jak obsługiwać te niestandardowe schematy importowania ani rozszerzenia. Może to być dobre rozwiązanie, jeśli masz kontrolę nad całym kodem i i tak używasz pakietu do tworzenia aplikacji, ale coraz częściej, aby zmniejszyć trudności, moduły JavaScript są używane bezpośrednio w przeglądarce (przynajmniej podczas tworzenia aplikacji). Ktoś, kto pracuje nad małą wersją demonstracyjną, może w ogóle nie potrzebować narzędzia do tworzenia pakietów, nawet w wersji produkcyjnej.
Uniwersalny wzorzec dla przeglądarek i grup tworzących pakiety
Jeśli pracujesz nad komponentem wielokrotnego użytku, chcesz, aby działał w obu środowiskach, niezależnie od tego, czy jest używany bezpośrednio w przeglądarce, czy jest wstępnie utworzony jako część większej aplikacji. Większość współczesnych pakietów pozwala na to, akceptując ten wzór w modułach JavaScript:
new URL('./relative-path', import.meta.url)
Ten wzorzec może być wykrywany statycznie przez narzędzia, prawie tak jakby był specjalną składnią, ale jest to prawidłowy wyrażenie JavaScript, które działa bezpośrednio w przeglądarce.
Jeśli użyjesz tego wzorca, kod z przykładu powyżej można przepisać w taki sposób:
// 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));
Jak to działa? Przeanalizujmy to. Konstruktor new URL(...)
przyjmuje jako pierwszy argument adres URL względny i rozwiązuje go na podstawie adresu URL bezwzględnego podanego jako drugi argument. W naszym przypadku drugim argumentem jest import.meta.url
, który podaje adres URL bieżącego modułu JavaScript, więc pierwszym argumentem może być dowolna ścieżka względna.
Ma podobne wady i zalety jak import dynamiczny. Można użyć polecenia import(...)
z dowolnymi wyrażeniami, takimi jak import(someUrl)
, jednak pakiety pakietów traktują w szczególny sposób wzorzec ze statycznym adresem URL import('./some-static-url.js')
w celu wstępnego przetworzenia zależności znanej podczas kompilacji, a jednocześnie podzielają ją na własny fragment, który jest ładowany dynamicznie.
Podobnie możesz używać new URL(...)
z dowolnymi wyrażeniami, np. new URL(relativeUrl, customAbsoluteBase)
, ale wzór new URL('...', import.meta.url)
jest wyraźnym sygnałem dla twórców pakietów, aby przetworzyli i umieścili zależność obok głównego kodu JavaScript.
niejednoznaczne względne adresy URL,
Być może zastanawiasz się, dlaczego pakiety pakietów wykrywają inne typowe wzorce, np. fetch('./module.wasm')
bez kodów new URL
?
Dzieje się tak, ponieważ w przeciwieństwie do instrukcji importu wszelkie żądania dynamiczne są rozwiązywane w stosunku do samego dokumentu, a nie do bieżącego pliku JavaScript. Załóżmy, że masz taką strukturę:
index.html
:
html <script src="src/main.js" type="module"></script>
src/
main.js
module.wasm
Jeśli chcesz załadować module.wasm
z main.js
, możesz użyć ścieżki względnej, np. fetch('./module.wasm')
.
fetch
nie zna jednak adresu URL pliku JavaScript, w którym został wykonany, ale rozpoznaje adresy URL w stosunku do dokumentu. W rezultacie usługa fetch('./module.wasm')
próbowała wczytać http://example.com/module.wasm
zamiast zamierzonego http://example.com/src/module.wasm
, co zakończy się niepowodzeniem (lub, co gorsza, dyskretnie załadował inny zasób, niż zamierzony).
Owijając adres URL bezwzględny w funkcji new URL('...', import.meta.url)
, możesz uniknąć tego problemu i zapewnić, że każdy podany adres URL zostanie rozwiązany w stosunku do adresu URL bieżącego modułu JavaScript (import.meta.url
), zanim zostanie przekazany ładownikom.
Zastąp fetch('./module.wasm')
elementem fetch(new URL('./module.wasm', import.meta.url))
, co spowoduje wczytanie oczekiwanego modułu WebAssembly, a pakiety pakietów będą mogły znaleźć te ścieżki względne także w czasie kompilacji.
Pomoc dotycząca narzędzi
Programy pakujące
Te programy obsługują już schemat new URL
:
- Webpack w wersji 5
- Zbiorczy (osiągane za pomocą wtyczek: @web/rollup-plugin-import-meta-assets w przypadku zasobów ogólnych i @surma/rollup-plugin-off-main-thread w przypadku Workers).
- Parcel v2 (beta)
- Vite
WebAssembly
Podczas pracy z WebAssembly zwykle nie ładujesz ręcznie modułu Wasm, ale zamiast tego importujesz kod pośredniczący JavaScript generowany przez zestaw narzędzi. Poniższe łańcuchy narzędzi mogą emitować opisany wzorzec new URL(...)
dla Ciebie.
C/C++ za pomocą Emscripten
Używając Emscripten, możesz poprosić o wygenerowanie kodu JavaScript jako modułu ES6 zamiast zwykłego skryptu, korzystając z jednego z tych opcji:
$ 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
W przypadku użycia tej opcji dane wyjściowe będą używać w tabeli new URL(..., import.meta.url)
, aby umożliwić pakietownikom automatyczne znajdowanie powiązanego pliku Wasm.
Możesz też użyć tej opcji w wątkach WebAssembly, dodając flagę -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
W takim przypadku wygenerowany Web Worker zostanie uwzględniony w taki sam sposób i będzie widoczny zarówno dla pakietów, jak i przeglądarek.
Rust za pomocą wasm-pack / wasm-bindgen
Wasm-pack, czyli główny łańcuch narzędzi Rusta do WebAssembly, ma też kilka trybów wyjścia.
Domyślnie wygeneruje moduł JavaScript, który korzysta z propozycja integracji ESM WebAssembly. W chwili pisania ta propozycja jest nadal w fazie eksperymentalnej i dane wyjściowe będą działać tylko po połączeniu z pakietem Webpack.
Zamiast tego można poprosić pakiet Wasm-pack o wygenerowanie przez --target web
modułu ES6 zgodnego z przeglądarką:
$ wasm-pack build --target web
Dane wyjściowe będą używać opisanego wzoru new URL(..., import.meta.url)
, a plik Wasm będzie automatycznie wykrywany przez pakietatory.
Jeśli chcesz używać wątków WebAssembly z Rust, sprawa jest nieco bardziej skomplikowana. Więcej informacji znajdziesz w odpowiedniej sekcji przewodnika.
Krótko mówiąc, nie możesz używać dowolnych interfejsów wątków, ale jeśli używasz Rayon, możesz połączyć go z adapterem wasm-bindgen-rayon, aby mógł tworzyć pracowników w sieci. Klej JavaScript używany przez Wasm-bindgen-rayon zawiera również wzorzec new URL(...)
, dzięki czemu instancje robocze będą widoczne i uwzględniane przez pakiety tworzące pakiet.
Przyszłe funkcje
import.meta.resolve
Specjalne połączenie w usłudze import.meta.resolve(...)
może stanowić potencjalne ulepszenie. Umożliwi to rozwiązywanie wskaźników w stosunku do bieżącego modułu w bardziej bezpośredni sposób, bez dodatkowych parametrów:
new URL('...', import.meta.url)
await import.meta.resolve('...')
Lepiej też integrowałby się z mapami importu i niestandardowymi rozwiązalnikami, ponieważ korzystałby z tego samego systemu rozwiązywania modułów co import
. Byłby to też silniejszy sygnał dla dostawców pakietów, ponieważ jest to składnia statyczna, która nie zależy od interfejsów API w czasie wykonywania, takich jak URL
.
Rozszerzenie import.meta.resolve
zostało już wdrożone jako eksperyment w Node.js, ale nadal istnieją nierozwiązane pytania dotyczące jego działania w internecie.
Zasady importowania
Założenia importu to nowa funkcja, która umożliwia importowanie typów innych niż moduły ECMAScript. Obecnie są one ograniczone do formatu JSON:
foo.json:
{ "answer": 42 }
main.mjs:
import json from './foo.json' assert { type: 'json' };
console.log(json.answer); // 42
Mogą one być też używane przez dostawców pakietów i zastępować przypadki użycia obecnie obsługiwane przez wzór new URL
, ale typy w założeniach importu są dodawane w odpowiednich przypadkach. Na razie obejmują one tylko format JSON, a moduły CSS zostaną dodane wkrótce, ale inne rodzaje zasobów nadal będą wymagały bardziej ogólnego rozwiązania.
Aby dowiedzieć się więcej o tej funkcji, zapoznaj się z opisem funkcji na stronie v8.dev.
Podsumowanie
Jak widzisz, istnieją różne sposoby włączania zasobów innych niż JavaScript do witryny, ale mają one różne wady i nie działają w różnych zestawach narzędzi. W przyszłości być może będziemy mogli importować takie zasoby za pomocą specjalnej składni, ale na razie jeszcze tego nie robimy.
Do tego czasu wzór new URL(..., import.meta.url)
jest najbardziej obiecującym rozwiązaniem, które działa już w przeglądarkach, różnych programach do tworzenia pakietów i narzędzi WebAssembly.