المولدات Generators والوحدات Modules في الجافاسكربت

المُنشِئات Generators

تقوم الدوال العادية بإرجاع قيمة واحدة فقط أو لا شئ.

أما الـgenertors فيمكنها أن تقوم بإرجاع عدة قيم, واحدة بعد الأخرى. وهذه الدوال تعمل بشكل جيد جدًا مع المتكررات iterables وتسمح بإنشاء تيارات من البيانات (data streams) بكل سهوله.

الدوال الـGenerator

لإنشاء generator سنحتاج إلى طريقة مخصّصة لذلك: function*، ولذلك تسمي “دالة generator”.

ويتم كتابتها كالآتى:

function* generateSequence() {
                yield 1;
                yield 2;
                return 3;
                }

تعمل الدوال الـGenerators بشكل مختلف من الدوال العادية. فعندما يتم استدعاء هذه الدالة فهي لا تقوم بتشغيل الكود بداخلها ولكن بدلًا من ذلك تقوم بإرجاع كائن (object) يسمي بـ"generator object" والذى يقوم بالتحكم فى التنفيذ.

ألق نظرة هنا:

function* generateSequence() {
                yield 1;
                yield 2;
                return 3;
                }

                // "generator function" تنشئ "generator object"
                let generator = generateSequence();
                alert(generator); // [object Generator]

تنفيذ الكود فى الدالة لم يبدأ بعد:

والدالة next() هي الدالة الأساسية فى الـgenerator. فعند استدعائها تقوم بتنفيذ الكود حتي أول جملة yield <value> (ويمكن حذف value وتكون عندئذ undefined) ثم يقف تنفيذ الدالة مؤقتًا ويتم إرجاع value للكود خارج الدالة.

ونتيجة استدعاء next() يكون دائمًا كائن يحتوى علي خاصيتين :

  • value: القيمة المنتَجة.
  • done: وتكون قيمتها true إذا انتعي تنفيذ الكود وتكون false إذا لم ينتهي بعد.

علي سبيل المثال، هنا قمنا بإنشاء generator والحصول على قيمته المنتَجة:

function* generateSequence() {
                yield 1;
                yield 2;
                return 3;
                }

                let generator = generateSequence();

                let one = generator.next();

                alert(JSON.stringify(one)); // {value: 1, done: false}

والآن، حصلنا علي أول قيمة فقط وتنفيذ الدالة متوقف عند السطر الثانى:

هيا نقوم باستدعاء generator.next() مرة أخرى، ستقوم باستكمال تنفيذ الكود وإرجاع الإنتاج التالي yield:

let two = generator.next();

                alert(JSON.stringify(two)); // {value: 2, done: false}

وإذا استدعينا الدالة مرة ثالثة فإن التنفيذ سيصل إلى جملة الـreturn والتى تنهى تنفيذ الدالة:

let three = generator.next();

                alert(JSON.stringify(three)); // {value: 3, done: true}

والآن انتهي عمل الـgenerator ويجب أن نرى هذا فى done:true واستخراج value:3 كقيمة نهائية.

وليس منطقيًا استدعاء generator.next() بعد ذلك. إذا قمنا بذلك مرة أخرى ستكون القيمة المسترجعة نفس الكائن: {done: true}.

function* f(…) أم function *f(…)?

كلا الطرقيتين صحيحة.

ولكن عادةً ما يُفضل استخدام أول طريقة function* f(…) لأن النجمة * تعنى أن هذه الدالة هي generator فهي تصف النوع لا الإسم ولذلك يجب أن تكون بجانب كلمة function.

الدوال الـGenerators تُعدّ متكررة iterable

كما أنك من المحتمل قد خمنت بالفعل عند استخدام الدالة next()، فإن الـGenerators هي متكررات iterable.

يمكننا أن نقوم بالتكرار عليهم باستخدام التكرار for..of:

function* generateSequence() {
                yield 1;
                yield 2;
                return 3;
                }

                let generator = generateSequence();

                for(let value of generator) {
                alert(value); // 1, then 2
                }

تبدو ألطف من استدعاء .next().value، أليس كذلك؟

…ولكن لاحظ: المثال أعلاه يُظهر 1 ثم 2 وهذا فقط ولا يُظهر 3!

وهذا لأن التكرار for..of يتجاهل آخر قيمة عندما تكون done: true، ولذلك إذا كنا نريد أن نُظهر كل النتائج باستخدام التكرار for..of، إذًا يجب أن نُرجع هذه القيم باستخدام yield:

function* generateSequence() {
                yield 1;
                yield 2;
                yield 3;
                }

                let generator = generateSequence();

                for(let value of generator) {
                alert(value); // 1, then 2, then 3
                }

بما أن الـgenerators قابلة للتكرار (iterable)، إذًا يمكننا أن نستخدم كل الوظائف المتعلقة بذلك مثل طريقة النشر (spread syntax) ...:

function* generateSequence() {
                yield 1;
                yield 2;
                yield 3;
                }

                let sequence = [0, ...generateSequence()];

                alert(sequence); // 0, 1, 2, 3

فى المثال أعلاه حوّلت ...generateSequence() الكائن المتكرر إلى قائمة (array) من العناصر (إقرأ المزيد عن طريقة النشر فى فصل المُعاملات «البقية» ومُعامل التوزيع)

استخدام الـgenerators مع المتكررات (iterables)

فى وقت سابق فى فصل المتكررات قمنا بإنشاء كائن متكرر يسمي range والذي يقوم بإرجاع القيم from..to.

هيا نتذكر الكود:

let range = {
                from: 1,
                to: 5,
                };

                // 1. عند تشغيل التكرار for..of فهي تقوم باستدعائ هذه الدالة
                range[Symbol.iterator] = function () {
                // ... وهذه الدالة تقوم بإرجاع الكائن المتكرر:
                // 2. بعد ذلك، يعمل التكرار for..of على هذا المتكرر فقط باحثًا عن القيم التالية
                return {
                current: this.from,
                last: this.to,

                // 3. يتم استدعاء الدالة next() فى كل دورة فى التكرار for..of
                next() {
                // 4. يجب أن تقوم بإرجاع القيمه على شكل الكائن {done:.., value :...}
                if (this.current <= this.last) {
                return { done: false, value: this.current++ };
                } else {
                return { done: true };
                }
                },
                };
                };

                // والآن التكرار يعمل!
                for (let num of range) {
                alert(num); // 1, then 2, 3, 4, 5
                }

يمكننا استخدام دالة generator للتكرار عن طريق إنشائها كـSymbol.iterator.

هنا الكائن range ولكن بإيجاز أكثر:

let range = {
                from: 1,
                to: 5,

                *[Symbol.iterator]() {
                // اختصارًا لـ [Symbol.iterator]: function*()
                for (let value = this.from; value <= this.to; value++) {
                yield value;
                }
                },
                };

                alert([...range]); // 1,2,3,4,5

إنها تعمل وذلك لأن range[Symbol.iterator]() تقوم بإرجاع generator والدوال التى هي عبارة عن generator هي ما يحتاجه التكرار for..of تمامًا:

  • تحتوى على الدالة .next()
  • تقوم بإرجاع القيمة كهذا الشكل: {value: ..., done: true/false}

وهذا بالطبع ليس بصدفة. فإن الـGenerators تمت إضافتها إلى الجافاسكريبت للمساعدة فى عمل المتكررات بشكل أسهل.

والمحتلف مع أى generator هو أنه مختصر أكثر من الكود المتكرر العادى range ويحتفظ بأدائه.

يمكن أن تُرجع الـGenerators قيمًا للأبد

فى المثال أعلاه أنشأنا تسلسلًا محدودًا ولكن يمكن أيضًا أن ننشئ generator يقوم بإنتاج قيم للأبد. على سبيل المثال، عدد غير منتهٍ من الأرقام العشوائية.

وهذا بالطبع يحتاج إلى break (أو return) فى التكرار على هذا الـgenerator باستخدام التكرار for..of. وإلا فإن التكرار سيعمل إلى الأبد و يتجمد.

تكوين الـGenerator

تكوين الـGenerator هي خاصية مميزة للـgenerators والتى تسمح بتكوين بتضمين generator بداخل آخر.

على سبيل المثال، لدينا دالة تقوم بإنشاء تسلسل من أرقم:

function* generateSequence(start, end) {
                for (let i = start; i <= end; i++) yield i;
                }

والآن نودّ أن ننشئ تسلسلًا أكثر تعقيدًا:

  • أولًا, الأرقام 0..9 (مع أرقام الأحرف فى الجدول ASCII من 48…57),
  • متبوعة بالأحرف الأبجدية A..Z (مع أرقام الأحرف فى الجدول ASCII من 65…90)
  • متبوعة بالأحرف الأبجدية a..z (مع أرقام الأحرف فى الجدول ASCII من 97…122)

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

حتى ندمج النتائج من دوال ممتعددة أخرى فى الدوال العادية فإننا نستدعيهم ونخزن القيم ثم ندمجهم فى النهاية.

أما فى الـgenerators فهناك شكل خاص yield* لتضمين generator بداخل آخر.

الـgenerator المُضمَّن:

