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