บล็อกสตรีมแบบสดสุดเร้าใจ - การแยกโค้ด

ในสตรีมแบบสด Supercharged ล่าสุดของเรา เราได้ติดตั้งใช้งานการแยกโค้ดและการแบ่งออกเป็นกลุ่มตามเส้นทาง เมื่อใช้ HTTP/2 และโมดูล ES6 ดั้งเดิม เทคนิคเหล่านี้จะกลายเป็นสิ่งจําเป็นในการโหลดและแคชทรัพยากรสคริปต์อย่างมีประสิทธิภาพ

เคล็ดลับและคำแนะนำอื่นๆ ในตอนนี้

  • asyncFunction().catch() ที่มี error.stack: 9:55
  • โมดูลและแอตทริบิวต์ nomodule ในแท็ก <script>: 7:30
  • promisify() ในโหนด 8: 17:20

TL;DR

วิธีแยกโค้ดผ่านการแบ่งกลุ่มตามเส้นทาง

  1. ดูรายการจุดแรกเข้า
  2. ดึงข้อมูลโมดูล Dependency ของจุดแรกเข้าทั้งหมดเหล่านี้
  3. ค้นหา Dependency ที่แชร์ระหว่างจุดแรกเข้าทั้งหมด
  4. รวมไฟล์แนบที่ใช้ร่วมกัน
  5. เขียนจุดแรกเข้าใหม่

การแยกโค้ดกับการแบ่งกลุ่มตามเส้นทาง

การแยกโค้ดและการแบ่งกลุ่มตามเส้นทางมีความเกี่ยวข้องกันมาก และมักใช้แทนกันได้ ซึ่งทำให้เกิดความสับสน เรามาลองทำความเข้าใจเรื่องนี้กัน

  • การแยกโค้ด: การแยกโค้ดเป็นกระบวนการแยกโค้ดออกเป็นหลายกลุ่ม หากคุณไม่ได้จัดส่งกลุ่มใหญ่ที่มี JavaScript ทั้งหมดไปยังไคลเอ็นต์ แสดงว่าคุณกำลังแยกโค้ด วิธีหนึ่งในการแยกโค้ดคือการแบ่งออกเป็นกลุ่มตามเส้นทาง
  • การแบ่งกลุ่มตามเส้นทาง: การแบ่งกลุ่มตามเส้นทางจะสร้างกลุ่มที่เกี่ยวข้องกับเส้นทางของแอป การวิเคราะห์เส้นทางและข้อกําหนดของเส้นทางจะช่วยให้เราเปลี่ยนโมดูลที่จะรวมไว้ในแพ็กเกจได้

เหตุผลที่ควรแยกโค้ด

โมดูลหลวม

เมื่อใช้โมดูล ES6 เนทีฟ โมดูล JavaScript แต่ละโมดูลจะนําเข้าทรัพยากรของตนเองได้ เมื่อเบราว์เซอร์ได้รับโมดูล คำสั่ง import ทั้งหมดจะเรียกใช้การดึงข้อมูลเพิ่มเติมเพื่อรับโมดูลที่จําเป็นต่อการเรียกใช้โค้ด อย่างไรก็ตาม โมดูลทั้งหมดเหล่านี้อาจมีทรัพยากรอ้างอิงของตนเอง อันตรายคือเบราว์เซอร์จะทำการดึงข้อมูลหลายครั้งติดต่อกันเป็นเวลานานก่อนที่จะดําเนินการโค้ดได้

การรวมกลุ่ม

