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 liste, araç çubuğu bileşenine bağlı simge resimleri veya JavaScript yapışkanına bağlı WebAssembly modülü için stil sayfası. 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 aktarılan çeşitli öğe türlerini görselleştiren 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 bir yaklaşım, 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);

Paketleyici eklentisi, tanıdığı bir uzantı veya böyle açık bir özel şema (yukarıdaki örnekte asset-url: ve js-url:) bulunan bir içe aktarma işlemi bulduğunda, referans verilen öğeyi derleme grafiğine ekler, nihai hedefe kopyalar, öğenin türüne uygun optimizasyonlar gerçekleştirir ve çalışma zamanı boyunca 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 olmayabilir. Ancak, en azından geliştirme sırasında, 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ını oluşturmak için statik URL'ye sahip bir kalıba özel işlem uygular.

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 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 çözer. Bunun sonucunda 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 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 desteklemektedir:

WebAssembly

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

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ılırken çıkış, gelişmiş altındaki 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ında 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 İşçisi aynı şekilde eklenecek ve aynı zamanda paketleyiciler ve 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 öneri, 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.

Rust ile WebAssembly iş parçacıklarını 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.

Özet olarak, istediğiniz iş parçacığı API'lerini kullanamazsınız. Ancak Rayon kullanıyorsanız Web'de çalışan oluşturabilmesi için wasm-bindgen-rayon bağdaştırıcısı ile birleştirebilirsiniz. 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 keşfedilebilir ve paketleyiciler tarafından dahil edilebilir.

Gelecekte kullanıma sunulacak özellikler

import.meta.resolve

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

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ındaki API'lere bağlı olmayan statik bir söz dizimi olduğundan, paketleyiciler için de daha güçlü bir sinyal olacaktır.

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.

Onayları 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üme ihtiyaç duyulacak.

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 teklifler, 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.