JavaScript olmayan kaynakları paket haline getirme

JavaScript'ten çeşitli öğe türlerinin nasıl içe aktarılacağını ve gruplandırılacağını öğrenin.

İngvar Stepanyan
Ingvar Stepanyan

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

Bu kaynakların bazılarına doğrudan HTML içinde referanslar eklemek mümkündür, ancak çoğu zaman bunlar yeniden kullanılabilir bileşenlere mantıksal olarak bağlıdır. Örneğin, JavaScript bölümüne bağlı özel açılır liste için bir 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, kaynaklara doğrudan kendi JavaScript modüllerinden başvurmak ve karşılık gelen bileşen yüklendiğinde dinamik olarak bunları yüklemek daha uygundur.

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

Ancak çoğu büyük projede, paketleme ve küçültme gibi ek optimizasyonlar ve içeriğin yeniden düzenlenmesi gibi işlemler yapan derleme sistemleri bulunur. Kodu yürütemez ve yürütme sonucunun ne olacağını tahmin edemezler. JavaScript'te bütün olası dizeyi inceleyip gezinerek bunun bir kaynak URL'si olup olmadığını tahmin edemezler. Peki, bunların JavaScript bileşenleri tarafından yüklenen dinamik öğeleri "görmesini" ve derlemeye dahil etmesini nasıl sağlayabilirsiniz?

Paketleyicilerde özel içe aktarmalar

Yaygın yaklaşımlardan biri, statik içe aktarma söz diziminin yeniden kullanılmasıdır. Bazı paketleyicilerde biçimi dosya uzantısına göre otomatik olarak algılarken bazıları da eklentilerin aşağıdaki örnekte olduğu gibi özel URL şeması kullanmasına izin verebilir:

// 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ı veya böyle açık bir özel şemaya (yukarıdaki örnekte asset-url: ve js-url:) sahip bir içe aktarma işlemi bulduğunda başvurulan öğeyi yapı grafiğine ekler, son hedefe kopyalar, öğenin türüne uygun 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 diziminin yeniden kullanılması, tüm URL'lerin statik ve geçerli dosyaya göre olmasını garanti eder; bu da derleme sistemi için bu tür bağımlılıkların bulunmasını kolaylaştırır.

Ancak önemli bir dezavantajı vardır: Tarayıcı bu özel içe aktarma şemalarını veya uzantıları nasıl işleyeceğini bilmediğinden bu kod doğrudan tarayıcıda çalışmaz. Tüm kodu kontrol etmeniz ve geliştirme için bir paketleyici kullanmanız iyi olabilir. Ancak sorunları azaltmak için en azından geliştirme sırasında JavaScript modüllerini doğrudan tarayıcıda kullanmak giderek yaygınlaşmaktadır. Küçük bir demo üzerinde çalışan biri, üretim sırasında bile bir paketleyiciye bile ihtiyaç duymayabilir.

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

Yeniden kullanılabilir bir bileşen üzerinde çalışıyorsanız, ister doğrudan tarayıcıda kullanılmış ister daha büyük bir uygulamanın parçası olarak önceden oluşturulmuş olsun, her iki ortamda da çalışmasını istersiniz. Modern paketleyicilerin çoğu, JavaScript modüllerinde aşağıdaki kalıbı kabul ederek buna izin verir:

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

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

Bu kalıbı kullanırken 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 Biraz ayrıntıya girelim. new URL(...) oluşturucu, göreli URL'yi ilk bağımsız değişken olarak alır ve bunu, ikinci bağımsız değişken olarak sağlanan mutlak URL'ye karşı çözümler. Örneğimizde, ikinci bağımsız değişken mevcut JavaScript modülünün URL'sini veren import.meta.url öğesidir. Dolayısıyla, ilk bağımsız değişken kendisine göre herhangi bir yol olabilir.

Dinamik içe aktarma ile benzer ödünleşimlere sahiptir. import(...) öğesini import(someUrl) gibi rastgele ifadelerle kullanmak mümkün olsa da paketleyiciler, derleme sırasında bilinen bir bağımlılığı önceden işlemenin bir yolu olarak import('./some-static-url.js') statik URL'sine sahip bir kalıbı özel olarak ele alır. Bu kalıp, dinamik olarak yüklenen kendi parçasına ayırır.

Benzer şekilde, new URL(...) öğesini new URL(relativeUrl, customAbsoluteBase) gibi rastgele ifadelerle kullanabilirsiniz. Ancak new URL('...', import.meta.url) kalıbı, paketleyicilerin ön işleme koyması ve ana JavaScript ile birlikte bir bağımlılık eklemesi için açık 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 ifadelerinin aksine, tüm dinamik isteklerin geçerli JavaScript dosyasıyla değil, belgenin kendisine göre çözümlenmesidir. 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

