مدونة بث مباشر فعّالة - تقسيم الرموز

في البث المباشر الأخير Supercharged، طبّقنا تقسيم الرموز والتقسيم استنادًا إلى المسار. باستخدام HTTP/2 ووحدات ES6 الأصلية، ستصبح هذه الأساليب ضرورية لإتاحة إمكانية التحميل الفعال لموارد النصوص البرمجية وتخزينها مؤقتًا.

نصائح وحيل متنوعة في هذه الحلقة

  • asyncFunction().catch() باستخدام "error.stack": 9:55
  • الوحدات وسمة nomodule في علامات <script>: 7:30
  • promisify() في العقدة 8: 17:20

الملخّص

كيفية تقسيم التعليمة البرمجية عبر التقسيم المستند إلى المسار:

  1. احصل على قائمة بنقاط الدخول.
  2. استخرِج تبعيات الوحدة لكل نقاط الدخول هذه.
  3. ابحث عن التبعيات المشتركة بين جميع نقاط الدخول.
  4. تجميع التبعيات المشتركة.
  5. أعِد كتابة نقاط الإدخال.

تقسيم التعليمة البرمجية مقابل التقسيم المستند إلى المسار

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

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

لماذا يتم تقسيم الرمز؟

الوحدات غير الصالحة

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

التجميع

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

تقسيم الرمز

تقسيم التعليمة البرمجية هو الأساس. ونحن على استعداد لاستثمار المزيد من عمليات الإرسال والاستقبال لتحسين كفاءة الشبكة من خلال تنزيل ما نحتاجه فقط، وتحسين كفاءة التخزين المؤقت من خلال تصغير عدد الوحدات لكل حزمة. إذا تم التجميع بشكل صحيح، فسيكون إجمالي عدد جولات الذهاب والإياب أقل بكثير مقارنةً بالوحدات الحرة. أخيرًا، يمكننا الاستفادة من آليات التحميل المُسبَق، مثل link[rel=preload] لتوفير مُدد جولات ثلاثية إضافية إذا لزم الأمر.

الخطوة الأولى: الحصول على قائمة بنقاط الدخول

وهذه طريقة واحدة فقط من بين عدّة طرق، ولكن في الحلقة، حلّلنا sitemap.xml للموقع الإلكتروني للحصول على نقاط الدخول إلى موقعنا الإلكتروني. عادةً، يتم استخدام ملف JSON مخصص يسرد جميع نقاط الدخول.

استخدام babel لمعالجة JavaScript

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

سوف نستعين بأداة babel بشكل مكثّف لرصد عمليات استيراد وحدة من JavaScript (ومعالجتها لاحقًا). قد تميل إلى اللجوء إلى التعبيرات العادية، لكن التعبيرات العادية ليست قوية بما يكفي لتحليل اللغة بشكل صحيح ويصعب الحفاظ عليها. إن الاعتماد على الأدوات المجربة مثل Babel سيوفر عليك الكثير من المتاعب.

في ما يلي مثال بسيط على تشغيل Babel باستخدام مكوّن إضافي مخصّص:

const plugin = {
  visitor: {
    ImportDeclaration(decl) {
      /* ... */
    }
  }
}
const {code} = babel.transform(inputCode, {plugins: [plugin]});

يمكن أن توفّر المكوّن الإضافي كائن visitor. يحتوي الزائر على دالة لأي نوع عقدة يريد المكون الإضافي التعامل معه. عند مصادفة عقدة من هذا النوع أثناء اجتياز AST، سيتم استدعاء الدالة المتجاوبة في الكائن visitor مع هذه العقدة كمَعلمة. في المثال أعلاه، سيتم استدعاء طريقة ImportDeclaration() لكل تعريف import في الملف. للتعرّف على أنواع العُقد ومؤشر AST، يمكنك الاطّلاع على astexplorer.net.

الخطوة 2: استخراج تبعيات الوحدة

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

