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

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

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

โมดูลหลวม

เมื่อใช้โมดูล 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: ดึงข้อมูลทรัพยากร Dependency ของโมดูล

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

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

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