main.js kaynağından module.wasm öğesini yüklemek istiyorsanız fetch('./module.wasm') gibi göreli bir 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 çözümler. Sonuç olarak fetch('./module.wasm'), istenen http://example.com/src/module.wasm yerine http://example.com/module.wasm öğesini yüklemeye çalışır ve başarısız olur (veya daha 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 herhangi bir URL'nin, mevcut JavaScript modülünün (import.meta.url) URL'sine göre çözümlenmesini garanti edebilirsiniz.

fetch('./module.wasm') öğesini fetch(new URL('./module.wasm', import.meta.url)) ile değiştirdiğinizde beklenen WebAssembly modülünü başarılı bir şekilde yüklemenin yanı sıra paketleyicilere derleme süresinde de bu göreli yolları bulma yolu sunulur.

Araç desteği

Paket Servis Makineleri

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

WebAssembly

WebAssembly ile çalışırken genellikle Wasm modülünü elle yüklemek yerine araç zincirinin yayınladığı JavaScript yapıştırıcısını içe aktarırsınız. Aşağıdaki araç zincirleri, arka planda açıklanan new URL(...) kalıbını sizin için oluşturabilir.

Emscripten üzerinden C/C++

Emscripten'i kullanırken, aşağıdaki seçeneklerden birini kullanarak normal bir komut dosyası yerine ES6 modülü olarak JavaScript yapıştırıcısı 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, çıkış arka plandaki new URL(..., import.meta.url) kalıbını kullanır. Böylece paketleyiciler ilişkili Wasm dosyasını otomatik olarak bulabilir.

Bu seçeneği -pthread işareti ekleyerek WebAssembly iş parçacıklarıyla da 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 Çalışanı aynı şekilde eklenir ve aynı şekilde paketleyiciler ve tarayıcılar tarafından da bulunabilir.

Wasm-pack / wasm-bindgen üzerinden pas

WebAssembly için temel Rust araç zinciri olan wasm-pack de birkaç çıkış moduna sahiptir.

Varsayılan olarak WebAssembly ESM entegrasyon teklifini temel alan bir JavaScript modülü yayınlar. Bu teklif yazıldığı sırada henüz deneme aşamasındadır ve çıkış yalnızca Webpack ile birlikte paketlendiğinde çalışacaktır.

Bunun yerine, wasm-pack'in --target web üzerinden tarayıcı uyumlu bir ES6 modülü oluşturmasını isteyebilirsiniz:

$ wasm-pack build --target web

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

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

Kısa versiyonda, rastgele iş parçacığı API'lerini kullanamıyorsunuz ancak Rayon kullanıyorsanız Web'de Workers (Çalışanlar) üretebilmek için bunu wasm-bindgen-rayon adaptörüyle birleştirebilirsiniz. wasm-bindgen-rayon tarafından kullanılan JavaScript yapıştırıcısı, arka planda new URL(...) kalıbını da içerir. Böylece Çalışanlar da bulunabilir ve paketleyiciler tarafından dahil edilir.

Gelecekteki özellikler

import.meta.resolve

import.meta.resolve(...) ile ilgili özel bir görüşme, ileride iyileştirilebilecek bir konudur. Bu, belirteçlerin mevcut modülle göreceli olarak, fazladan parametreler olmadan daha basit bir şekilde çözümlenmesine olanak tanır:

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

Ayrıca, import ile aynı modül çözünürlük sisteminden geçeceği için içe aktarma haritaları ve özel çözümleyicilerle daha iyi entegrasyon sağlar. 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 zaten Node.js'de bir deneme olarak uygulanmış, ancak web'de nasıl çalışması gerektiği konusunda hâlâ bazı çözümlenmemiş sorular var.

Onayları içe aktar

İçe aktarma onayları, ECMAScript modülleri dışındaki türlerin içe aktarılmasına olanak tanıyan yeni bir özelliktir. Şimdilik 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 new URL kalıbının kapsadığı kullanım alanlarının yerini alabilir ancak içe aktarma onaylarındaki türler her durum için ayrı ayrı eklenir. Şimdilik yalnızca JSON'u kapsamaktadır. CSS modülleri yakında kullanıma sunulacaktır. Ancak diğer öğe türleri için daha genel bir çözüm gerekecektir.

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

Sonuç

Gördüğünüz gibi, JavaScript olmayan kaynakları web'e eklemenin çeşitli yolları vardır, ancak bunların çeşitli dezavantajları vardır ve çeşitli araç zincirlerinde çalışmazlar. Gelecekteki teklifler, bu tür öğeleri özel söz dizimiyle içe aktarmamıza olanak tanıyabilir, ancak henüz o düzeyde değiliz.

O zamana kadar, new URL(..., import.meta.url) kalıbı günümüzde tarayıcılarda, çeşitli paketleyicilerde ve WebAssembly araç zincirlerinde işe yarayan ve en fazla umut vadeden çözümdür.