وبلاگ پخش زنده فوق العاده - تقسیم کد

در جدیدترین Supercharged Livestream خود، تقسیم کد و تکه‌شدن مبتنی بر مسیر را پیاده‌سازی کردیم. با HTTP/2 و ماژول‌های بومی ES6، این تکنیک‌ها برای فعال کردن بارگذاری و ذخیره‌سازی کارآمد منابع اسکریپت ضروری خواهند بود.

نکات و ترفندهای متفرقه در این قسمت

  • asyncFunction().catch() با error.stack : 9:55
  • ماژول ها و ویژگی nomodule در برچسب های <script> : 7:30
  • promisify() در گره 8: 17:20

TL; DR

نحوه انجام تقسیم کد از طریق تکه تکه سازی مبتنی بر مسیر:

  1. لیستی از نقاط ورود خود را دریافت کنید.
  2. وابستگی های ماژول همه این نقاط ورودی را استخراج کنید.
  3. وابستگی های مشترک بین تمام نقاط ورودی را پیدا کنید.
  4. بسته‌بندی وابستگی‌های مشترک
  5. نقاط ورودی را دوباره بنویسید.

تقسیم کد در مقابل تکه تکه شدن مبتنی بر مسیر

تقسیم کد و قطعه‌سازی مبتنی بر مسیر ارتباط نزدیکی با هم دارند و اغلب به جای یکدیگر استفاده می‌شوند. این باعث ایجاد سردرگمی هایی شده است. بیایید سعی کنیم این را روشن کنیم:

  • تقسیم کد : تقسیم کد فرآیند تقسیم کد شما به چند بسته است. اگر یک بسته بزرگ با تمام جاوا اسکریپت خود را به مشتری ارسال نمی کنید، در حال انجام تقسیم کد هستید. یکی از راه‌های خاص برای تقسیم کد، استفاده از قطعه‌سازی مبتنی بر مسیر است.
  • تکه‌شکل‌سازی مبتنی بر مسیر : تکه‌شدن مبتنی بر مسیر، بسته‌هایی ایجاد می‌کند که به مسیرهای برنامه شما مرتبط هستند. با تجزیه و تحلیل مسیرهای شما و وابستگی‌های آنها، می‌توانیم ماژول‌هایی را که در کدام بسته قرار می‌گیرند تغییر دهیم.

چرا تقسیم کد؟

ماژول های شل

با ماژول های بومی ES6، هر ماژول جاوا اسکریپت می تواند وابستگی های خود را وارد کند. هنگامی که مرورگر یک ماژول را دریافت می کند، تمام دستورات import واکشی های اضافی را برای به دست آوردن ماژول هایی که برای اجرای کد ضروری هستند، راه اندازی می کنند. با این حال، همه این ماژول ها می توانند وابستگی های خاص خود را داشته باشند. خطر این است که مرورگر به مجموعه‌ای از واکشی‌ها ختم می‌شود که برای چندین بار رفت و برگشت قبل از اینکه کد در نهایت اجرا شود، دوام می‌آورد.

بسته بندی

بسته‌بندی، که همه ماژول‌های شما را در یک بسته منفرد قرار می‌دهد، مطمئن می‌شود که مرورگر تمام کد مورد نیاز خود را پس از ۱ رفت و برگشت دارد و می‌تواند با سرعت بیشتری کد را اجرا کند. با این حال، این کاربر را مجبور می کند تا کدهای زیادی را دانلود کند که مورد نیاز نیست، بنابراین پهنای باند و زمان تلف شده است. علاوه بر این، هر تغییری در یکی از ماژول‌های اصلی ما منجر به تغییر در بسته می‌شود و هر نسخه حافظه پنهان بسته را باطل می‌کند. کاربران باید همه چیز را دوباره دانلود کنند.

تقسیم کد

تقسیم کد راه میانی است. ما حاضریم سفرهای رفت و برگشت اضافی را سرمایه گذاری کنیم تا با دانلود آنچه نیاز داریم، کارایی شبکه را به دست آوریم و با کوچکتر کردن تعداد ماژول ها در هر بسته، کارایی ذخیره سازی بهتر را داشته باشیم. اگر بسته بندی به درستی انجام شود، تعداد کل رفت و برگشت ها بسیار کمتر از ماژول های شل خواهد بود. در نهایت، می‌توانیم از مکانیسم‌های پیش‌بارگیری مانند link[rel=preload] برای ذخیره زمان‌های سه‌گانه دور اضافی در صورت نیاز استفاده کنیم.

مرحله 1: فهرستی از نقاط ورود خود را به دست آورید

این تنها یکی از بسیاری از رویکردها است، اما در قسمت ما sitemap.xml وب سایت را تجزیه کردیم تا نقاط ورود به وب سایت خود را دریافت کنیم. معمولاً از یک فایل JSON اختصاصی که تمام نقاط ورودی را فهرست می کند استفاده می شود.

استفاده از babel برای پردازش جاوا اسکریپت

Babel معمولاً برای "transpiling" استفاده می شود: مصرف کدهای جاوا اسکریپت لبه دار و تبدیل آن به نسخه قدیمی جاوا اسکریپت به طوری که مرورگرهای بیشتری قادر به اجرای کد باشند. اولین قدم در اینجا تجزیه جاوا اسکریپت جدید با یک تجزیه کننده است (Babel از babylon استفاده می کند) که کد را به اصطلاح "درخت نحو انتزاعی" (AST) تبدیل می کند. هنگامی که AST تولید شد، یک سری از افزونه ها AST را تجزیه و تحلیل و خراب می کنند.

ما قصد داریم از 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 و غیره تکیه کنید.

می توانید کد ما را در مخزن GitHub بیابید.

بعدا می بینمت!