Dowiedz się, jak importować i łączyć różne typy zasobów z JavaScriptu.
Załóżmy, że pracujesz nad aplikacją internetową. W takim przypadku najprawdopodobniej masz do czynienia nie tylko z modułami JavaScript, ale także z innymi rodzajami zasobów – komponentami Web Workers (które należą też do JavaScriptu, ale nie są częścią zwykłego wykresu w modułach), obrazami, arkuszami stylów, czcionkami, modułami WebAssembly i innymi.
Odniesienia do niektórych z tych zasobów można umieścić bezpośrednio w kodzie HTML, ale często są one logicznie powiązane z komponentami wielokrotnego użytku. Może to być na przykład arkusz stylów niestandardowego menu powiązanego z jej częścią w języku JavaScript, obrazy ikon powiązane z komponentem paska narzędzi czy moduł WebAssembly powiązany z klejem JavaScript. W takich przypadkach wygodniej jest odwoływać się do zasobów bezpośrednio z ich modułów JavaScript i wczytywać je dynamicznie podczas wczytywania odpowiedniego komponentu.
Większość dużych projektów korzysta jednak z systemów kompilacji, które wykonują dodatkowe optymalizacje i reorganizację treści, na przykład grupowanie i minifikacja. Nie mogą wykonać kodu i przewidywać wyniku jego wykonania, nie mogą też przemierzyć każdego możliwego literału ciągu w JavaScript i zgadnąć, czy URL zasobu odnosi się do URL-a zasobu, czy nie. Jak mogę więc sprawić, żeby „widzieli” te zasoby dynamiczne ładowane przez komponenty JavaScript i uwzględniały je w kompilacji?
Importy niestandardowe w pakietach
Jednym z typowych sposobów jest ponowne wykorzystanie składni statycznej importu. Niektóre pakiety mogą automatycznie wykrywać format na podstawie rozszerzenia pliku, a inne pozwalają wtyczkom używać niestandardowego schematu 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 pakietu SDK znajdzie import z rozpoznawanym rozszerzeniem lub z wyraźnym schematem niestandardowym (asset-url:
i js-url:
w przykładzie powyżej), dodaje ten zasób do wykresu kompilacji, kopiuje go do ostatecznego miejsca docelowego, przeprowadza optymalizacje odpowiednie dla typu zasobu i zwraca końcowy adres URL, który jest używany w czasie działania.
Zalety tego podejścia: ponowne wykorzystanie składni importu JavaScriptu gwarantuje, że wszystkie adresy URL są statyczne i powiązane z bieżącym plikiem, co ułatwia systemowi kompilacji znalezienie tego typu zależności.
Ma jednak jedną poważną wadę: taki kod nie może działać bezpośrednio w przeglądarce, ponieważ przeglądarka nie wie, jak obsłużyć niestandardowe schematy importu lub rozszerzenia. Może to nie być odpowiednie, jeśli kontrolujesz cały kod i i tak polegasz na programowaniu pakietów. Jednak coraz częściej staje się coraz bardziej powszechne używanie modułów JavaScript bezpośrednio w przeglądarce, a przynajmniej podczas programowania. Pozwala to zmniejszyć liczbę problemów. Ktoś, kto pracuje nad niewielką wersją demonstracyjną, może w ogóle nie potrzebować usługi pakietu – nawet w wersji produkcyjnej.
Uniwersalny wzorzec dla przeglądarek i pakietów
Jeśli pracujesz nad komponentem wielokrotnego użytku, powinien on działać w dowolnym środowisku – bezpośrednio w przeglądarce lub wstępnie utworzony jako część większej aplikacji. Większość współczesnych pakietów aplikacji umożliwia to, akceptując ten wzorzec w modułach JavaScript:
new URL('./relative-path', import.meta.url)
Narzędzia mogą wykryć ten wzorzec statycznie, niemal tak, jakby miał on specjalną składnię. Jest to jednak prawidłowe wyrażenie JavaScript, które działa bezpośrednio w przeglądarce.
W przypadku użycia tego wzorca powyższy przykład można napisać ponownie w takiej postaci:
// 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? Rozłóżmy to. Konstruktor new URL(...)
wykorzystuje jako pierwszy argument adres URL względny i przetwarza go w odniesieniu do bezwzględnego adresu URL podanego jako drugi argument. W naszym przypadku drugi argument to import.meta.url
, który podaje adres URL bieżącego modułu JavaScript, więc pierwszy argument może być dowolną ścieżką względem niego.
Ma podobne zalety do importu dynamicznego. Chociaż można używać import(...)
z dowolnymi wyrażeniami, takimi jak import(someUrl)
, pakiety SDK zapewniają specjalne traktowanie wzorca ze statycznym adresem URL import('./some-static-url.js')
, aby wstępnie przetworzyć zależność znaną w czasie kompilowania, ale dzieląc ją na własny fragment, który jest ładowany dynamicznie.
Analogicznie możesz używać polecenia new URL(...)
z dowolnymi wyrażeniami, takimi jak new URL(relativeUrl, customAbsoluteBase)
, jednak wzorzec new URL('...', import.meta.url)
stanowi wyraźną wskazówkę dla pakietów, które muszą wstępnie przetworzyć dane i uwzględnić zależność wraz z głównym kodem JavaScript.
Niejednoznaczne względne adresy URL
Być może zastanawiasz się, dlaczego usługi tworzące pakiety nie mogą wykryć innych typowych wzorców, na przykład fetch('./module.wasm')
bez kodów new URL
.
Wynika to z tego, że w przeciwieństwie do instrukcji importu wszystkie żądania dynamiczne są przetwarzane względem samego dokumentu, a nie 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 wczytać plik module.wasm
z platformy main.js
, warto użyć ścieżki względnej, takiej jak fetch('./module.wasm')
.
Jednak fetch
nie zna adresu URL pliku JavaScript, w którym jest wykonywany, ale rozpoznaje adresy URL względem dokumentu. W efekcie przeglądarka fetch('./module.wasm')
próbowałaby załadować http://example.com/module.wasm
zamiast zamierzonego http://example.com/src/module.wasm
, co zakończyłoby się niepowodzeniem (lub, co gorsza, dyskretnym załadowaniem zasobu innego niż zamierzony).
Pakowanie względnego adresu URL w tag new URL('...', import.meta.url)
pozwala uniknąć tego problemu i zagwarantować, że każdy z podanych adresów URL zostanie rozpoznany względem adresu URL bieżącego modułu JavaScript (import.meta.url
) przed przekazaniem go do modułów ładowania.
Zastąp fetch('./module.wasm')
wartością fetch(new URL('./module.wasm', import.meta.url))
, aby załadować oczekiwany moduł WebAssembly, a pakietom dasz też pakietom możliwość znalezienia tych ścieżek względnych podczas kompilacji.
Pomoc dotycząca narzędzi
Pakiety
Te pakiety SDK obsługują już schemat new URL
:
- Pakiet internetowy w wersji 5
- Podsumowanie (uzyskiwane za pomocą wtyczek – @web/rollup-plugin-import-meta-assets w przypadku zasobów ogólnych, a @surma/rollup-plugin-off-main-thread (tylko dla pracowników)
- Parcel v2 (beta)
- Vite
WebAssembly
Podczas pracy z WebAssembly zazwyczaj nie wczytuje się modułu Wasm ręcznie, a zamiast tego importuje klej JavaScript generowany przez łańcuch narzędzi. Poniższe łańcuchy narzędzi mogą wysyłać za Ciebie opisany wzór new URL(...)
.
C/C++ w Emscripten
Korzystając z Emscripten, możesz poprosić o emitowanie kleju JavaScript jako modułu ES6 zamiast zwykłego skryptu, korzystając z jednej 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
Gdy ta opcja jest włączona, dane wyjściowe używają wzorca new URL(..., import.meta.url)
, dzięki czemu osoby tworzące pakiety mogą automatycznie znaleźć powiązany plik Wasm.
Tej opcji możesz też użyć z wątkami 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 wygenerowana instancja robocza zostanie uwzględniona w taki sam sposób i będzie też możliwa do wykrycia zarówno przez programy, jak i przeglądarki.
Rdza przez wasm-pack / wam-bindgen
wasm-pack – podstawowy łańcuch narzędzi Rust dla WebAssembly – ma też kilka trybów wyjściowych.
Domyślnie emituje moduł JavaScript, który korzysta z propozycji integracji WebAssembly ESM. Obecnie ta oferta jest nadal w fazie eksperymentalnej i jej wyniki będą działać tylko po połączeniu z pakietem Webpack.
Zamiast tego możesz poprosić Wasm-pack o wyemitowanie przy użyciu --target web
modułu ES6 zgodnego z przeglądarką:
$ wasm-pack build --target web
Dane wyjściowe będą korzystać z opisanego wzorca new URL(..., import.meta.url)
, a plik Wasm będzie też automatycznie wykrywany przez systemy tworzenia pakietów.
Jeśli w aplikacji Rust chcesz używać wątków WebAssembly, cała historia jest nieco bardziej złożona. Więcej informacji znajdziesz w odpowiedniej sekcji tego przewodnika.
W skrócie mówimy, że nie można używać interfejsów API dowolnych wątków, ale jeśli używasz Rayona, możesz połączyć go z adapterem wasm-bindgen-rayon, aby umożliwić generowanie instancji roboczych w internecie. Klej JavaScript używany przez wasm-bindgen-rayon zawiera też znajdujący się poniżej wzorca new URL(...)
, dzięki czemu instancje robocze będą wykrywalne i uwzględniane przez pakiety.
Przyszłe funkcje
import.meta.resolve
Specjalne połączenie z usługą import.meta.resolve(...)
może być ulepszeniem w przyszłości. Umożliwiłoby to rozpoznawanie specyfikatorów względem bieżącego modułu w prostszy sposób, bez dodatkowych parametrów:
new URL('...', import.meta.url)
await import.meta.resolve('...')
Lepiej integruje się też z mapami importu i niestandardowymi resolverami, ponieważ przechodzi przez ten sam system rozpoznawania modułów co import
. Będzie to też silniejszy sygnał dla pakietów, ponieważ jest to składnia statyczna, która nie zależy od interfejsów API środowiska wykonawczego, takich jak URL
.
Zasób import.meta.resolve
został już wdrożony jako eksperyment w środowisku Node.js, ale wciąż istnieją pewne nierozwiązane pytania dotyczące jego działania w internecie.
Importowanie asercji
Potwierdzenia 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ą być też używane przez pakiety i zastąpić przypadki użycia objęte obecnie wzorcem new URL
, ale typy w asercjach importu są dodawane oddzielnie dla każdego przypadku. Na razie omawiamy tylko format JSON, a moduły CSS zostaną udostępnione wkrótce, ale inne rodzaje zasobów nadal będą wymagały bardziej ogólnego rozwiązania.
Przeczytaj wyjaśnienie funkcji w wersji v8.dev, aby dowiedzieć się więcej o tej funkcji.
Podsumowanie
Jak widać, jest wiele sposobów na uwzględnienie w internecie zasobów innych niż JavaScript, ale mają one różne wady i nie sprawdzają się w różnych łańcuchach narzędzi. Kolejne propozycje mogą umożliwić nam importowanie takich zasobów o specjalnej składni, ale to jeszcze nie koniec.
Do tego czasu wzorzec new URL(..., import.meta.url)
to najbardziej obiecujące rozwiązanie, które obecnie działa w przeglądarkach, różnych pakietach i łańcuchach narzędzi WebAssembly.