การรวม ซึ่งก็คือการฝังโมดูลทั้งหมดไว้ในแพ็กเกจเดียว จะช่วยให้มั่นใจได้ว่าเบราว์เซอร์จะมีโค้ดทั้งหมดที่จำเป็นหลังจากการติดต่อ 1 รอบ และสามารถเริ่มเรียกใช้โค้ดได้เร็วขึ้น อย่างไรก็ตาม วิธีนี้บังคับให้ผู้ใช้ดาวน์โหลดโค้ดจำนวนมากที่ไม่จำเป็น จึงเป็นการสิ้นเปลืองแบนด์วิดท์และเวลา นอกจากนี้ การเปลี่ยนแปลงใดๆ ในโมดูลเดิมของเราจะส่งผลให้มีการเปลี่ยนในแพ็กเกจ ซึ่งจะทำให้แพ็กเกจเวอร์ชันแคชไว้ใช้งานไม่ได้ ผู้ใช้จะต้องดาวน์โหลดทั้งหมดอีกครั้ง

การแยกโค้ด

การแยกโค้ดเป็นทางสายกลาง เรายินดีลงทุนเพิ่มในการส่งข้อมูลไปมาเพื่อรับประสิทธิภาพเครือข่ายด้วยการดาวน์โหลดเฉพาะสิ่งที่ต้องการ และเพิ่มประสิทธิภาพการแคชให้ดียิ่งขึ้นด้วยการทำจำนวนโมดูลต่อแต่ละกลุ่มให้เล็กลงมาก หากการรวมกลุ่มทําอย่างถูกต้อง จํานวนรอบทั้งหมดจะต่ำกว่ามากเมื่อเทียบกับการใช้ข้อบังคับแบบหลวม สุดท้าย เราอาจใช้กลไกการโหลดล่วงหน้า เช่น link[rel=preload] เพื่อประหยัดเวลาในรอบที่ 3 เพิ่มเติม หากจำเป็น

ขั้นตอนที่ 1: ดูรายการจุดแรกเข้า

นี่เป็นเพียงวิธีหนึ่งในหลายๆ วิธี แต่ในตอนนี้ เราได้แยกวิเคราะห์sitemap.xml ของเว็บไซต์เพื่อหาจุดแรกเข้าของเว็บไซต์ โดยปกติแล้วจะใช้ไฟล์ JSON โดยเฉพาะซึ่งแสดงรายการจุดแรกเข้าทั้งหมด

การใช้ Babel เพื่อประมวลผล JavaScript

Babel มักใช้สำหรับ "การแปลง" ซึ่งก็คือการใช้โค้ด JavaScript เวอร์ชันล่าสุดและเปลี่ยนเป็น JavaScript เวอร์ชันเก่าเพื่อให้เบราว์เซอร์จำนวนมากขึ้นเรียกใช้โค้ดได้ ขั้นตอนแรกคือการแยกวิเคราะห์ JavaScript ใหม่ด้วยโปรแกรมแยกวิเคราะห์ (Babel ใช้ babylon) ซึ่งจะเปลี่ยนโค้ดให้เป็น "Abstract Syntax Tree" (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: ค้นหารายการที่ต้องใช้ร่วมกันระหว่างจุดแรกเข้าทั้งหมด

เนื่องจากเรามีชุดต้นไม้ของ Dependency หรือป่า Dependency นั่นเอง เราจึงค้นหา Dependency ที่แชร์ได้โดยมองหาโหนดที่ปรากฏในทุกต้นไม้ เราจะผสานและกรองข้อมูลฟอเรสต์เพื่อเก็บเฉพาะองค์ประกอบที่ปรากฏในต้นไม้ทั้งหมด

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: รวม Dependency ที่แชร์

หากต้องการรวมชุดของ Dependency ที่แชร์ เราสามารถต่อไฟล์โมดูลทั้งหมดเข้าด้วยกันได้ การใช้แนวทางดังกล่าวจะทำให้เกิดปัญหา 2 อย่าง ปัญหาแรกคือกลุ่มจะยังคงมีคำสั่ง import ซึ่งจะทำให้เบราว์เซอร์พยายามดึงข้อมูล ปัญหาที่ 2 คือ Dependency ของ Dependency ไม่ได้รวมไว้ เนื่องจากเราเคยทำมาแล้ว เราจะเขียนปลั๊กอิน 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

ไว้พบกันใหม่นะคะ