JavaScript olmayan kaynakları paket haline getirme

JavaScript'ten çeşitli öğe türlerini nasıl içe aktaracağınızı ve paketleyeceğinizi öğrenin.

Ingvar Stepanyan
Ingvar Stepanyan

Bir web uygulaması üzerinde çalıştığınızı varsayalım. Bu durumda, yalnızca JavaScript modülleriyle değil, aynı zamanda diğer her türlü kaynakla (Web Çalışanları da JavaScript'dir ancak normal modül grafiğinin bir parçası değildir), resimler, stil sayfaları, yazı tipleri, WebAssembly modülleri ve diğerleriyle de uğraşmanız gerekebilir.

Bu kaynakların bazılarının referanslarını doğrudan HTML'ye dahil etmek mümkündür ancak genellikle mantıksal olarak yeniden kullanılabilir bileşenlerle birleştirilir. Örneğin, JavaScript bölümüne bağlı özel bir açılır menünün stil sayfası, araç çubuğu bileşenine bağlı simge resimleri veya JavaScript yapıştırıcısına bağlı WebAssembly modülü. Bu gibi durumlarda, kaynakları doğrudan JavaScript modüllerinden referansla almak ve ilgili bileşen yüklendiğinde (veya yüklenirse) dinamik olarak yüklemek daha uygundur.

JS'ye içe aktarılan çeşitli öğe türlerini gösteren grafik.

Ancak büyük projelerin çoğunda, ek optimizasyonlar ve içeriklerin yeniden düzenlenmesini (ör. paketleme ve küçültme) gerçekleştiren derleme sistemleri bulunur. Kodu çalıştırıp yürütmenin sonucunu tahmin edemezler. Ayrıca JavaScript'teki her olası dize değişmez değerini tarayamaz ve bunun bir kaynak URL'si olup olmadığı hakkında tahminde bulunamazlar. Peki JavaScript bileşenleri tarafından yüklenen bu dinamik öğeleri nasıl "görebilir" ve derlemeye dahil edebilirsiniz?

Paketleyicilerde özel içe aktarma işlemleri

Yaygın olarak kullanılan yaklaşımlardan biri, statik içe aktarma söz dizimini yeniden kullanmaktır. Bazı paketleyicilerde biçim, dosya uzantısına göre otomatik olarak algılanabilir. Diğerlerinde ise eklentilerin aşağıdaki örnekteki gibi özel bir URL şeması kullanmasına izin verilir:

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

Bir paketleyici eklentisi, tanıdığı bir uzantıya veya bu tür bir açık özel şemaya (yukarıdaki örnekte asset-url: ve js-url:) sahip bir içe aktarma bulduğunda, başvurulan öğeyi derleme grafiğine ekler, nihai hedefe kopyalar, öğenin türü için geçerli optimizasyonları gerçekleştirir ve çalışma zamanında kullanılacak nihai URL'yi döndürür.

Bu yaklaşımın avantajları: JavaScript içe aktarma söz dizimini yeniden kullanmak, tüm URL'lerin statik ve geçerli dosyaya göreli olmasını sağlar. Bu da derleme sistemi için bu tür bağımlılıkları kolayca bulmanızı sağlar.

Ancak bu yöntemin önemli bir dezavantajı vardır: Tarayıcılar bu özel içe aktarma şemalarını veya uzantılarını nasıl işleyeceğini bilmediği için bu tür kodlar doğrudan tarayıcıda çalışamaz. Tüm kodu kontrol ediyorsanız ve geliştirme için zaten bir paketleyici kullanıyorsanız bu sorun yaşamayabilirsiniz. Ancak, en azından geliştirme sırasındaki karmaşıklığı azaltmak için JavaScript modüllerini doğrudan tarayıcıda kullanmak giderek daha yaygın hale geliyor. Küçük bir demo üzerinde çalışan bir kullanıcının, üretimde bile bir paketleyiciye ihtiyacı olmayabilir.

Tarayıcılar ve paketleyiciler için evrensel kalıp

Yeniden kullanılabilir bir bileşen üzerinde çalışıyorsanız bu bileşenin, doğrudan tarayıcıda kullanılsın veya daha büyük bir uygulamanın parçası olarak önceden derlenmiş olsun, her iki ortamda da çalışmasını istersiniz. Çoğu modern paketleyici, JavaScript modüllerinde aşağıdaki kalıbı kabul ederek buna olanak tanır:

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

Bu kalıp, neredeyse özel bir söz dizimiymiş gibi araçlar tarafından statik olarak algılanabilir. Ancak doğrudan tarayıcıda da çalışan geçerli bir JavaScript ifadesidir.

Bu kalıp kullanıldığında, yukarıdaki örnek şu şekilde yeniden yazılabilir:

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

İşleyiş şekli Bu konuyu ayrıntılı olarak inceleyelim. new URL(...) kurucusu, ilk bağımsız değişken olarak göreli bir URL alır ve bunu ikinci bağımsız değişken olarak sağlanan mutlak bir URL'ye göre çözer. Bizim durumumuzda ikinci bağımsız değişken, mevcut JavaScript modülünün URL'sini veren import.meta.url bağımsız değişkenidir. Bu nedenle, ilk bağımsız değişken, bu URL'ye göre herhangi bir yol olabilir.

Dinamik içe aktarma ile benzer avantaj ve dezavantajlara sahiptir. import(...)import(someUrl) gibi rastgele ifadelerle kullanmak mümkün olsa da paketleyiciler, derleme zamanında bilinen bir bağımlılığı ön işleme almak ve aynı zamanda dinamik olarak yüklenen kendi parçasına ayırmak için statik URL'ye sahip bir kalıba özel işlem uygular.import('./some-static-url.js')

Benzer şekilde, new URL(...)new URL(relativeUrl, customAbsoluteBase) gibi rastgele ifadelerle kullanabilirsiniz. Ancak new URL('...', import.meta.url) kalıbı, paketleyicilerin ön işlem yapması ve ana JavaScript'in yanına bir bağımlılık eklemesi için net bir sinyaldir.

Belirsiz göreli URL'ler

Paketleyicilerin neden diğer yaygın kalıpları (ör. new URL sarmalayıcıları olmadan fetch('./module.wasm')) algılayamadığını merak ediyor olabilirsiniz.

Bunun nedeni, içe aktarma ifadelerinden farklı olarak tüm dinamik isteklerin geçerli JavaScript dosyasına göre değil, dokümanın kendisine göre çözülmesidir. Aşağıdaki yapıya sahip olduğunuzu varsayalım:

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

module.wasm dosyasını main.js dosyasından yüklemek istiyorsanız fetch('./module.wasm') gibi bir göreli yol kullanmak cazip gelebilir.

Ancak fetch, yürütüldüğü JavaScript dosyasının URL'sini bilmez. Bunun yerine, URL'leri dokümana göre çözer. Sonuç olarak fetch('./module.wasm'), amaçlanan http://example.com/src/module.wasm yerine http://example.com/module.wasm yüklemeye çalışır ve başarısız olur (veya daha da kötüsü, amaçladığınızdan farklı bir kaynağı sessizce yükler).

Göreli URL'yi new URL('...', import.meta.url) içine sarmalayarak bu sorunu önleyebilir ve sağlanan URL'nin, herhangi bir yükleyiciye iletilmeden önce geçerli JavaScript modülünün URL'sine göre çözülmesini sağlayabilirsiniz (import.meta.url).

fetch('./module.wasm') yerine fetch(new URL('./module.wasm', import.meta.url)) yazarsanız beklenen WebAssembly modülü başarıyla yüklenir ve paketleyicilere derleme sırasında bu göreli yolları bulmaları için bir yol sağlanır.

Araç desteği

Paketleyiciler

Aşağıdaki paketleyiciler new URL şemasını zaten destekliyor:

WebAssembly

WebAssembly ile çalışırken genellikle Wasm modülünü manuel olarak yüklemezsiniz. Bunun yerine, araç zinciri tarafından yayınlanan JavaScript yapıştırıcısını içe aktarırsınız. Aşağıdaki araç zincirleri, açıklanan new URL(...) desenini sizin için arka planda yayınlayabilir.

Emscripten aracılığıyla C/C++

Emscripten'i kullanırken aşağıdaki seçeneklerden birini kullanarak JavaScript yapıştırıcısını normal bir komut dosyası yerine ES6 modülü olarak yayınlamasını isteyebilirsiniz:

$ 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

Bu seçenek kullanıldığında, paketleyicilerin ilişkili Wasm dosyasını otomatik olarak bulabilmesi için çıkışta new URL(..., import.meta.url) kalıbı kullanılır.

Bu seçeneği, -pthread işareti ekleyerek WebAssembly iş parçacıkları ile de kullanabilirsiniz:

$ 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

Bu durumda, oluşturulan Web İşleyici aynı şekilde dahil edilir ve hem paketleyiciler hem de tarayıcılar tarafından bulunabilir.

wasm-pack / wasm-bindgen aracılığıyla Rust

WebAssembly için birincil Rust araç zinciri olan wasm-pack'in de çeşitli çıkış modları vardır.

Varsayılan olarak, WebAssembly ESM entegrasyon önerisine dayalı bir JavaScript modülü yayınlar. Bu teklif, yazının yazıldığı sırada hâlâ deneme aşamasındadır ve çıkış yalnızca Webpack ile birlikte kullanıldığında çalışır.

Bunun yerine, wasm-pack'ten --target web aracılığıyla tarayıcı uyumlu bir ES6 modülü yayınlamasını isteyebilirsiniz:

$ wasm-pack build --target web

Çıkışta açıklanan new URL(..., import.meta.url) kalıbı kullanılır ve Wasm dosyası, paketleyiciler tarafından da otomatik olarak bulunur.

WebAssembly ile Rust'ta iş parçacıkları kullanmak istiyorsanız durum biraz daha karmaşıktır. Daha fazla bilgi edinmek için kılavuzun ilgili bölümüne göz atın.

Özet olarak, istediğiniz iş parçacığı API'lerini kullanamazsınız. Ancak Rayon kullanıyorsanız bunu wasm-bindgen-rayon bağdaştırıcısı ile birleştirerek Web'de çalışan oluşturabilirsiniz. wasm-bindgen-rayon tarafından kullanılan JavaScript yapıştırıcısı, new URL(...) kalıbını da içerir. Bu nedenle, işçiler de paketleyiciler tarafından bulunabilir ve dahil edilebilir.

Gelecekteki özellikler

import.meta.resolve

Özel bir import.meta.resolve(...) görüşmesi, gelecekte yapılacak olası bir iyileştirmedir. Bu, ek parametreler olmadan belirteci geçerli modüle göre daha basit bir şekilde çözmenize olanak tanır:

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

Ayrıca import ile aynı modül çözümleme sisteminden geçeceği için içe aktarma haritaları ve özel çözümleyicilerle daha iyi entegre olur. URL gibi çalışma zamanı API'lerine bağlı olmayan statik bir söz dizimi olduğundan paketleyiciler için de daha güçlü bir sinyal olur.

import.meta.resolve, Node.js'te deneme olarak uygulanmış olsa da web'de nasıl çalışması gerektiğiyle ilgili bazı çözüme ulaştırılmamış sorular hâlâ var.

İddiaları içe aktarma

İçe aktarma iddiaları, ECMAScript modülleri dışındaki türlerin içe aktarılmasına olanak tanıyan yeni bir özelliktir. Şu anda yalnızca JSON ile sınırlıdır:

foo.json:

{ "answer": 42 }

main.mjs:

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

Paketleyiciler tarafından da kullanılabilir ve şu anda new URL kalıbıyla kapsanan kullanım alanlarını değiştirebilirler ancak içe aktarma beyanlarında türler her duruma göre eklenir. Şu anda yalnızca JSON'u kapsayan bu modüller yakında CSS modüllerini de içerecek. Ancak diğer öğe türleri için daha genel bir çözüm gerekecek.

Bu özellik hakkında daha fazla bilgi edinmek için v8.dev özellik açıklamalarına göz atın.

Sonuç

Gördüğünüz gibi, web'e JavaScript dışındaki kaynakları dahil etmenin çeşitli yolları vardır ancak bunların çeşitli dezavantajları vardır ve çeşitli araç zincirlerinde çalışmaz. Gelecekteki öneriler, bu tür öğeleri özel söz dizimi ile içe aktarmamıza olanak tanıyabilir ancak henüz bu aşamaya gelmedik.

O zamana kadar new URL(..., import.meta.url) kalıbı, tarayıcılarda, çeşitli paketleyicilerde ve WebAssembly araç zincirlerinde halihazırda çalışan en umut verici çözümdür.