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

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

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

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

يتيح لك هذا الخيار إنشاء رسم بياني لأنواع مختلفة من مواد العرض التي تم استيرادها إلى JavaScript.

ومع ذلك، تحتوي معظم المشاريع الكبيرة على أنظمة إنشاء تُجري تحسينات إضافية وإعادة تنظيم للمحتوى، مثل التجميع والتصغير. ولا يمكنها تنفيذ الرمز البرمجي والتوقّع بنتيجة التنفيذ، كما لا يمكنها التنقّل في كل سلسلة حرفية محتملة في 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 الذي تم إنشاؤه بالطريقة نفسها، وسيكون أيضًا قابلاً للاكتشاف من قِبل أدوات تجميع الحِزم والمتصفّحات على حد سواء.

صدأ عبر 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.