Memaketkan resource non-JavaScript

Pelajari cara mengimpor dan memaketkan berbagai jenis aset dari JavaScript.

Misalkan Anda sedang mengerjakan aplikasi web. Dalam kasus ini, kemungkinan Anda harus menangani tidak hanya modul JavaScript, tetapi juga dengan segala jenis resource lainnya—Web Worker (yang juga merupakan JavaScript, tetapi bukan bagian dari grafik modul reguler), gambar, stylesheet, font, modul WebAssembly, dan lain-lain.

Diperbolehkan untuk menyertakan referensi ke beberapa sumber daya tersebut langsung di HTML, tetapi seringkali referensi tersebut dikaitkan secara logis ke komponen yang dapat digunakan kembali. Misalnya, stylesheet untuk dropdown kustom yang terkait dengan bagian JavaScript-nya, gambar ikon yang terkait dengan komponen toolbar, atau modul WebAssembly yang terkait dengan perekat JavaScript-nya. Dalam kasus tersebut, akan lebih mudah untuk mereferensikan langsung dari modul JavaScript-nya dan memuatnya secara dinamis saat (atau jika) komponen yang sesuai dimuat.

Grafik yang memvisualisasikan berbagai jenis aset yang diimpor ke dalam JS.

Namun, sebagian besar project besar memiliki sistem build yang melakukan pengoptimalan tambahan dan penataan ulang konten, misalnya pemaketan dan minifikasi. Skrip ini tidak dapat mengeksekusi kode dan memprediksi hasil eksekusi, juga tidak dapat melewati setiap literal string yang mungkin dalam JavaScript dan membuat perkiraan apakah itu URL sumber daya atau bukan. Jadi, bagaimana cara membuat mereka "melihat" aset dinamis yang dimuat oleh komponen JavaScript, dan menyertakannya dalam build?

Impor kustom di pemaket

Salah satu pendekatan yang umum adalah menggunakan kembali sintaks impor statis. Di beberapa pemaket, pemaket mungkin otomatis mendeteksi format melalui ekstensi file, sementara yang lain mengizinkan plugin menggunakan skema URL kustom seperti dalam contoh berikut:

// 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);

Saat plugin pemaket menemukan impor dengan ekstensi yang dikenalinya atau skema kustom eksplisit (asset-url: dan js-url: dalam contoh di atas), plugin tersebut akan menambahkan aset yang direferensikan ke grafik build, menyalinnya ke tujuan akhir, melakukan pengoptimalan yang berlaku untuk jenis aset, dan menampilkan URL final yang akan digunakan selama runtime.

Manfaat pendekatan ini: menggunakan kembali sintaks impor JavaScript menjamin bahwa semua URL bersifat statis dan relatif terhadap file saat ini, yang membuat pencarian dependensi tersebut mudah bagi sistem build.

Namun, cara ini memiliki satu kelemahan signifikan: kode tersebut tidak dapat berfungsi secara langsung di browser, karena browser tidak tahu cara menangani skema atau ekstensi impor khusus tersebut. Mungkin tidak apa-apa jika Anda mengontrol semua kode dan tetap mengandalkan pemaket untuk pengembangan, tetapi penggunaan modul JavaScript secara langsung di browser akan semakin umum, setidaknya selama pengembangan, untuk mengurangi hambatan. Seseorang yang mengerjakan demo kecil mungkin tidak memerlukan pemaket sama sekali, bahkan dalam produksi.

Pola universal untuk browser dan pemaket

Jika Anda mengerjakan komponen yang dapat digunakan kembali, Anda ingin komponen tersebut berfungsi di salah satu lingkungan, baik yang digunakan langsung di browser maupun yang telah dibuat sebelumnya sebagai bagian dari aplikasi yang lebih besar. Sebagian besar pemaket modern mengizinkannya dengan menerima pola berikut di modul JavaScript:

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

