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

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

إنغفار ستيبانيان
إنغفار ستيبانيان

لنفترض أنّك تعمل على أحد تطبيقات الويب. في هذه الحالة، من المرجَّح أنّك لن تضطر فقط إلى استخدام وحدات JavaScript، ولكن عليك أيضًا التعامل مع جميع أنواع الموارد الأخرى، مثل "عاملو الويب" (التي تكون أيضًا عبارة عن 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));

How does it work? لنحلل هذا الأمر. تستخدم الدالة الإنشائية 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

Wast-pack - سلسلة أدوات Rust الأساسية لـ WebAssembly - تحتوي أيضًا على العديد من أوضاع الإخراج.

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

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

$ wasm-pack build --target web

وستستخدم الإخراج نمط new URL(..., import.meta.url) الموضّح، وستكتشف برامج Wasm تلقائيًا ملف 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.