تجميع موارد لا تعتمد على JavaScript

تعرَّف على كيفية استيراد أنواع مختلفة من مواد العرض وتجميعها من JavaScript.

لنفترض أنّك تعمل على تطبيق ويب. في هذه الحالة، من المرجّح أنّك لن تتعامل فقط مع وحدات JavaScript، بل مع جميع أنواع الموارد الأخرى، مثل Web Workers (وهي أيضًا JavaScript، ولكنها ليست جزءًا من الرسم البياني العادي للوحدات) والصور وأوراق الأنماط والخطوط ووحدات WebAssembly وغيرها.

من الممكن تضمين إشارات إلى بعض هذه الموارد مباشرةً في رمز HTML، ولكن غالبًا ما تكون مرتبطة منطقيًا بمكونات قابلة لإعادة الاستخدام. على سبيل المثال، ملفّ أسلوب لقوائم منسدلة مخصّصة مرتبطة بجزء JavaScript الخاص بها، أو صور رموز مرتبطة بعنصر شريط أدوات، أو وحدة WebAssembly مرتبطة بجزء JavaScript الخاص بها. في هذه الحالات، يكون من الأسهل الإشارة إلى الموارد مباشرةً من وحدات JavaScript وتحميلها ديناميكيًا عند تحميل المكوّن المقابل (أو في حال) تحميله.

رسم بياني يعرض أنواعًا مختلفة من مواد العرض التي تم استيرادها إلى JS

ومع ذلك، تحتوي معظم المشاريع الكبيرة على أنظمة إنشاء تُجري تحسينات إضافية وإعادة تنظيم للمحتوى، مثل التجميع والتصغير. ولا يمكنها تنفيذ الرمز وتوقع نتيجة التنفيذ، كما لا يمكنها التنقّل في كل سلسلة حرفية محتملة في JavaScript وإجراء تخمينات حول ما إذا كان عنوان URL للمورد أم لا. إذًا، كيف يمكنك جعلها "ترى" مواد العرض الديناميكية التي تحمّلها مكوّنات JavaScript، وتضمينها في التصميم؟

عمليات الاستيراد المخصّصة في أدوات تجميع التطبيقات

ومن الطرق الشائعة إعادة استخدام بنية الاستيراد الثابت. في بعض أدوات تجميع الحِزم، قد يتم رصد التنسيق تلقائيًا حسب امتداد الملف، في حين تسمح أدوات أخرى للمكوّنات الإضافية باستخدام مخطّط عنوان URL مخصّص، كما هو موضّح في المثال التالي:

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

عندما يعثر مكوّن إضافي لحزمة على عملية استيراد باستخدام إضافة يتعرّف عليها أو مخطّط مخصّص صريح (asset-url: وjs-url: في المثال أعلاه)، يضيف مادة العرض المُشار إليها إلى مخطّط الإنشاء، وينسخها إلى الوجهة النهائية، ويُجري تحسينات تنطبق على نوع مادة العرض، ويعرض عنوان URL النهائي الذي سيتم استخدامه أثناء وقت التشغيل.

مزايا هذا النهج: يضمن إعادة استخدام بنية استيراد JavaScript أن تكون جميع عناوين URL ثابتة ونسبًا إلى الملف الحالي، ما يسهّل على نظام الإنشاء تحديد موقع هذه الملحقات.

ومع ذلك، هناك عيب واحد مهمّ: لا يمكن استخدام هذا الرمز مباشرةً في المتصفّح، لأنّ المتصفّح لا يعرف كيفية التعامل مع هذه المخططات أو الإضافات المخصّصة للاستيراد. قد يكون هذا مناسبًا إذا كنت تتحكّم في كل الرموز وتعتمد على أداة تجميع للتطوير على أي حال، ولكن من الشائع بشكل متزايد استخدام وحدات JavaScript مباشرةً في المتصفّح، على الأقل أثناء التطوير، لتقليل المشاكل. قد لا يحتاج الشخص الذي يعمل على إصدار تجريبي صغير إلى أداة تجميع على الإطلاق، حتى في مرحلة الإنتاج.

نمط عام للمتصفّحات وأدوات تجميع الحِزم

إذا كنت تعمل على مكوّن قابل لإعادة الاستخدام، ستحتاج إلى أن يعمل في أيّ من البيئتَين، سواء تم استخدامه مباشرةً في المتصفّح أو تم إنشاؤه مسبقًا كجزء من تطبيق أكبر. تسمح معظم حِزم التجميع الحديثة بذلك من خلال قبول النمط التالي في وحدات JavaScript:

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