function* generateSequence(start, end) {
                for (let i = start; i <= end; i++) yield i;
                }

                function* generatePasswordCodes() {

                // 0..9
                yield* generateSequence(48, 57);

                // A..Z
                yield* generateSequence(65, 90);

                // a..z
                yield* generateSequence(97, 122);

                }

                let str = '';

                for(let code of generatePasswordCodes()) {
                str += String.fromCharCode(code);
                }

                alert(str); // 0..9A..Za..z

الشكل yield* يقوم بتفويض التنفيذ إلى generator آخر. هذا المصطلح يعني أن yield* gen تقوم بالتكرار على هذا الـgenerator gen و ترسل منتجاتها خارجًا كأن هذه القيم تم إنتاجها بالـgenerator الخارجى.

إن النتيجة هي نفسها كما لو أننا وضعنا الكود كما هو بداخل generators واحد:

function* generateSequence(start, end) {
                for (let i = start; i <= end; i++) yield i;
                }

                function* generateAlphaNum() {

                // yield* generateSequence(48, 57);
                for (let i = 48; i <= 57; i++) yield i;

                // yield* generateSequence(65, 90);
                for (let i = 65; i <= 90; i++) yield i;

                // yield* generateSequence(97, 122);
                for (let i = 97; i <= 122; i++) yield i;

                }

                let str = '';

                for(let code of generateAlphaNum()) {
                str += String.fromCharCode(code);
                }

                alert(str); // 0..9A..Za..z

تكوين الـgenerators هي طريقة طبيعية لوضع عمل generator بداخل آخر. ولا تحتاج إلى ذاكرة إضافية لتخزين أى نتائج وسيطه.

“yield” طريق باتجاهين

حتى هذه اللحظه كانت الـgenerators شبيهة بالكائنات المتكررة مع طريقة خاصة لإنشاء القيم. ولكن فى الحقيقة فهم أكثر قوة ومرونة.

وهذا لأن yield هي طريق باتجاهين: فهي لا تقوم بإرجاع القيمة خارجًا فقط ولكن أيضًا يمكنها أن تمرر القيمة بداخل الـgenerator.

لفعل ذلك، يجب أن نستدعي generator.next(arg) بداخلها متغير وهذا المتغير سيكون نتيجة الـyield.

هيا نرى مثالًا:

function* gen() {
                // تمرير السؤال إلى الخارج وانتظار الإجابة
                let result = yield "2 + 2 = ?"; // (*)

                alert(result);
                }

                let generator = gen();

                let question = generator.next().value; // <-- تخزين السؤال

                generator.next(4); // --> تمرير الإجابة
  1. أول استدعاء generator.next() يجب أن يتم دائما بلا متغيرات (سيتم تجاهل المتغير إذا تم تمريره). فتبدأ التنفيذ وتقوم بإرجاع قيمة yield "2+2=?" الأول. عند هذه النقطة يقف الـgenerator عن التنفيذ بينما يقف عند السطر (*).
  2. بعد ذلك، وكما هو موضح في الصورة أعلاه، فإن قيمة yield تُخزن فى المتغير question.
  3. عند استدعاء generator.next(4) فإن الـgenerator يستأنف عمله ونسترجع 4 كقيمة: let result = 4.

لاحظ أن الكود الخارجي لا يجب أن يقوم باستدعاء next(4) فورًا، فهذا ليس بمشكلة: سينتظر الـgenerator.

علي سبيل المثال:

// استئناف الgenerator بعد بعض الوقت
                setTimeout(() => generator.next(4), 1000);

كما نرى، وهذا لا يحدث فى الدوال العادية، فإن الـgenerator والكود الذي يتم تنفيذه يمكنهما تبادل النتائج وتمرير القيم فى next/yield.

لجعل الأمور أكثر وضوحًا، إليك مثال آخر باستدعاءات أكثر:

function* gen() {
                let ask1 = yield "2 + 2 = ?";

                alert(ask1); // 4

                let ask2 = yield "3 * 3 = ?";

                alert(ask2); // 9
                }

                let generator = gen();

                alert(generator.next().value); // "2 + 2 = ?"

                alert(generator.next(4).value); // "3 * 3 = ?"

                alert(generator.next(9).done); // true

صورة التشغيل:

  1. أول استدعاء .next() بدأ التنفيذ… حتى وصل إلى أول yield.
  2. تم إرجاع النتيجة إلى الكود خارجًا.
  3. الإستدعاء الثانى .next(4) مرّر 4 إلى الـgenerator كنتيجة لأول yield واستكمل التنفيذ.
  4. …وصلنا إلى ثاني yield وأصبحت نتيجة استدعاء الـgenerator.
  5. ثالث استدعاء next(9) مرّر 9 للـgenerator كنتيجة لثاني yield واستأنف التنفيذ حتى وصل إلى نهاية الدالة ولذلك أصبحت done: true.

هذا يشبه لعبة “ping-pong” حيث أن كل next(value) (عدا أول استدعاء) تمرّر القيمة إلى الـgenerator وهي تصبح قيمة yield الحالية وبعد ذلك تحصل علي نتيجة yield التالية.

generator.throw

كما لاحظنا فى المثال أعلاه فإن الكود الخارجي يمكنه أن يمرر قيمة إلى الـgenerator كنتيجة لـyield.

…ولكن يمكنه أيضًا أن ينشئ خطأًا هناك. وهذا طبيعي خطأ كنتيجة.

لتمرير خطأ إلى yield، يجب أن نستدعى generator.throw(err) وفى هذه الحالة فإن err يتم إلقاؤه\ظهوره فى السطر الموجودة فيه yield.

علي سبيل المثال، فى قيمة yield "2 + 2 = ?" ستؤدي إلى خطأ:

function* gen() {
                try {
                let result = yield "2 + 2 = ?"; // (1)

                alert("لن يصل التنفيذ إلى هنا لأن الخطأ تم إلقاؤه فى السطر أعلاه");
                } catch(e) {
                alert(e); // يعرض الخطأ
                }
                }

                let generator = gen();

                let question = generator.next().value;

                generator.throw(new Error("The answer is not found in my database")); // (2)

تم إلقاء الخطأ إلى الـgenerator فى السطر (2) مما أدي إلى استثناء (exception) فى السطر (1) مع yield. فى المثال أعلاه ستجد try..catch قد استقبلت الخطأ وعرضته.

إذا لم نستقبل الخطأ فإنه مثل أى خطأ فإنه يُنهي الـgenerator.

إن السطر الحالي من الإستدعاء هو الذي فيه generator.throw والمُعلَّم بـ (2) ولذلك يمكننا أن نستقبل الخطأ هنا كالآتى:

function* generate() {
                let result = yield "2 + 2 = ?"; // خطأ فى هذا السطر
                }

                let generator = generate();

                let question = generator.next().value;

                try {
                generator.throw(new Error("The answer is not found in my database"));
                } catch(e) {
                alert(e); // يعرض الخطأ
                }

إذا لم نستقبل الخطأ هناك فإنه كالمعتاد سيُنهي الـgenerator ويخرج إلى الكود خارج الـgenerator (إذا كان هناك) وإذا لم يتم التعامل معه سيُنهي السكريبت (script).

الملخص

  • يتم إنشاء الـGenerators عن طريق دوال الـGenerator function* f(…) {…}.
  • بداخل الـgenerator توجد yield فقط.
  • الكود الخارجي والـ generator يمكنهما تبادل أى نتائج عن طريق next/yield.

فى الجافاسكريبت الحديثة يندر استخدام الـgenerators ولكن فى بعض الأوقات يصبحون مفيدين جدًا وهذا لقدرة الدالة لتبادل البيانات مع الكود الخارجي خلال التنفيذ وهذا فريد من نوعه. وبالطبع فإنهم مفيدين جدا لإنشاء كائنات متكررة (iterable objects).

وسنتعلم فى الفصل القادم الـgenerators الغير متزامنة (async generators) والتي تستخدم في قراءة تدفق البيانات بشكل غير متزامن (asynchronously) فى التكرار for await ... of.

فى برمجة الويب نتعامل غالبًا مع بيانات متدفقة streamed data ولذلك فإن هذه حالة أخري مهمة جدًا.

مهمه

هناك مواطن كثيرة حيث نحتاج إلى بيانات عشوائية.

واحدة منها هي الإختبار (testing). يمكن أن نحتاج إلى بيانات عشوائية: نصوص أو أرقام وهكذا لاختبار الأشياء جيدّا.

في الجافاسكريبت يمكننا استخدام Math.random() ولكن إذا حدث أى خطأ فإننا يمكن أن نود أن نعيد الإختبار باستخدام نفس البيانات.

من أجل ذلك نستخدم ما يسمي “seeded pseudo-random generators” فهي تأخذ بذرة “seed” كمتغير أول وتقوم بإنشاء القيم التالية باستخدام معادلة ولذلك فإن البذرة نفسها تظل فى نفس التتابع ويمكن تكرار نفس الخطوات بسهولة. نحتاج فقط أن نتذكر الذرة لتكرارها.

مثال على هذه المعادلة والتى تقوم بإنشاء قيم:

next = previous * 16807 % 2147483647

إذا استخدمنا 1 كبذرة فإن القيم ستكون:

  1. 16807
  2. 282475249
  3. 1622650073
  4. …وهكذا…

المهمة تقتضي أن تنشئ دالة generator pseudoRandom(seed) والتى تأخذ seed وتنشئ الـgenerator بهذه المعادلة.

مثال على استخدامها:

let generator = pseudoRandom(1);

                      alert(generator.next().value); // 16807
                      alert(generator.next().value); // 282475249
                      alert(generator.next().value); // 1622650073

افتح sandbox بالإختبارات.

function* pseudoRandom(seed) {
                          let value = seed;

                          while(true) {
                          value = value * 16807 % 2147483647
                          yield value;
                          }

                          };

                          let generator = pseudoRandom(1);

                          alert(generator.next().value); // 16807
                          alert(generator.next().value); // 282475249
                          alert(generator.next().value); // 1622650073

لاحظ أن هذا يمكن عمله بدالة عادية كهذا:

function pseudoRandom(seed) {
                          let value = seed;

                          return function() {
                          value = value * 16807 % 2147483647;
                          return value;
                          }
                          }

                          let generator = pseudoRandom(1);

                          alert(generator()); // 16807
                          alert(generator()); // 282475249
                          alert(generator()); // 1622650073

وهذا يعمل أيضًا ولكن فقدنا الإمكانية أن نكرر باستخدام التكرار for..of واستخدام تكوين الـgenerator وهذا يمكن أن يكون مفيدًا فى مكان ما.

افتح الحل الإختبارات في sandbox.

------

الـgenerators والتكرار الغير متزامن

تسمح لنا المتكررات الغير متزامنة (Asynchronous iterators) أن نقوم بالتكرار على بيانات تأتى بشكل غير متزامن. على سبيل المثال عندما نقوم بتحميل شيئ ما من الشبكة جزءًا بعد جزء. والـgenerators الغير متزامنة تجعل ذلك مناسبًا أكثر.

هيا نرى مثالًا لنتعلم الشكل ثم نرى حالة استخدام حقيقية.

المتكررات الغير متزامنة Async iterators

الـAsynchronous iterators تشبه المتكررات العادية مع قليل من الإختلافات فى التركيب.

الكائن المتكرر iterable object “العادى” تم وصفه فى فصل المتكررات وهو يكون كالآتى:

let range = {
                                from: 1,
                                to: 5,

                                // يستدعى التكرار for..of هذه الدالة مرة واحده فى البداية
                                [Symbol.iterator]() {
                                // ...تقوم بإرجاع الـiterator object:
                                // يعمل التكرار مع هذا الكائن فقط,
                                // ويتوقع منه القيم التالية باستخدام next()
                                return {
                                current: this.from,
                                last: this.to,

                                // next() يتم استدعاؤها فى كل دورة من قبل التكرار
                                next() { // (2)
                                // يجب أن ترجع القيمة كـ {done:.., value :...}
                                if (this.current <= this.last) {
                                return { done: false, value: this.current++ };
                                } else {
                                return { done: true };
                                }
                                }
                                };
                                }
                                };

                                for(let value of range) {
                                alert(value); // 1 then 2, then 3, then 4, then 5
                                }

أنظر إلى فصل المتكررات لمزيد من التفاصيل عن المتكررات العادية.

لجعل الكائن (object) متكررًا بشكل غير متزامن:

  1. نحتاج إلى أن نستعمل Symbol.asyncIterator بدلًا من Symbol.iterator.
  2. يجب أن ترجع الدالة next() كائن promise.
  3. للتكرار على كائن كهذا يجب أن نستخدم التكرار for await (let item of iterable)

هيا ننشئ متكرر range مثل السابق ولكن الآن سيقوم بإرجاع القيم بشكل غير متزامن، قيمة كل ثانية:

let range = {
                                from: 1,
                                to: 5,

                                // يستدعى التكرار هذه الدالة مرة واحدة فى البداية
                                [Symbol.asyncIterator]() { // (1)
                                // ...تقوم بإرجاع iterator object:
                                // ويعمل التكرار for await..of مع هذا الكائن فقط,
                                // سائلًا إياه عن القيمة التاالية باستخدام next()
                                return {
                                current: this.from,
                                last: this.to,

                                // next() يتم استدعاؤها فى كل دورة
                                async next() { // (2)
                                // يجب أن تقوم بإرجاع النتيجة كالآتى {done:.., value :...}
                                // (وتُحاط تلقائيًا بـ promise عند استخدام async)

                                // يمكن استخدام await:
                                await new Promise(resolve => setTimeout(resolve, 1000)); // (3)

                                if (this.current <= this.last) {
                                return { done: false, value: this.current++ };
                                } else {
                                return { done: true };
                                }
                                }
                                };
                                }
                                };

                                (async () => {

                                for await (let value of range) { // (4)
                                alert(value); // 1,2,3,4,5
                                }

                                })()

كما نرى فإن الشكل والتركيب مثل الـiterators العادية:

  1. لجعل الكائن iterable بشكل غير متزامن، يجب أن يحتوى على الدالة Symbol.asyncIterator (1).
  2. هذه الدالة يجب أن تقوم بإرجاع الكائن باستخدام next() والتى تقوم بإرجاع promise (2).
  3. لا يجب أن تكون الدالة next() عباره عن async ويمكن أن تكون دالة عادية تُرجع promise ولكن الكلمة async تمكننا من استخدام الكلمة await وهذا مناسب. وهنا يمكننا التأخير ثانية (3).
  4. للقيام بالتكرار نستخدم التكرار for await(let value of range) (4) ونضع الكلمة “await” بعد “for”. وهي تستدعي الدالة range[Symbol.asyncIterator]() مرة واحدة وثم الدالة next() من أجل القيم.

هنا ملخص بسيط:

Iterators Async iterators
الدوال لإنشاء متكرر Symbol.iterator Symbol.asyncIterator
القيمة التى تُرجعها next() أى فيمة Promise
للتكرار، نستخدم for..of for await..of
شكل الإنتشار ... لا تعمل بشكل غير متزامن

الأشياء التي تتطلب iterators عادية ومتزامنة synchronous لا تعمل فى المناطق الغير متزامنة.

على سبيل المثال، لا تعمل الـspread syntax:

alert([...range]); // Error, no Symbol.iterator

وهذا طبيعى، لأنها تتوقع وجود الدالة Symbol.iterator مثل التكرار for..of من غير الكلمة await وليس Symbol.asyncIterator.

الـgenerators الغير متزامنة

كما نعلم بالفعل أن الجافاسكريبت تدعم الـgenerators وهم أيضا iterables.

هيا نقوم باسترجاع التسلسل الذي أنشأناه فى الفصل المُنشِئات Generators. هذه الدالة تقوم بإنشاء تسلسل من القيم من start إلى end:

function* generateSequence(start, end) {
                                for (let i = start; i <= end; i++) {
                                yield i;
                                }
                                }

                                for(let value of generateSequence(1, 5)) {
                                alert(value); // 1, then 2, then 3, then 4, then 5
                                }

فى الـgenerators العادية لا يمكننا استخدام االكلمة await. فكل القيم تأتي بشكل متزامن: لا يوجد مكان للتأخير فى التكرار for..of فهي متزامنة.

ولكن ماذا إذا أردنا أن نستخدم الكلمة await فى الـgenerator؟ للقيام بطلبات من الشبكة على سبيل المثال.

لا توجد مشكلة سنضع الكلمة async فى البداية كالآتى:

async function* generateSequence(start, end) {

                                for (let i = start; i <= end; i++) {

                                // يمكنك استخدام await!
                                await new Promise(resolve => setTimeout(resolve, 1000));

                                yield i;
                                }

                                }

                                (async () => {

                                let generator = generateSequence(1, 5);
                                for await (let value of generator) {
                                alert(value); // 1, then 2, then 3, then 4, then 5
                                }

                                })();

لدينا الآن async generator و أيضًا قابل للتكرار iterable باستخدام for await...of.

هذا سهل جدًا. نضيف الكلمة async ويمكن للـgenerator الآن أن يستخدم الكلمة await بداخله ومعتمدًا على الـpromises وغيره من الدوال الغير متزامنة.

عمليًا يوجد اختلاف آخر فى الـasync generator وهو أن generator.next() أصبحت غير متزامنة asynchronous أيضًا وتقوم بإرجاع promises.

فى أى generator عادي يمكن أن نستخدم result = generator.next() للحصول على القيم. وفى الـasync generator يجب أن نستخدم await كالآتى:

result = await generator.next(); // result = {value: ..., done: true/false}

المتكررات الغير متزامنة Async iterables

كما علمنا فإنه لجعل الكائن قابل للتكرار iterable فيجب أن نضيف له Symbol.iterator.

let range = {
                                from: 1,
                                to: 5,
                                [Symbol.iterator]() {
                                return <object with next to make range iterable>
                                }
                                }

ومن الشائع أن تقوم Symbol.iterator بإرجاع generator بدلًا من كائن عادى باستخدام الدالة next كما فى مثال سابق.