Pola ini dapat dideteksi secara statis oleh alat, hampir seolah-olah merupakan sintaks khusus, namun pola ini juga merupakan ekspresi JavaScript valid yang berfungsi langsung di browser.

Jika menggunakan pola ini, contoh di atas dapat ditulis ulang sebagai:

// 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));

Bagaimana cara kerjanya? Mari kita pisahkan. Konstruktor new URL(...) menggunakan URL relatif sebagai argumen pertama dan me-resolve URL absolut yang diberikan sebagai argumen kedua. Dalam kasus kita, argumen kedua adalah import.meta.url yang memberikan URL modul JavaScript saat ini, sehingga argumen pertama dapat berupa jalur apa pun yang terkait dengannya.

Opsi ini memiliki konsekuensi yang mirip dengan impor dinamis. Meskipun Anda dapat menggunakan import(...) dengan ekspresi arbitrer seperti import(someUrl), pemaket memberikan perlakuan khusus pada pola dengan URL statis import('./some-static-url.js') sebagai cara untuk melakukan pra-pemrosesan dependensi yang diketahui pada waktu kompilasi, tetapi membaginya menjadi bagian-bagiannya sendiri yang dimuat secara dinamis.

Demikian pula, Anda dapat menggunakan new URL(...) dengan ekspresi arbitrer seperti new URL(relativeUrl, customAbsoluteBase), tetapi pola new URL('...', import.meta.url) merupakan sinyal yang jelas bagi pemaket untuk melakukan prapemrosesan dan menyertakan dependensi bersama JavaScript utama.

URL relatif ambigu

Anda mungkin bertanya-tanya, mengapa pemaket tidak dapat mendeteksi pola umum lainnya—misalnya, fetch('./module.wasm') tanpa wrapper new URL?

Alasannya adalah, tidak seperti pernyataan impor, setiap permintaan dinamis diselesaikan secara relatif ke dokumen itu sendiri, dan bukan ke file JavaScript saat ini. Katakanlah Anda memiliki struktur berikut:

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

Jika ingin memuat module.wasm dari main.js, Anda mungkin tergoda untuk menggunakan jalur relatif seperti fetch('./module.wasm').

Akan tetapi, fetch tidak mengetahui URL file JavaScript tempatnya dieksekusi, melainkan me-resolve URL relatif ke dokumen. Akibatnya, fetch('./module.wasm') akan mencoba memuat http://example.com/module.wasm, bukan http://example.com/src/module.wasm yang diinginkan, dan gagal (atau, lebih buruk lagi, memuat resource yang berbeda secara diam-diam dari yang Anda inginkan).

Dengan menggabungkan URL relatif ke dalam new URL('...', import.meta.url), Anda dapat menghindari masalah ini dan menjamin bahwa URL yang diberikan diselesaikan sesuai dengan URL modul JavaScript saat ini (import.meta.url) sebelum diteruskan ke loader apa pun.

Ganti fetch('./module.wasm') dengan fetch(new URL('./module.wasm', import.meta.url)) dan kode ini akan berhasil memuat modul WebAssembly yang diharapkan, serta memberi pemaket cara untuk menemukan jalur relatif tersebut selama waktu build.

Dukungan alat

Bundler

Pemaket berikut sudah mendukung skema new URL:

WebAssembly

Saat menggunakan WebAssembly, Anda biasanya tidak akan memuat modul Wasm secara manual, tetapi mengimpor glue JavaScript yang dimunculkan oleh toolchain. Toolchain berikut dapat memunculkan pola new URL(...) yang telah dijelaskan untuk Anda.

C/C++ melalui Emscripten

Saat menggunakan Emscripten, Anda dapat memintanya untuk memunculkan glue JavaScript sebagai modul ES6, bukan skrip biasa melalui salah satu opsi berikut:

$ 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

Saat menggunakan opsi ini, output-nya akan menggunakan pola new URL(..., import.meta.url) di balik layar, sehingga pemaket dapat menemukan file Wasm terkait secara otomatis.