يمكن للأدوات رصد هذا النمط بشكلٍ ثابت، كما لو كان بنية خاصة تقريبًا، إلا أنّه تعبير JavaScript صالح يعمل مباشرةً في المتصفّح أيضًا.

عند استخدام هذا النمط، يمكن إعادة كتابة المثال أعلاه على النحو التالي:

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

كيف تعمل هذه الميزة؟ لنحلّل هذا الأمر. يأخذ عنصر الإنشاء new URL(...) عنوان URL نسبيًا كوسيطة أولى ويحلّله مقابل عنوان URL مطلق مقدَّم كوسيطة ثانية. في حالتنا، الوسيطة الثانية هي import.meta.url التي تقدّم عنوان URL لمكوّن JavaScript الحالي، لذا يمكن أن تكون الوسيطة الأولى أي مسار نسبيًا إليه.

وتُطبَّق عليها مقايضات مماثلة للاستيراد الديناميكي. على الرغم من أنّه من الممكن استخدام import(...) مع تعبيرات عشوائية مثل import(someUrl)، تُعامل حِزم الترميز الأنماط التي تحتوي على عنوان URL ثابت import('./some-static-url.js') بشكل خاص كطريقة لمعالجة التبعيات المعروفة في وقت الترجمة، ولكن يتم تقسيمها إلى مجموعة خاصة بها يتم تحميلها ديناميكيًا.

وبالمثل، يمكنك استخدام new URL(...) مع تعبيرات عشوائية مثل new URL(relativeUrl, customAbsoluteBase)، إلا أنّ نمط new URL('...', import.meta.url) هو إشارة واضحة لأدوات تجميع الحِزم لإجراء معالجة مسبقة وتضمين تبعية إلى جانب JavaScript الرئيسي.

عناوين URL النسبية الغامضة

قد تتساءل، لماذا لا يمكن لأدوات تجميع الحِزم رصد أنماط شائعة أخرى، مثل fetch('./module.wasm') بدون حِزم new URL؟

والسبب في ذلك هو أنّه، على عكس عبارات الاستيراد، يتمّ حلّ أي طلبات ديناميكية نسبةً إلى المستند نفسه، وليس إلى ملف JavaScript الحالي. لنفترض أنّ لديك البنية التالية:

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

إذا كنت تريد تحميل module.wasm من main.js، قد يكون من المغري استخدام مسار نسبي مثل fetch('./module.wasm').

ومع ذلك، لا يعرف fetch عنوان URL لملف JavaScript الذي يتم تنفيذه فيه، بل يحدّد عناوين URL نسبةً إلى المستند. ونتيجةً لذلك، سينتهي الأمر بمحاولة fetch('./module.wasm') تحميل http://example.com/module.wasm بدلاً من http://example.com/src/module.wasm المقصود وسيتعذّر عليه ذلك (أو الأسوأ من ذلك، قد يتم تحميل مورد مختلف بصمت عن المقصود).

من خلال تضمين عنوان URL النسبي في new URL('...', import.meta.url)، يمكنك تجنُّب هذه المشكلة وضمان حلّ أي عنوان URL مقدَّم بالنسبة إلى عنوان URL لمكوّن JavaScript الحالي (import.meta.url) قبل تمريره إلى أيّ أداة تحميل.

استبدِل fetch('./module.wasm') بـ fetch(new URL('./module.wasm', import.meta.url)) وسيؤدي ذلك إلى تحميل وحدة WebAssembly المتوقّعة بنجاح، بالإضافة إلى منح أدوات تجميع الرموز طريقة للعثور على هذه المسارات النسبية أثناء وقت الإنشاء أيضًا.

دعم الأدوات

حِزم

تتوفّر حِزم التطبيقات التالية التي تتيح استخدام مخطّط new URL:

WebAssembly

عند العمل مع WebAssembly، لن تحمِّل عادةً وحدة Wasm يدويًا، ولكن ستستورد بدلاً من ذلك رابط JavaScript الذي تنشئه سلسلة الأدوات. يمكن أن تُنشئ سلاسل الأدوات التالية نمط new URL(...) الموضَّح لك تلقائيًا.

C/C++ من خلال Emscripten

عند استخدام Emscripten، يمكنك أن تطلب منه إنشاء رابط JavaScript كوحدة ES6 بدلاً من نص عادي من خلال أحد الخيارَين التاليَين:

$ 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

عند استخدام هذا الخيار، سيستخدم الإخراج نمط new URL(..., import.meta.url) بشكل أساسي، حتى تتمكّن حِزم البرامج من العثور على ملف Wasm المرتبط تلقائيًا.

يمكنك أيضًا استخدام هذا الخيار مع خيوط WebAssembly عن طريق إضافة علامة -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