هيا نسترجع مثالًا من فصل المُنشِئات Generators:

let range = {
                                from: 1,
                                to: 5,

                                *[Symbol.iterator]() { // اختصارًا لـ [Symbol.iterator]: function*()
                                for(let value = this.from; value <= this.to; value++) {
                                yield value;
                                }
                                }
                                };

                                for(let value of range) {
                                alert(value); // 1, then 2, then 3, then 4, then 5
                                }

هنا الكائن range متكرر والـgenerator *[Symbol.iterator] يصنع المنطق لعرض القيم كقائمة.

إذا أردنا أن نضيف أفعالًا غير متزامنة للـgenerator إذًا يجب أن نستبدل Symbol.iterator بالدالة Symbol.asyncIterator:

let range = {
                                from: 1,
                                to: 5,

                                async *[Symbol.asyncIterator]() { // هو نفسه [Symbol.asyncIterator]: async function*()
                                for(let value = this.from; value <= this.to; value++) {

                                // make a pause between values, wait for something
                                await new Promise(resolve => setTimeout(resolve, 1000));

                                yield value;
                                }
                                }
                                };

                                (async () => {

                                for await (let value of range) {
                                alert(value); // 1, then 2, then 3, then 4, then 5
                                }

                                })();

والآن تأتى القيم بتأخير ثانية بين كل قيمة.

مثال عملي

لقد رأينا حتي الآن أمثلة بسيطة لفهم الأساسيات والآن هيا نري مثالًا عمليًا.

توجد الكثير من الخدمات التي تقدم بيانات متجزئة. على سبيل المثال عندما نريد قائمة من المستخدمين فإن الطلب يقوم بإرجاع رقم مُعطي سابقًا (مثلا 100 مستخدم) – فى الصفحة الواحدة وتعطي رابط للصفحة التالية.

هذا النمط شائع جدًا. وهذا ليس مع المستخدمين فقط ولكن مع كل شيئ. على سبيل المثال يمكننا موقع جيتهاب أن نسترجع الـcommits علي نفس التجزئة:

  • يجب أن نقوم بطلب على رابط بهذا الشكل https://api.github.com/repos/<repo>/commits.
  • فتقوم بالرد بـ JSON يتكون من 30 commits وأيضًا تعطي رابطًا للصفحة التالية فى الـLink header.
  • بعد ذلك يمكننا أن نستخدم هذا الرابط للطلب التالى للحصول على المزيد من الـcommits وهكذا.

ولكن نود أن نحصل على API أبسط: كائن قابل للتكرار يتكون من commits ولذلك يمكننا أن نكرر عليهم كالآتى:

let repo = 'javascript-tutorial/en.javascript.info'; // GitHub repository to get commits from

                                for await (let commit of fetchCommits(repo)) {
                                // process commit
                                }

نود أن ننشئ دالة fetchCommits(repo) بتجلب لنا الـcommits وترسل الطلبات عند الحاجة. وتهتم هي بالتجزئة وبالنسبة لنا ستكون مجرد for await..of.

وهذا سهل باستخدام الـasync generators:

async function* fetchCommits(repo) {
                                let url = `https://api.github.com/repos/${repo}/commits`;

                                while (url) {
                                const response = await fetch(url, { // (1)
                                headers: {'User-Agent': 'Our script'}, // هذا ضرورى من أجل جيتهاب
                                });

                                const body = await response.json(); // (2) response is JSON (array of commits)

                                // (3) رابط الصفحة التالية موجود فى الهيدرز فنستخرجه
                                let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/);
                                nextPage = nextPage?.[1];

                                url = nextPage;

                                for(let commit of body) { // (4) yield commits one by one, until the page ends
                                yield commit;
                                }
                                }
                                }
  1. استخدمنا الدالة fetch الموجودة فى المتصفح للتحميل من رابط بعيد. فهي تسمح لنا بأن نضع هيدرز للطلب كما نريد وهنا يحتاج جيتهاب إلى الهيدر User-Agent.
  2. نتيجة الـfetch هي JSON مُحوَّل وهذه أيضًا دالة تخص الfetch.
  3. ويجب أن نحصل على رابط الصفحة التالية\الجزء التالي من الهيدر المسمّي Link والموجود فى هيدرز الرد. وهذا شكل خاص لذلك سنحتاج إلى استخدام regexp من أجل ذلك. وسيكون رابط الجزء التالى كهذا https://api.github.com/repositories/93253246/commits?page=2. وهو هكذا من قبل جيتهاب.
  4. بعد ذلك نقوم بإنتاج كل الـcommits التى وصلت إلينا وعندما ينتهون سيتم تنفيذ الدورة التالية من while(url) لعمل طلب آخر.

ومثال على الإستخدام:

(async () => {

                                let count = 0;

                                for await (const commit of fetchCommits('javascript-tutorial/en.javascript.info')) {

                                console.log(commit.author.login);

                                if (++count == 100) { // let's stop at 100 commits
                                break;
                                }
                                }

                                })();

هذا ما أردناه فقط. وما يحدث داخليًا هو غير مرئي. وما نعرفه أنه مجرد async generator يقوم بإرجاع commits.

الملخص

المتكررات الطبيعية Regular iterators والـ generators تعمل جيدًا مع البيانات التي لا تأخذ وقتًا ليتم إنتاجها.

عندما نتوقع أن تأتى البينات بشكل غير متزامن asynchronously بتأخير فيمكن عندئذ استخدام قدراتهم الغير متزامنة واستخدام التكرار for await..of بدلًا من for..of.

اختلاف الشكل ما بين المتكررات الطبيعية والغير متزامنه:

Iterable Async Iterable
الدالة لإنشاء متكرر Symbol.iterator Symbol.asyncIterator
القيمة التى تقوم بإرجاعها next() {value:…, done: true/false} Promise والذي يصل إلى {value:…, done: true/false}

الإختلاف فى الشكل بين الـasync و regular generators:

Generators Async generators
التعريف function* async function*
القيمة التى ترجعها next() {value:…, done: true/false} Promise والذي يصل إلى {value:…, done: true/false}

فى برمجة الويب نقابل غالبًا تدفقات من البيانات، عندما تأتى جزءًا بعد جزء. على سبيل المثال، تحميل أو رفع ملف كبير الحجم.

يمكننا استخدام الـasync generators لاستعمال بيانات كهذه. وجدير بالذكر أنه فى بعض البيئات مثل المتصفحات هناك أيضًا وسائل أخرى تسمي Streams والتى تعطي أشكالًا خاصة للتعامل مع التدفقات لتحويل البيانات وتمريرها من تدفق إلى آخر (مثل التحميل من مكان وإرساله فورًا إلى مكان آخر).

---------

مقدّمة إلى الوِحدات

سنرى سريعًا بينما تطبيقنا يكبُر حجمًا وتعقيدًا بأنّ علينا تقسيمه إلى ملفات متعدّدة، أو ”وِحدات“ (module). عادةً ما تحتوي الوِحدة على صنف أو مكتبة فيها دوالّ.

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

ولكن كالعادة صارت السكربتات هذه أكثر تعقيدًا وأكبر، فكان على المجتمع اختراع طرائق مختلفة لتنظيم الشيفرات في وحدات (أو مكتبات خاصّة تُحمّل تلك الوِحدات حين الطلب).

مثال:

  • AMD: هذه إحدى نُظم المكتبات القديمة جدًا والتي كتبت تنفيذها بدايةً المكتبة require.js.
  • CommonJS: نظام الوِحدات الذي صُنِع لخوادم Node.js.
  • UMD: نظام وِحدات آخر (اقتُرح ليكون للعموم أجمعين) وهو متوافق مع AMD وCommonJS.

أمّا الآن فهذه المكتبات صارت (أو تصير، يومًا بعد آخر) جزءًا من التاريخ، ولكن مع ذلك سنراها في السكربتات القديمة.

ظهر نظام الوِحدات (على مستوى اللغة) في المعيار عام 2015، وتطوّر شيئًا فشيئًا منذئذ وصارت الآن أغلب المتصفّحات الرئيسة (كما و Node.js) تدعمه. لذا سيكون أفضل لو بدأنا دراسة عملها من الآن.

ما الوِحدة؟

الوِحدة هي ملف، فقط. كلّ نص برمجي يساوي وحدة واحدة.

يمكن أن تُحمّل الوِحدات بعضها البعض وتستعمل توجيهات خاصة مثل التصدير export والاستيراد import لتتبادل الميزات فيما بينها وتستدعي الدوالّ الموجودة في وحدة ص، من وحدة س:

  • تقول الكلمة المفتاحية export للمتغيرات والدوالّ بأنّ الوصول إليها من خارج الوِحدة الحالية هو أمر مُتاح.
  • وتُتيح import استيراد تلك الوظائف من الوِحدات الأخرى.

فمثلًا لو كان لدينا الملف sayHi.js وهو يُصدّر دالّةً من الدوالّ:

// 📁 sayHi.js
                export function sayHi(user) {
                alert(`Hello, ${user}!`);
                }

فيمكن لملف آخر استيراده واستعمالها:

// 📁 main.js
                import {sayHi} from './sayHi.js';

                alert(sayHi); // function... نوعها دالة
                sayHi('John'); // Hello, John!

تتوجه تعليمة import للوِحدة ‎./sayHi.js عبر المسار النسبي المُمرر لها. ويسند التابع sayHi للمتغيّر الذي يحمل نفس اسم التابع.

لنشغّل المثال في المتصفّح.

تدعم الوِحدات كلمات مفتاحية ومزايا خاصة، لذلك علينا إخبار المتصفّح بأنّ هذا السكربت هو وِحدة ويجب أن يُعامل بهذا النحو، ذلك باستعمال الخاصية ‎<script type="module">‎.

هكذا:

نتيجة
say.js
index.html
export function sayHi(user) {
                return `Hello, ${user}!`;
                }
<!doctype html>
                <script type="module">
                import {sayHi} from './say.js';

                document.body.innerHTML = sayHi('John');
                </script>

يجلب المتصفّح الوِحدة تلقائيًا ويقيم الشيفرة البرمجية بداخلها (ويستورد جميع الوحدات المتعلقة بها إن لزم الأمر)، وثمّ يشغلها.

Modules work only via HTTP(s), not in local files

If you try to open a web-page locally, via file:// protocol, you’ll find that import/export directives don’t work. Use a local web-server, such as static-server or use the “live server” capability of your editor, such as VS Code Live Server Extension to test modules.

ميزات الوِحدات الأساسية

=======

ولكن ما الفرق بين الوِحدات والسكربتات (الشيفرات) "العادية“ تلك؟

للوِحدات ميزات أساسية تعمل على محرّكات جافاسكربت للمتصفّحات وللخوادم على حدّ سواء.

الوضع الصارم الإفتراضي

تستخدم الوِحدات الوضع الصارم تلقائيًا فمثلًا إسناد قيمة لمتحول غير معرّف سينتج خطأ.

<script type="module">
                a = 5; // خطأ
                </script>

النطاق على مستوى الوحدات

كلّ وِحدة لها نطاق عالي المستوى خاص بها. بتعبيرٍ آخر، لن يُنظر للمتغيّرات والدوالّ من الوحدات الأخرى، وإنما يكون نطاق المتغيرات محلي.

نرى في المثال أدناه أنّا حمّلنا نصّين برمجيين، ويحاول الملف hello.js استعمال المتغير user المصرّح عنه في الملف user.js ولا يقدر:

نتيجة
hello.js
user.js
index.html
alert(user); // no such variable (each module has independent variables)
let user = "John";
<!doctype html>
                <script type="module" src="user.js"></script>
                <script type="module" src="hello.js"></script>

على الوِحدات تصدير export ما تريد للآخرين من خارجها رؤيته، واستيراد import ما تحتاج استعماله.

لذا علينا استيراد user.js وhello.js وأخذ المزايا المطلوبة منهما بدل الاعتماد على المتغيّرات العمومية.

هذه النسخة الصحيحة من الشيفرة:

نتيجة
hello.js
user.js
index.html
import {user} from './user.js';

                document.body.innerHTML = user; // John
export let user = "John";
<!doctype html>
                <script type="module" src="hello.js"></script>

يوجد في المتصفح نطاق مستقل عالي المستوى. وهو موجود أيضًا للوحدات ‎<script type="module">‎:

<script type="module">
                // سيكون المتغير مرئي في مجال هذه الوِحدة فقط
                let user = "John";
                </script>

                <script type="module">
                alert(user); // ‫خطأ: المتغير user غير معرّف
                </script>

ولو أردنا أن ننشئ متغير عام على مستوى النافذة يمكننا تعيينه صراحة للمتغيّر window ويمكننا الوصول إليه هكذا window.user. ولكن لابد من وجود سبب وجيهٍ لذلك.

تقييم شيفرة الوِحدة لمرة واحدة فقط

لو استوردتَ نفس الوِحدة في أكثر من مكان، فلا تُنفّذ شيفرتها إلّا مرة واحدة، وبعدها تُصدّر إلى من استوردها.

ولهذا توابع مهمّ معرفتها. لنرى بعض الأمثلة.

أولًا، لو كان لشيفرة الوِحدة التي ستُنفّذ أيّ تأثيرات (مثل عرض رسالة أو ما شابه)، فاستيرادها أكثر من مرّة سيشغّل ذلك التأثير مرة واحدة، وهي أول مرة فقط:

// 📁 alert.js
                alert("Module is evaluated!"); // ‫نُفّذت شيفرة الوِحدة!
// نستورد نفس الوِحدة من أكثر من ملف

                // 📁 1.js
                import `./alert.js`; // ‫نُفّذت شيفرة الوِحدة!

                // 📁 2.js
                import `./alert.js`; // (لا نرى شيئًا هنا)

في الواقع، فشيفرات الوِحدات عالية المستوى في بنية البرمجية لا تُستعمل إلّا لتمهيد بنى البيانات الداخلية وإنشائها. ولو أردنا شيئًا نُعيد استعماله، نُصدّر الوِحدة.

الآن حان وقت مثال مستواه متقدّم أكثر.

لنقل بأنّ هناك وحدة تُصدّر كائنًا:

// 📁 admin.js
                export let admin = {
                name: "John"
                };

لو استوردنا هذه الوِحدة من أكثر من ملف، فلا تُنفّذ شيفرة الوِحدة إلّا أول مرة، حينها يُصنع كائن المدير admin ويُمرّر إلى كلّ من استورد الوِحدة.

وهكذا تستلم كلّ الشيفرات كائن مدير admin واحد فقط لا أكثر ولا أقل:

// 📁 1.js
                import {admin} from './admin.js';
                admin.name = "Pete";

                // 📁 2.js
                import {admin} from './admin.js';
                alert(admin.name); // Pete

                // ‫كِلا الملفين ‎1.js و ‎2.js سيستوردان نفس الكائن
                // ‫التغييرات الّتي ستحدثُ في الملف ‎1.js ستكون مرئية في الملف ‎2.js

ولنؤكد مجددًا – تُنفذّ الوِحدة لمرة واحدة فقط. وتُنشئ الوِحدات المراد تصديرها وتُشارك بين المستوردين لذا فإن تغير شيء ما في كائن admin فسترى الوِحدات الأخرى ذلك.

يتيح لنا هذا السلوك ”ضبط“ الوِحدة عند أوّل استيراد لها، فنضبط خاصياتها المرة الأولى، ومتى ما استوُردت مرة أخرى تكون جاهزة.

فمثلًا قد تقدّم لنا وحدة admin.js بعض المزايا ولكن تطلب أن تأتي امتيازات الإدارة من خارج كائن admin إلى داخله:

// 📁 admin.js
                export let admin = { };

                export function sayHi() {
                alert(`Ready to serve, ${admin.name}!`);
                }

نضبط في init.js (أوّل نص برمجي لتطبيقنا) المتغير admin.name. بعدها سيراه كلّ من أراد بما في ذلك الاستدعاءات من داخل وحدة admin.js نفسها:

// 📁 init.js
                import {admin} from './admin.js';
                admin.name = "Pete";

ويمكن لوحدة أخرى استعمال admin.name:

// 📁 other.js
                import {admin, sayHi} from './admin.js';

                alert(admin.name); // Pete

                sayHi(); // Ready to serve, Pete!

import.meta

يحتوي الكائن import.meta على معلومات الوِحدة الحالية.

ويعتمد محتواها على البيئة الحالية، ففي المتصفّحات يحتوي على عنوان النص البرمجي أو عنوان صفحة الوِب الحالية لو كان داخل HTML:

html run height=0
                <script type="module">
                alert(import.meta.url); // ‫عنوان URL للسكربت (عنوان URL لصفحة HTML للسكربت الضمني)
                </script>

this في الوِحدات ليست معرّفة

قد تكون هذه الميزة صغيرة، ولكنّا سنذكرها ليكتمل هذا الفصل.

في الوحدات، قيمة this عالية المستوى غير معرّفة.

وازن بينها وبين السكربتات غير المعتمدة على الوحدات، إذ ستكون this كائنًا عامًا:

html run height=0
                <script>
                alert(this); // window
                </script>

                <script type="module">
                alert(this); // غير معرّف
                </script>

الميزات الخاصة بالمتصفّحات

كما أن هناك عدّة فروق تخصّ المتصفحات السكربتات (المعتمدة على الوحدات) بالنوع type="module"‎ موازنةً بتلك العادية.

لو كنت تقرأ هذا الفصل لأول مرة، أو لم تكن تستعمل المحرّك في المتصفّح فيمكنك تخطّي هذا القسم.

سكربتات الوِحدات مؤجلة

دائمًا ما تكون سكربتات الوِحدات مؤجلة، ومشابهة لتأثير السِمة defer (الموضحة في هذا المقال)، لكل من السكربتات المضمّنة والخارجية.

أي وبعبارة أخرى:

  • تنزيل السكربتات المعتمدة على الوِحدات الخارجية ‎<script type="module" src=‎"...">‎ لا تُوقف معالجة HTML فتُحمّل بالتوازي مع الموارد الأخرى.
  • تنتظر السكربتات المعتمدة على الوِحدات حتّى يجهز مستند HTML تمامًا (حتّى لو كانت صغيرة وحُمّلت بنحوٍ أسرع من HTML) وتُشغّل عندها.
  • تحافظ على الترتيب النسبي للسكربتات: فالسكربت ذو الترتيب الأول ينفذّ أولًا.