async function buildDependencyTree(file) {
  let code = await readFile(file);
  code = code.toString('utf-8');

  // `dep` will collect all dependencies of `file`
  let dep = [];
  const plugin = {
    visitor: {
      ImportDeclaration(decl) {
        const importedFile = decl.node.source.value;
        // Recursion: Push an array of the dependency’s dependencies onto the list
        dep.push((async function() {
          return await buildDependencyTree(`./app/${importedFile}`);
        })());
        // Push the dependency itself onto the list
        dep.push(importedFile);
      }
    }
  }
  // Run the plugin
  babel.transform(code, {plugins: [plugin]});
  // Wait for all promises to resolve and then flatten the array
  return flatten(await Promise.all(dep));
}

الخطوة 3: البحث عن التبعيات المشتركة بين جميع نقاط الدخول

بما أنّ لدينا مجموعة من أشجار التبعية، وهي غابة تبعية إن أردت، يمكننا العثور على التبعيات المشتركة من خلال البحث عن العُقد التي تظهر في كل شجرة. سنقوم بتبسيط وإزالة تكرار الغابة والتصفية للحفاظ فقط على العناصر التي تظهر في جميع الأشجار.

function findCommonDeps(depTrees) {
  const depSet = new Set();
  // Flatten
  depTrees.forEach(depTree => {
    depTree.forEach(dep => depSet.add(dep));
  });
  // Filter
  return Array.from(depSet)
    .filter(dep => depTrees.every(depTree => depTree.includes(dep)));
}

الخطوة 4: تجميع التبعيات المشتركة

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

تتشابه الرمز البرمجي إلى حد ما مع المكوّن الإضافي الأول، ولكن بدلاً من استخراج عمليات الاستيراد، سنزيلها أيضًا ونُدرج نسخة مجمّعة من الملف المستورَد:

async function bundle(oldCode) {
  // `newCode` will be filled with code fragments that eventually form the bundle.
  let newCode = [];
  const plugin = {
    visitor: {
      ImportDeclaration(decl) {
        const importedFile = decl.node.source.value;
        newCode.push((async function() {
          // Bundle the imported file and add it to the output.
          return await bundle(await readFile(`./app/${importedFile}`));
        })());
        // Remove the import declaration from the AST.
        decl.remove();
      }
    }
  };
  // Save the stringified, transformed AST. This code is the same as `oldCode`
  // but without any import statements.
  const {code} = babel.transform(oldCode, {plugins: [plugin]});
  newCode.push(code);
  // `newCode` contains all the bundled dependencies as well as the
  // import-less version of the code itself. Concatenate to generate the code
  // for the bundle.
  return flatten(await Promise.all(newCode)).join('\n');
}

الخطوة 5: إعادة كتابة نقاط الدخول

في الخطوة الأخيرة، سنكتب مكوّنًا إضافيًا آخر لـ Babel. مهمتها هي إزالة جميع عمليات استيراد الوحدات الموجودة في الحزمة المشتركة.

async function rewrite(section, sharedBundle) {
  let oldCode = await readFile(`./app/static/${section}.js`);
  oldCode = oldCode.toString('utf-8');
  const plugin = {
    visitor: {
      ImportDeclaration(decl) {
        const importedFile = decl.node.source.value;
        // If this import statement imports a file that is in the shared bundle, remove it.
        if(sharedBundle.includes(importedFile))
          decl.remove();
      }
    }
  };
  let {code} = babel.transform(oldCode, {plugins: [plugin]});
  // Prepend an import statement for the shared bundle.
  code = `import '/static/_shared.js';\n${code}`;
  await writeFile(`./app/static/_${section}.js`, code);
}

الانتهاء

كانت هذه الرحلة مفيدة، أليس كذلك؟ يُرجى العِلم أنّ هدفنا من هذه الحلقة كان شرح كيفية تقسيم الرموز البرمجية وتوضيحها. والنتيجة جيدة، ولكنها تقتصر على موقعنا الإلكتروني الذي يتضمّن عرضًا توضيحيًا، وستفشل بشكل كبير في الحالة العامة. في مرحلة الإنتاج، أوصي بالاعتماد على أدوات معروفة مثل WebPack وrollUp وما إلى ذلك.

يمكنك العثور على الرمز البرمجي الخاص بنا في مستودع جيت هب.

مع أطيب التحيات،