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