ويسبّب هذا بأن ”ترى“ السكربتات المعتمدة على الوِحدات صفحة HTML المحمّلة كاملة بما فيه عناصر الشجرة أسفلها.

مثال:

<script type="module">

                alert(typeof button); // ‫كائن (object): يستطيع السكربت رؤية العناصر أدناه
                // بما أن الوِحدات مؤجلة. سيُشغل السكربت بعد تحميل كامل الصفحة
                </script>

                Compare to regular script below:

                <script>

                alert(typeof button); // ‫خطأ: الزر (button) غير معرّف. لن يستطيع السكربت رؤية العناصر أدناه
                // السكربت العادي سيُشغل مباشرة قبل أن يُستكمل تحميل الصفحة
                </script>

                <button id="button">Button</button>

لاحِظ كيف أنّ النص البرمجي الثاني يُشغّل فعليًا قبل الأول! لذا سنرى أولًا undefined وبعدها object. وذلك بسبب كون عملية تشغيل الوِحدات مُؤجلة لذلك سننتظر لاكتمال معالجة المستند. نلاحظ أن السكربت العادي سيُشغلّ مباشرة بدون تأجيل ولذا سنرى نتائجه أولًا.

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

خاصية Async على السكربتات المضمّنة

بالنسبة للسكربتات غير المعتمدة على الوِحدات فإن خاصية async (اختصارًا لكلمة Asynchronous أي غير المتزامن) تعمل على السكربتات الخارجية فقط. وتُشغل السكربتات غير المتزامنة مباشرة عندما تكون جاهزة،بشكل مستقل عن السكربتات الأخرى أو عن مستند HTML.

تعمل السكربتات المعتمدة على الوِحدات طبيعيًا في السكربتات المضمّنة.

فمثلًا يحتوي السكربت المُضمن أدناه على الخاصية async، لذلك سيُشغّل مباشرة ولن ينتظر أي شيء.

وهو ينفذ عملية الاستيراد (اجلب الملف ./analytics.js) وشغله عندما يصبح جاهزًا، حتى وإن لم ينتهِ مستند HTML بعد. أو السكربتات الأُخرى لا تزال معلّقة.

وهذا جيد للتوابع المستقلة مثل العدادات والإعلانات ومستمع الأحداث على مستوى المستند.

في المثال أدناه، جُلبت جميع التبعيات (من ضمنها analytics.js).‫ ومن ثمّ شُغّل السكربت ولم ينتظر حتى اكتمال تحميل المستند أو السكربتات الأخرى.

<script async type="module">
                import {counter} from './analytics.js';

                counter.count();
                </script>

السكربتات الخارجية

تختلف السكربتات الخارجية التي تحتوي على السمة type="module"‎ في جانبين:

  1. تنفذ السكربتات الخارجية التي لها نفس القيمة للخاصية src مرة واحدة فقط. فهنا مثلًا سيُجلب السكربت my.js وينفذ مرة واحدة فقط.

    <script type="module" src="my.js"></script>
                        <script type="module" src="my.js"></script>
  2. تتطلب السكربتات الخارجية التي تجلب من مصدر مستقل (موقع مختلف عن الأساسي) ترويسات CORS والموضحة في هذا المقال. بتعبير آخر إن جُلِبَ سكربت يعتمد على الوِحدات من مصدر معين فيجب على الخادم البعيد أن يدعم ترويسات السماح بالجلب Access-Control-Allow-Origin. يجب أن يدعم المصدر المستقل Access-Control-Allow-Origin (في المثال أدناه المصدر المستقل هو another-site.com) وإلا فلن يعمل السكربت.

    <script type="module" src="http://another-site.com/their.js"></script>

وذلك سيضمن لنا مستوى أمان أفضل إفتراضيًا.

لا يُسمح بالوحدات المجردة

في المتصفح، يجب أن تحصل تعليمة import على عنوان URL نسبي أو مطلق. وتسمى الوِحدات التي بدون أي مسار بالوحدات المجردة. وهي ممنوع في تعليمة import.

لنأخذ مثالًا يوضح الأمر، هذا import غير صالح:

import {sayHi} from 'sayHi'; // خطأ وِحدة مجردة
                // ‫يجب أن تمتلك الوِحدة مسارًا مثل: '‎./sayHi.js' أو مهما يكُ موقع هذه الوِحدة

تسمح بعض البيئات، مثل Node.js أو أدوات تجميع الوِحدات باستخدام الوِحدات المجردة، دون أي مسار، حيث أن لديها طرقها الخاصة للعثور على الوِحدات والخطافات لضبطها. ولكن حتى الآن لا تدعم المتصفحات الوِحدات المجردة.

التوافقية باستخدام “nomodule”

لا تفهم المتصفحات القديمة طريقة استخدام الوِحدات في الصفحات type ="module"‎.بل وإنها تتجاهل السكربت ذو النوعٍ غير المعروف. بالنسبة لهم، من الممكن تقديم نسخة مخصصة لهم باستخدام السمة nomodule:

<script type="module">
                alert("Runs in modern browsers");
                </script>

                <script nomodule>
                alert("Modern browsers know both type=module and nomodule, so skip this"):// ‫المتصفحات الحديثة تعرف type=module  و nomodule لذا لن تنفذ الأخير
                alert("Old browsers ignore script with unknown type=module, but execute this.");// ‫المتصفحات القديمة ستتجاهل الوسم ذو السِمة type=module ولكن ستنفذ وسم nomodule
                </script>

أدوات البناء

