ในสตรีมแบบสด Supercharged ครั้งล่าสุด เราได้ใช้การแยกโค้ดและการแบ่งส่วนเนื้อหาตามเส้นทาง เมื่อใช้โมดูล HTTP/2 และ ES6 แบบเนทีฟ เทคนิคเหล่านี้จะกลายเป็นสิ่งสำคัญในการเปิดใช้การโหลดและแคชทรัพยากรสคริปต์อย่างมีประสิทธิภาพ
เคล็ดลับและคำแนะนำเบ็ดเตล็ดในรายการตอนนี้
asyncFunction().catch()
กับerror.stack
: 9:55- โมดูลและแอตทริบิวต์
nomodule
ในแท็ก<script>
: 7:30 promisify()
ในโหนด 8: 17:20
สรุปคร่าวๆ
วิธีแยกโค้ดผ่านการแบ่งส่วนตามเส้นทาง:
- รับรายการจุดแรกเข้า
- แยกทรัพยากร Dependency ของโมดูลของจุดแรกเข้าเหล่านี้ทั้งหมด
- ค้นหาทรัพยากร Dependency ที่ใช้ร่วมกันระหว่างจุดแรกเข้าทั้งหมด
- รวมทรัพยากร Dependency ที่แชร์
- เขียนจุดแรกเข้าใหม่
การแยกโค้ดเทียบกับการแบ่งส่วนตามเส้นทาง
การแยกโค้ดและการแบ่งออกเป็นส่วนๆ ตามเส้นทางมีความเกี่ยวข้องกันอย่างใกล้ชิดและมักใช้สลับกันได้ ซึ่งก่อให้เกิดความสับสน เรามาลองแก้ปัญหานี้กัน
- การแยกโค้ด: การแยกโค้ดเป็นกระบวนการแยกโค้ดออกเป็นหลายๆ แพ็กเกจ หากคุณไม่ได้จัดส่งแพ็กเกจขนาดใหญ่ 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
แล้วพบกันใหม่โอกาสหน้า