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

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

เคล็ดลับและคำแนะนำเบ็ดเตล็ดในรายการตอนนี้

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

สรุปคร่าวๆ

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

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

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

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

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

ทำไมจึงต้องแยกโค้ด

โมดูลแบบไม่เจาะจง

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

แพ็กเกจ

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

การแยกโค้ด

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

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

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

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

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

ในการสร้างโครงสร้างทรัพยากร Dependency ของโมดูล เราจะแยกวิเคราะห์โมดูลดังกล่าวและสร้างรายการโมดูลทั้งหมดที่นําเข้า นอกจากนี้เรายังต้องแยกวิเคราะห์ทรัพยากร 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

แล้วพบกันใหม่โอกาสหน้า