في الحياة الواقعية، نادرًا ما تستخدم وحدات المتصفح في شكلها “الخام”. بل عادةّ نجمعها مع أداة خاصة مثل [Webpack] (https://webpack.js.org/) وننشرها على خادم النشر.

إحدى مزايا استخدام المجمعات – فهي تمنح المزيد من التحكم في كيفية التعامل مع الوحدات، مما يسمح بالوحدات المجردة بل وأكثر من ذلك بكثير، مثل وحدات HTML/CSS.

تؤدي أدوات البناء بعض الوظائف منها:

  1. جلب الوِحدة الرئيسية main، وهي الوِحدة المراد وضعها في وسم ‎<script type ="module">‎ في ملف HTML.
  2. تحليل التبعيات: تحليل تعليمات الاستيراد الخاصة بالملف الرئيسي وثم للملفات المستوردة أيضًا وما إلى ذلك.
  3. إنشاء ملفًا واحدًا يحتوي على جميع الوِحدات (مع إمكانية تقسيمهُ لملفات متعددة)، مع استبدال تعليمة import الأصلية بتوابع الحزم لكي يعمل السكربت. كما تدعم أنواع وحدات “خاصة” مثل وحدات HTML/CSS.
  4. يمكننا تطبيق عمليات تحويل وتحسينات أخرى في هذه العملية مثل:
    • إزالة الشيفرات الّتي يتعذر الوصول إليها.
    • إزالة تعليمات التصدير غير المستخدمة (مشابهة لعملية هز الأشجار وسقوط الأوراق اليابسة).
    • إزالة العبارات الخاصة بمرحلة التطوير مثل console وdebugger.
    • تحويل شيفرة جافاسكربت الحديثة إلى شيفرة أقدم باستخدام وظائف مماثلة للحزمة [Babel] (https://babeljs.io/).
    • تصغير الملف الناتج (إزالة المسافات، واستبدال المتغيرات بأسماء أقصر، وما إلى ذلك).

عند استخدامنا لأدوات التجميع سيُجمع السكربت ليصبح في ملف واحد (أو ملفات قليلة) ، تُستبدل تعليمات import/export بداخل السكربتات بتوابع المُجمّع الخاصة. لذلك لا يحتوي السكربت “المُجَمّع” الناتج على أي تعليمات import/export، ولا يتطلب السِمة type="module"‎، ويمكننا وضعه في سكربت عادي: في المثال أدناه لنفترض أننا جمعّنا الشيفرات في ملف bundle.js باستخدام مجمع حزم مثل: Webpack.

<script src="bundle.js"></script>

ومع ذلك يمكننا استخدام الوِحدات الأصلية (في شكلها الخام). لذلك لن نستخدم هنا أداة Webpack: يمكنك التعرف عليها وضبطها لاحقًا.

خلاصة

لنلخص المفاهيم الأساسية:

  1. الوِحدة هي مجرد ملف. لجعل تعليمتي import/export تعملان، ستحتاج المتصفحات إلى وضع السِمة التالية ‎<script type ="module">‎. تحتوي الوِحدات على عدة مُميزات:
    • مؤجلة إفتراضيًا.
    • تعمل الخاصية Async على السكربتات المضمّنة.
    • لتحميل السكربتات الخارجية من مصدر مستقل، يجب استخدام طريقة (المَنفذ / البروتوكول / المجال)، وسنحتاج لترويسات CORS أيضًا.
    • ستُتجاهل السكربتات الخارجية المكررة.
  2. لكل وِحدة من الوِحدات نطاق خاص بها، وتتبادلُ الوظائف فيما بينها من خلال استيراد وتصدير الوِحدات import/export.
  3. تستخدم الوِحدات الوضع الصارم دومًا use strict.
  4. تُنفذ شيفرة الوِحدة لمرة واحدة فقط. وتُصدر إلى من استوردها لمرة واحدة أيضًا، ومن ثمّ تُشارك بين المستوردين.

عندما نستخدم الوحدات، تنفذ كل وِحدة وظيفة معينة وتُصدرها. ونستخدم تعليمة import لاستيرادها مباشرة عند الحاجة. إذ يُحمل المتصفح السكربت ويقيّمه تلقائيًا.

وبالنسبة لوضع النشر، غالبًا ما يستخدم الناس مُحزّم الوِحدات مثل [Webpack] (https://webpack.js.org) لتجميع الوِحدات معًا لرفع الأداء ولأسباب أخرى.

سنرى في الفصل التالي مزيدًا من الأمثلة عن الوِحدات، وكيفية تصديرها واستيرادها.

ترجمة -وبتصرف- للفصل Modules, introduction من كتاب The JavaScript language

------

التصدير والاستيراد

لمُوجِّهات (تعليمات) الاستيراد والتصدير أكثر من صياغة برمجية واحدة.

رأينا في الفصل السابق استعمالًا بسيطًا له، فهيًا نرى بقية الاستعمالات.

التصدير قبل التصريح

يمكننا أن نقول لأيّ تصريح بأنّه مُصدّر بوضع عبارة export قبله، كان التصريح عن متغيّر أو عن دالة أو عن صنف.

فمثلًا، التصديرات هنا كلّها صحيحة:

// تصدير مصفوفة
                    export let months = ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

                    // تصدير ثابت
                    export const MODULES_BECAME_STANDARD_YEAR = 2015;

                    // تصدير صنف
                    export class User {
                    constructor(name) {
                    this.name = name;
                    }
                    }

ملاحظة: لا يوجد فواصل منقوطة بعد تعليمة التصدير للأصناف أو الدوالّ لاحظ أن تعليمة export قبل الصنف أو الدالة لا يجعلها تعابير الدوالّ. ولو أنه يصُدرها، لكنه لا يزال تعريفًا للدالّة أو الصنف.

لا توصي معظم الأدلة التعليمية بوضع فاصلة منقوطة بعد تعريف الدوال والأصناف.

لهذا السبب لا داعي للفاصلة المنقوطة في نهاية التعليمة export class والتعليمة export function:

export function sayHi(user) {
                    alert(`Hello, ${user}!`);
                    } // لاحظ لا يوجد فاصلة منقوطة في نهاية التعريف

التصدير بعيدًا عن التصريح

كما يمكننا وضع عبارة export لوحدها.

هنا نصرّح أولًا عن الدالتين وبعدها نُصدّرهما:

// 📁 say.js
                    function sayHi(user) {
                    alert(`Hello, ${user}!`);
                    }

                    function sayBye(user) {
                    alert(`Bye, ${user}!`);
                    }

                    export {sayHi, sayBye}; // a list of exported variables

أو… يمكننا تقنيًا وضع export أعلى الدوال أيضًا.

عبارة استيراد كلّ شيء

عادةً نضع قائمة بما نريد استيراده في أقواس معقوفة import {...}‎، هكذا:

// 📁 main.js
                    import {sayHi, sayBye} from './say.js';

                    sayHi('John'); // Hello, John!
                    sayBye('John'); // Bye, John!

ولكن لو أردنا استيراد وحدات كثيرة، فيمكننا استيراد كلّ شيء كائنًا واحدًا باستعمال import * as <obj>‎ هكذا:

// 📁 main.js
                    import * as say from './say.js';

                    say.sayHi('John');
                    say.sayBye('John');

يقول المرء من النظرة الأولى ”استيراد كلّ شيء فكرة جميلة جدًا، وكتابة الشيفرة سيكون أسرع. أساسًا لمَ نقول جهارةً ما نريد استيراده؟“

ذلك… لأسباب وجيهة.

  1. أدوات البناء الحديثة (مثل: webpack وغيرها)

    لنقل مثلًا بأنّا أضفنا مكتبة خارجية اسمها say.js إلى مشروعنا، وفيها دوالّ عديدة:

    // 📁 say.js
                            export function sayHi() { ... }
                            export function sayBye() { ... }
                            export function becomeSilent() { ... }

    هكذا نستعمل واحدة فقط من دوالّ say.js في مشروعنا:

    // 📁 main.js
                            import {sayHi} from './say.js';

    …حينها تأتي أداة التحسين وترى ذلك، فتُزيل الدوال الأخرى من الشيفرة … بذلك يصغُر حجم الملف المبني. هذا ما نسميه هز الشجر (لتَسقطَ الأوراق اليابسة).

  2. لو وضّحنا بالضبط ما نريد استيراده فيمكننا كتابته باسم أقصر: sayHi()‎ بدل say.sayHi()‎.

  3. بكتابة قائمة الاستيراد جهارةً نستطيع أن نفهم بنية الشيفرة دون الخوض في التفاصيل (أي نعرف ما نستعمل من وحدات، وأين نستعملها). هذا يسهّل دعم الشيفرة وإعادة كتابتها لو تطلّب الأمر.

استيراد كذا بالاسم كذا as

يمكننا كذلك استعمال as لاستيراد ما نريد بأسماء مختلفة.

فمثلًا يمكننا استيراد الدالة sayHi في المتغير المحلي hi لنختصر الكلام، واستيراد sayBye على أنّها bye:

// 📁 main.js
                    import {sayHi as hi, sayBye as bye} from './say.js';

                    hi('John'); // Hello, John!
                    bye('John'); // Bye, John!

تصدير كذا بالاسم كذا as

نفس صياغة الاستيراد موجودة أيضًا للتصدير export.

فلنصدّر الدوال على أنّها hi وbye:

// 📁 say.js
                    ...
                    export {sayHi as hi, sayBye as bye};

الآن صارت hi وbye هي الأسماء ”الرسمية“ للشيفرات الخارجية وستُستعمل عند الاستيراد:

// 📁 main.js
                    import * as say from './say.js';

                    // لاحِظ الفرق
                    say.hi('John'); // Hello, John!
                    say.bye('John'); // Bye, John!

التصدير المبدئي

في الواقع العملي، ثمّة نوعين رئيسين من الوحدات.

  1. تلك التي تحتوي مكتبة (أي مجموعة من الدوال) مثل وحدة say.js أعلاه.
  2. وتلك التي تصرّح عن كيانٍ واحد مثل وحدة user.js التي تُصدّر class User فقط.

عادةً ما يُحبّذ استعمال الطريقة الثانية كي يكون لكلّ ”شيء“ وحدةً خاصة به.

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

توفر الوِحدات طريقة لصياغة عبارة export default (التصدير المبدئي) لجعل “سطر تصدير واحد لكلّ وِحدة” تبدو أفضل.

ضَع export default قبل أيّ كيان لتصديره:

// 📁 user.js
                    export default class User { // ‫نُضيف ”default“ فقط
                    constructor(name) {
                    this.name = name;
                    }
                    }

لكلّ ملف سطر تصدير export default واحد لا أكثر.

وبعدها… نستورد الكيان بدون الأقواس المعقوفة:

// 📁 main.js
                    import User from './user.js'; // ‫لا نضع {User}، بل User

                    new User('John');

أسطر الاستيراد التي لا تحتوي الأقواس المعقوفة أجمل من تلك التي تحتويها. يشيع خطأ نسيان تلك الأقواس حين يبدأ المطورون باستعمال الوِحدات. لذا تذكّر دائمًا، يطلب سطر الاستيراد import أقواس معقوفة للكيانات المُصدّرة والتي لها أسماء، ولا يطلبها لتلك المبدئية.

التصدير الذي له اسم التصدير المبدئي
export class User {...} export default class User {...}‎
import {User} from ... import User from ...

يمكننا نظريًا وضع النوعين من التصدير معًا في نفس الوحدة (الذي له اسم والمبدئي)، ولكن عمليًا لا يخلط الناس عادةً بينها، بل للوِحدة إمّا تصديرات لها أسماء، أو التصدير المبدئي.

ولأنّه لا يمكن أن يكون لكلّ ملف إلا تصديرًا مبدئيًا واحدًا، فيمكن للكيان الذي صُدّر ألّا يحمل أيّ اسم.

فمثلًا التصديرات أسفله كلّها صحيحة مئة في المئة:

export default class { // لا اسم للصنف
                    constructor() { ... }
                    }
export default function(user) { // لا اسم للدالة
                    alert(`Hello, ${user}!`);
                    }
// نُصدّر قيمةً واحدة دون صنع متغيّر
                    export default ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

لا مشكلة بتاتًا بعدم كتابة الاسم إذ لا نرى export default إلّا مرّة في الملف، بهذا تعرف تمامًا أسطر import (بدون استعمال الأقواس المعقوفة) ما عليها استيراده.

ولكن دون default فهذا التصدير سيُعطينا خطأً:

export class { // Error! (non-default export needs a name)
                    constructor() {}
                    }

الاسم المبدئي

تُستعمل في حالات معيّنة الكلمة المفتاحية default للإشارة إلى التصدير المبدئي.

فمثلًا لتصدير الدالة بنحوٍ منفصل عن تعريفها:

function sayHi(user) {
                    alert(`Hello, ${user}!`);
                    }

                    // ‫كما لو أضفنا ”export default“ قبل الدالة
                    export {sayHi as default};

أو لنقل بأنّ الوحدة user.js تُصدّر شيئًا واحدًا ”مبدئيًا“ وأخرى لها أسماء (نادرًا ما يحدث، ولكنّه يحدث):

// 📁 user.js
                    export default class User {
                    constructor(name) {
                    this.name = name;
                    }
                    }

                    export function sayHi(user) {
                    alert(`Hello, ${user}!`);
                    }

هكذا نستورد التصدير المبدئي مع ذلك الذي لديه اسم:

// 📁 main.js
                    import {default as User, sayHi} from './user.js';

                    new User('John');

وأخيرًا، حين نستورد كلّ شيء * على أنّه كائن، فستكون خاصية default هي كما التصدير المبدئي:

// 📁 main.js
                    import * as user from './user.js';

                    let User = user.default; // the default export
                    new User('John');

كلمتين بخصوص سوء التصديرات المبدئية

التصديرات التي لها أسماء تكون صريحة، أي أنّها تقول تمامًا ما الّذي يجب أن نستورده، وبذلك يكون لدينا هذه المعلومات منهم، وهذا شيء جيد.

تُجبرنا التصديرات التي لها أسماء باستعمال الاسم الصحيح كما هو بالضبط لاستيراد الوحدة:

import {User} from './user.js';
                    // ‫ولن تعمل import {MyUser}‎ إذ يجب أن يكون الاسم {User}

بينما في حالة التصدير المبدئي نختار نحن الاسم حين نستورد الوِحدة:

import User from './user.js'; // works
                    import MyUser from './user.js'; // works too
                    // ‫ويمكن أيضًا أن تكون ”استورِد كل شيء“ import Anything... وستعمل بلا أدنى مشكلة

هذا قد يؤدّي إلى أن يستعمل أعضاء الفريق أسماء مختلفة لاستيراد الشيء ذاته، وهذا طبعًا ليس بالجيد.

عادةً ولنتجنّب ذلك ونُحافظ على اتساق الشيفرة، نستعمل القاعدة القائلة بأنّ أسماء المتغيرات المُستورَدة يجب أن تُوافق أسماء الملفات، هكذا مثلًا:

import User from './user.js';
                    import LoginForm from './loginForm.js';
                    import func from '/path/to/func.js';
                    ...

مع ذلك تنظُر بعض الفِرق لهذا الأمر على أنه عقبة للتصديرات المبدئية فتفضّل استعمال التصديرات التي لها اسم دومًا. فحتّى لو كانت نصدّر شيئًا واحدًا فقط فما زالت تُصدّره باسم دون استعمال default.

كما يسهّل هذا إعادة التصدير (طالِع أسفله).

إعادة التصدير

تُتيح لنا صياغة ”إعادة التصدير“ export ... from ...‎ استيراد الأشياء وتصديرها مباشرةً (ربما باسم آخر) هكذا:

export {sayHi} from './say.js'; // ‫نُعيد تصدير sayHi

                    export {default as User} from './user.js'; // نُعيد تصدير المبدئي

ولكن فيمَ نستعمل هذا أصلًا؟ لنرى مثالًا عمليًا.

لنقل بأننا نكتب ”حزمة“، أي مجلدًا فيه وحدات كثيرة وأردنا تصدير بعض ميزاتها إلى الخارج (تتيح لنا الأدوات مثل NPM نشر هذه الحزم وتوزيعها)، ونعلم أيضًا أن الكثير من وحداتها ما هي إلّا وحدات مُساعِدة

يمكن أن تكون بنية الملفات هكذا:

auth/
                    index.js
                    user.js
                    helpers.js
                    tests/
                    login.js
                    providers/
                    github.js
                    facebook.js
                    ...

ونريد عرض مزايا الحزمة باستعمال نقطة واحدة (أي الملف الأساسي auth/index.js) لتُستعمل هكذا:

import {login, logout} from 'auth/index.js'

الفكرة هي عدم السماح للغرباء (أي المطوّرين مستعملي الحزمة) بالتعديل على البنية الداخلية والبحث عن الملفات داخل مجلد الحزمة. نريد تصدير المطلوب فقط في auth/index.js وإخفاء الباقي عن أعين المتطفّلين.

نظرًا لكون الوظيفة الفعلية المصدّرة مبعثرة بين الحزمة، يمكننا استيرادها إلى auth/index.js وتصديرها من هنالك أيضًا:

// 📁 auth/index.js

                    // ‫اِستورد login/logout وصدِرهن مباشرةً
                    import {login, logout} from './helpers.js';
                    export {login, logout};

                    // ‫استورد الملف المبدئي كـ User وصدره من جديد
                    import User from './user.js';
                    export {User};
                    ...

والآن يمكن لمستخدمي الحزمة الخاصة بنا استيرادها هكذا import {login} from "auth/index.js"‎.

إن الصياغة export ... from ...‎ ماهي إلا اختصار للاستيراد والتصدير:

// 📁 auth/index.js
                    // ‫اِستورد login/logout وصدِرهن مباشرةً
                    export {login, logout} from './helpers.js';

                    // ‫استورد الملف المبدئي كـ User وصدره من جديد
                    export {default as User} from './user.js';
                    ...

إعادة تصدير التصديرات المبدئية

يحتاج التصدير المبدئي لمعالجة منفصلة عند إعادة التصدير.

لنفترض أن لدينا user.js، ونود إعادة تصدير الصنف User منه:

// 📁 user.js
                    export default class User {
                    // ...
                    }
  1. لن تعمل التعليمة export User from './user.js'‎. ما الخطأ الذي حدث؟ ولكن هذا الخطأ في صياغة!

    لإعادة تصدير الملفات المصدرة إفتراضيًا ، علينا كتابة export {default as User}‎ ، كما في المثال أعلاه.

  2. تعيد التعليمة export * from './user.js'‎ تصدير التصديرات الّتي لها أسماء فقط، ولكنها تتجاهل التصديرات المبدئية.

    إذا رغبنا في إعادة تصدير التصديرات المبدئية والتي لها أسماء أيضًا، فسنحتاج إلى العبارتين:

    export * from './user.js'; // لإعادة تصدير التصديرات الّتي لها أسماء
                            export {default} from './user.js'; // لإعادة تصدير التصديرات المبدئية

هذه الغرابة في طريقة إعادة تصدير التصديرات المبدئية هي من أحد الأسباب لجعل بعض المطورين لا يحبونها.

خلاصة

والآن سنراجع جميع أنواع طرق التصدير export التي تحدثنا عنها في هذا الفصل والفصول السابقة.

تحقق من معلوماتك بقراءتك لهم وتذكر ما تعنيه كلُّ واحدةٍ منهم:

  • قبل التعريف عن صنف / دالّة / …:
    • export [default] class/function/variable ...‎
  • تصدير مستقل:
    • export {x [as y], ...}‎.
  • إعادة التصدير:
    • export {x [as y], ...} from "module"‎
    • export * from "module"‎ (لا يُعيد التصدير المبدئي).
    • export {default [as y]} from "module"‎ (يعيد التصدير المبدئي).

استيراد:

  • الصادرات التي لها أسماء من الوِحدة:
    • import {x [as y], ...} from "module"‎
  • التصدير المبدئي:
    • import x from "module"‎
    • import {default as x} from "module"‎
  • استيراد كل شيء:
    • import * as obj from "module"‎
  • استيراد الوحدة (وشغِّل شيفرتها البرمجية)، ولكن لا تُسندها لمتغير:
    • import "module"‎

لا يهم مكان وضع عبارات (تعليمات) import/export سواءً في أعلى أو أسفل السكربت فلن يغير ذلك أي شيء.

لذا تقنيًا تعدُّ هذه الشيفرة البرمجية لا بأس بها:

sayHi();

                    // ...

                    import {sayHi} from './say.js'; // اِستورد في نهاية الملف

عمليًا عادة ما تكون تعليمات الاستيراد في بداية الملف فقط لتنسيق أفضل للشيفرة.

لاحظ أن تعليمتي import/export لن يعملا إن كانا في داخل جملة شرطية.

لن يعمل الاستيراد الشرطي مثل هذا المثال:

if (something) {
                    import {sayHi} from "./say.js"; // Error: import must be at top level
                    }

… ولكن ماذا لو احتجنا حقًا لاستيراد شيء ما بشروط معينة؟ أو في وقتٍ ما؟ مثل: تحميل الوِحدة عند الطلب، عندما تكون هناك حاجة إليها حقًا؟

سنرى الاستيراد الديناميكي في المقالة التالية.

ترجمة -وبتصرف- للفصل Export and Import من كتاب The JavaScript language