Anda juga dapat menggunakan opsi ini dengan thread WebAssembly dengan menambahkan tanda -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

Dalam hal ini, Web Worker yang dihasilkan akan disertakan dengan cara yang sama dan juga dapat ditemukan oleh pemaket dan browser.

Karat melalui wasm-pack / wasm-bindgen

wasm-pack— toolchain Rust utama untuk WebAssembly—juga memiliki beberapa mode output.

Secara default, modul ini akan memunculkan modul JavaScript yang bergantung pada proposal integrasi ESM WebAssembly. Saat ini ditulis, proposal ini masih bersifat eksperimental, dan hasilnya hanya akan berfungsi jika dipaketkan dengan Webpack.

Sebagai gantinya, Anda dapat meminta wasm-pack untuk memunculkan modul ES6 yang kompatibel dengan browser melalui --target web:

$ wasm-pack build --target web

Output-nya akan menggunakan pola new URL(..., import.meta.url) yang dijelaskan, dan file Wasm juga akan ditemukan oleh pemaket secara otomatis.

Jika Anda ingin menggunakan thread WebAssembly dengan Rust, ceritanya akan sedikit lebih rumit. Lihat bagian yang sesuai dalam panduan untuk mempelajari lebih lanjut.

Versi singkatnya adalah Anda tidak dapat menggunakan API thread arbitrer, tetapi jika menggunakan Rayon, Anda dapat menggabungkannya dengan adaptor wasm-bindgen-rayon sehingga dapat menghasilkan Pekerja di Web. Lem JavaScript yang digunakan oleh wasm-bindgen-rayon juga menyertakan pola new URL(...) di balik layar, sehingga Pekerja juga dapat ditemukan dan disertakan oleh pemaket.

Fitur mendatang

import.meta.resolve

Panggilan import.meta.resolve(...) khusus adalah potensi peningkatan pada masa mendatang. Ini akan memungkinkan penyelesaian penentu relatif ke modul saat ini dengan cara yang lebih mudah, tanpa parameter tambahan:

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

API ini juga akan terintegrasi secara lebih baik dengan peta impor dan resolver kustom karena akan melalui sistem resolusi modul yang sama dengan import. Ini akan menjadi sinyal yang lebih kuat bagi pemaket karena ini merupakan sintaksis statis yang tidak bergantung pada API runtime seperti URL.

import.meta.resolve sudah diterapkan sebagai eksperimen di Node.js, tetapi masih ada beberapa pertanyaan yang belum terselesaikan tentang cara kerjanya di web.

Mengimpor pernyataan

Pernyataan impor adalah fitur baru yang memungkinkan impor jenis selain modul ECMAScript. Untuk saat ini, konfigurasi tersebut terbatas untuk JSON:

foo.json:

{ "answer": 42 }

main.mjs:

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

Atribut ini juga dapat digunakan oleh pemaket dan menggantikan kasus penggunaan yang saat ini tercakup oleh pola new URL, tetapi jenis pernyataan impor ditambahkan per kasus. Untuk saat ini, hal tersebut hanya mencakup JSON, dengan modul CSS akan segera hadir, tetapi jenis aset lainnya masih akan memerlukan solusi yang lebih umum.

Lihat penjelasan fitur v8.dev untuk mempelajari fitur ini lebih lanjut.

Kesimpulan

Seperti yang Anda lihat, ada berbagai cara untuk menyertakan resource non-JavaScript di web, tetapi cara tersebut memiliki berbagai kelemahan dan tidak berfungsi di berbagai toolchain. Proposal di masa mendatang mungkin akan memungkinkan kami mengimpor aset seperti itu dengan sintaksis khusus, tetapi kami belum sampai di sana.

Hingga saat itu, pola new URL(..., import.meta.url) adalah solusi paling menjanjikan yang sudah berfungsi di browser, berbagai pemaket, dan toolchain WebAssembly saat ini.