في هذه الحالة، سيتم تضمين Web Worker الذي تم إنشاؤه بالطريقة نفسها، وسيكون أيضًا قابلاً للاكتشاف من قِبل أدوات تجميع الحِزم والمتصفّحات على حد سواء.

Rust من خلال wasm-pack / wasm-bindgen

تتوفّر أيضًا عدّة أوضاع إخراج في أداة wasm-pack، وهي سلسلة أدوات Rust الأساسية لـ WebAssembly.

وسيُنشئ الإطار تلقائيًا وحدة JavaScript تعتمد على اقتراح دمج WebAssembly ESM. في وقت كتابة هذه السطور، لا يزال هذا الاقتراح تجريبيًا، ولن تعمل النتيجة إلا عند تجميعها مع Webpack.

بدلاً من ذلك، يمكنك أن تطلب من wasm-pack إنشاء وحدة ES6 متوافقة مع المتصفّح من خلال --target web:

$ wasm-pack build --target web

سيستخدم الإخراج نمط new URL(..., import.meta.url) الموضّح، وسيتم اكتشاف ملف Wasm تلقائيًا من قِبل أدوات تجميع الحِزم أيضًا.

إذا أردت استخدام سلاسل مهام WebAssembly مع Rust، تصبح الأمور أكثر تعقيدًا. اطّلِع على القسم المقابل من الدليل للاطّلاع على مزيد من المعلومات.

في الأساس، لا يمكنك استخدام واجهات برمجة تطبيقات المواضيع العشوائية، ولكن إذا كنت تستخدم Rayon، يمكنك دمجه مع مهايئ wasm-bindgen-rayon حتى يتمكّن من إنشاء "عمال" على الويب. إنّ رابط JavaScript الذي يستخدمه wasm-bindgen-rayon يتضمّن أيضًا نمط new URL(...) في الخلفية، وبالتالي ستتمكّن حِزم التجميع من اكتشاف "العمال" وتضمينها أيضًا.

الميزات المستقبلية

import.meta.resolve

إنّ إجراء مكالمة مخصّصة بشأن import.meta.resolve(...) هو أحد التحسينات المحتملة في المستقبل. سيسمح ذلك بحلّ المحدّدات بالنسبة إلى الوحدة الحالية بطريقة أكثر وضوحًا، بدون مَعلمات إضافية:

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

وسيتيح ذلك أيضًا دمجًا أفضل مع خرائط الاستيراد وأدوات التحويل المخصّصة، لأنّه سيخضع لنظام حلّ الوحدات نفسه المستخدَم في import. وستكون هذه الإشارة أقوى أيضًا لأدوات تجميع الحِزم، لأنّها بنية نحوية ثابتة لا تعتمد على واجهات برمجة تطبيقات وقت التشغيل مثل URL.

تمّ تنفيذ import.meta.resolve كتجربة في Node.js، ولكن لا تزال هناك بعض الأسئلة غير المُجاب عنها حول كيفية عملها على الويب.

تأكيدات الاستيراد

التأكيدات على الاستيراد هي ميزة جديدة تتيح استيراد أنواع أخرى غير وحدات ECMAScript. في الوقت الحالي، تقتصر على تنسيق JSON:

foo.json:

{ "answer": 42 }

main.mjs:

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

وقد تستخدمها أيضًا حِزم البرامج وتستبدل حالات الاستخدام التي يشملها نمط new URL حاليًا، ولكن يتمّ إضافة الأنواع في تأكيدات الاستيراد على أساس كلّ حالة على حدة. في الوقت الحالي، لا تتضمّن هذه الميزة سوى ملفات JSON، وستتوفّر وحدات CSS قريبًا، ولكن سيظلّ أنواع مواد العرض الأخرى تتطلّب حلًا أكثر عمومية.

اطّلِع على شرح ميزة v8.dev لمعرفة المزيد من المعلومات عن هذه الميزة.

الخاتمة

كما ترى، هناك طرق مختلفة لتضمين موارد غير JavaScript على الويب، ولكنّ لها عيوبًا مختلفة ولا تعمل على مستوى سلاسل الأدوات المختلفة. قد تسمح لنا الاقتراحات المستقبلية باستيراد مواد العرض هذه باستخدام بنية خاصة، ولكننا لم نصل إلى هذا الحدّ بعد.

وحتى ذلك الحين، يُعدّ نمط new URL(..., import.meta.url) هو الحلّ الأكثر تفاؤلاً الذي يعمل حاليًا في المتصفّحات وأدوات تجميع الحِزم المختلفة وسلسلة أدوات WebAssembly.