Memaketkan resource non-JavaScript

Pelajari cara mengimpor dan memaketkan berbagai jenis aset dari JavaScript.

Misalnya, Anda sedang mengerjakan aplikasi web. Dalam hal ini, Anda mungkin tidak hanya harus menangani modul JavaScript, tetapi juga berbagai resource lainnya—Pekerja Web (yang juga merupakan JavaScript, tetapi bukan bagian dari grafik modul reguler), gambar, stylesheet, font, modul WebAssembly, dan lainnya.

Anda dapat menyertakan referensi ke beberapa resource tersebut secara langsung di HTML, tetapi sering kali referensi tersebut secara logis digabungkan dengan komponen yang dapat digunakan kembali. Misalnya, stylesheet untuk dropdown kustom yang terikat dengan bagian JavaScript-nya, gambar ikon yang terikat dengan komponen toolbar, atau modul WebAssembly yang terikat dengan lem JavaScript-nya. Dalam kasus tersebut, akan lebih mudah untuk mereferensikan resource langsung dari modul JavaScript-nya dan memuat resource secara dinamis saat (atau jika) komponen yang sesuai dimuat.

Grafik yang memvisualisasikan berbagai jenis aset yang diimpor ke JS.

Namun, sebagian besar project besar memiliki sistem build yang melakukan pengoptimalan tambahan dan pengaturan ulang konten—misalnya, penggabungan dan minifikasi. Mereka tidak dapat mengeksekusi kode dan memprediksi hasil eksekusi, serta tidak dapat menjelajahi setiap kemungkinan string literal di JavaScript dan menebak apakah itu URL resource atau bukan. Jadi, bagaimana Anda dapat membuatnya "melihat" aset dinamis yang dimuat oleh komponen JavaScript, dan menyertakannya dalam build?

Impor kustom di bundler

Salah satu pendekatan umum adalah menggunakan kembali sintaksis impor statis. Di beberapa alat penggabungan, format mungkin terdeteksi secara otomatis berdasarkan ekstensi file, sementara yang lain memungkinkan 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 bundler menemukan impor dengan ekstensi yang dikenali atau skema kustom eksplisit seperti itu (asset-url: dan js-url: dalam contoh di atas), plugin tersebut akan menambahkan aset yang dirujuk ke grafik build, menyalinnya ke tujuan akhir, melakukan pengoptimalan yang berlaku untuk jenis aset, dan menampilkan URL akhir yang akan digunakan selama runtime.

Manfaat pendekatan ini: menggunakan kembali sintaksis impor JavaScript menjamin bahwa semua URL bersifat statis dan relatif terhadap file saat ini, sehingga memudahkan sistem build untuk menemukan dependensi tersebut.

Namun, metode ini memiliki satu kelemahan yang signifikan: kode tersebut tidak dapat berfungsi langsung di browser, karena browser tidak tahu cara menangani skema atau ekstensi impor kustom tersebut. Hal ini mungkin tidak masalah jika Anda mengontrol semua kode dan mengandalkan bundler untuk pengembangan, tetapi semakin umum untuk menggunakan modul JavaScript langsung di browser, setidaknya selama pengembangan, untuk mengurangi hambatan. Seseorang yang mengerjakan demo kecil mungkin bahkan tidak memerlukan bundler sama sekali, bahkan dalam produksi.

Pola universal untuk browser dan bundler

Jika Anda sedang mengerjakan komponen yang dapat digunakan kembali, Anda ingin komponen tersebut berfungsi di salah satu lingkungan, baik digunakan langsung di browser maupun di-build sebelumnya sebagai bagian dari aplikasi yang lebih besar. Sebagian besar bundler modern memungkinkan hal ini dengan menerima pola berikut di modul JavaScript:

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

Pola ini dapat dideteksi secara statis oleh alat, hampir seperti sintaksis khusus, tetapi juga merupakan ekspresi JavaScript yang valid dan berfungsi langsung di browser.

Saat 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 bahas secara mendetail. Konstruktor new URL(...) menggunakan URL relatif sebagai argumen pertama dan me-resolve-nya terhadap URL absolut yang diberikan sebagai argumen kedua. Dalam kasus ini, argumen kedua adalah import.meta.url yang memberikan URL modul JavaScript saat ini, sehingga argumen pertama dapat berupa jalur apa pun yang relatif terhadapnya.

Tindakan ini memiliki kompromi yang serupa dengan impor dinamis. Meskipun Anda dapat menggunakan import(...) dengan ekspresi arbitrer seperti import(someUrl), penggabungan memberikan perlakuan khusus pada pola dengan URL statis import('./some-static-url.js') sebagai cara untuk memproses dependensi yang diketahui pada waktu kompilasi, tetapi memisahkannya menjadi 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) adalah sinyal yang jelas bagi bundler untuk memproses sebelumnya dan menyertakan dependensi bersama JavaScript utama.

URL relatif yang ambigu

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

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

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

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

Namun, fetch tidak mengetahui URL file JavaScript tempat file tersebut dieksekusi. Sebagai gantinya, fetch me-resolve URL secara 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 di-resolve secara relatif terhadap 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 modul WebAssembly yang diharapkan akan berhasil dimuat, serta memberi bundler cara untuk menemukan jalur relatif tersebut selama waktu build.

Dukungan alat

Penggabung

Berikut adalah bundler yang sudah mendukung skema new URL:

WebAssembly

Saat menggunakan WebAssembly, Anda biasanya tidak akan memuat modul Wasm secara manual, tetapi mengimpor glue JavaScript yang dikeluarkan oleh toolchain. Toolchain berikut dapat memunculkan pola new URL(...) yang dijelaskan di balik layar 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 akan menggunakan pola new URL(..., import.meta.url) di balik layar, sehingga penggabungan dapat menemukan file Wasm terkait secara otomatis.

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

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

Rust 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 mengandalkan proposal integrasi ESM WebAssembly. Saat ini, proposal ini masih bersifat eksperimental, dan output 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 akan menggunakan pola new URL(..., import.meta.url) yang dijelaskan, dan file Wasm juga akan otomatis ditemukan oleh bundler.

Jika Anda ingin menggunakan thread WebAssembly dengan Rust, ceritanya sedikit lebih rumit. Lihat bagian panduan yang sesuai 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 membuat Pekerja di Web. Perekat JavaScript yang digunakan oleh wasm-bindgen-rayon juga menyertakan pola new URL(...) di balik layar, sehingga Pekerja juga akan dapat ditemukan dan disertakan oleh bundler.

Fitur mendatang

import.meta.resolve

Panggilan import.meta.resolve(...) khusus adalah potensi peningkatan di masa mendatang. Hal ini akan memungkinkan penyelesaian pengonfigurasi secara relatif terhadap modul saat ini dengan cara yang lebih mudah, tanpa parameter tambahan:

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

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

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

Mengimpor pernyataan

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

foo.json:

{ "answer": 42 }

main.mjs:

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

Jenis ini juga dapat digunakan oleh bundler dan menggantikan kasus penggunaan yang saat ini tercakup dalam pola new URL, tetapi jenis dalam pernyataan impor ditambahkan berdasarkan kasus. Untuk saat ini, alat ini hanya mencakup JSON, dengan modul CSS yang akan segera hadir, tetapi jenis aset lainnya masih memerlukan solusi yang lebih umum.

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

Kesimpulan

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

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