हाल ही में हुई Supercharged लाइव स्ट्रीम में, हमने कोड को अलग-अलग हिस्सों में बांटने और रूट के हिसाब से अलग-अलग हिस्सों में बांटने की सुविधा लागू की है. एचटीटीपी/2 और नेटिव ES6 मॉड्यूल के साथ, स्क्रिप्ट रिसॉर्स को बेहतर तरीके से लोड करने और कैश मेमोरी में सेव करने के लिए, ये तकनीकें ज़रूरी हो जाएंगी.
इस एपिसोड में अन्य सुझाव और सलाह
asyncFunction().catch()
के साथerror.stack
: 9:55<script>
टैग पर मॉड्यूल औरnomodule
एट्रिब्यूट: 7:30- आठवें नोड में
promisify()
: 17:20
कम शब्दों में कहा जाए तो
रूट के हिसाब से चंक करने की सुविधा का इस्तेमाल करके, कोड को अलग-अलग करने का तरीका:
- अपने एंट्री पॉइंट की सूची पाएं.
- इन सभी एंट्री पॉइंट की मॉड्यूल डिपेंडेंसी निकालें.
- सभी एंट्री पॉइंट के बीच शेयर की गई डिपेंडेंसी ढूंढें.
- शेयर की गई डिपेंडेंसी को बंडल करें.
- एंट्री पॉइंट फिर से लिखें.
कोड को अलग-अलग हिस्सों में बांटना बनाम रूट के हिसाब से अलग-अलग हिस्सों में बांटना
कोड को अलग-अलग हिस्सों में बांटने और रूट के हिसाब से अलग-अलग हिस्सों में बांटने की सुविधा एक-दूसरे से मिलती-जुलती है. साथ ही, इनका इस्तेमाल अक्सर एक-दूसरे के बदले किया जाता है. इससे कुछ उलझन पैदा हुई है. आइए, इस बारे में ज़्यादा जानकारी देते हैं:
- कोड को अलग-अलग करने की सुविधा: कोड को अलग-अलग करने की सुविधा का इस्तेमाल करके, अपने कोड को कई बंडलों में बांटा जा सकता है. अगर क्लाइंट को अपने सभी JavaScript के साथ एक बड़ा बंडल नहीं भेजा जा रहा है, तो इसका मतलब है कि कोड को अलग-अलग किया जा रहा है. कोड को अलग-अलग करने का एक खास तरीका, रूट के हिसाब से चंकिंग का इस्तेमाल करना है.
- रूट के हिसाब से चंक करने की सुविधा: रूट के हिसाब से चंक करने की सुविधा, आपके ऐप्लिकेशन के रूट से जुड़े बंडल बनाती है. आपके रूट और उनकी डिपेंडेंसी का विश्लेषण करके, हम यह बदल सकते हैं कि कौनसे मॉड्यूल किस बंडल में शामिल हों.
कोड को अलग-अलग हिस्सों में क्यों बांटते हैं?
अलग-अलग मॉड्यूल
नेटिव ES6 मॉड्यूल की मदद से, हर JavaScript मॉड्यूल अपनी डिपेंडेंसी इंपोर्ट कर सकता है. जब ब्राउज़र को कोई मॉड्यूल मिलता है, तो कोड को चलाने के लिए ज़रूरी मॉड्यूल हासिल करने के लिए, सभी import
स्टेटमेंट अतिरिक्त फ़ेच ट्रिगर करेंगे. हालांकि, इन सभी मॉड्यूल में अपनी डिपेंडेंसी हो सकती हैं. इस वजह से, ब्राउज़र को कई बार फ़ेच करने की ज़रूरत पड़ती है. इससे कोड को लागू होने में काफ़ी समय लगता है.
बंडलिंग
बंडलिंग, आपके सभी मॉड्यूल को एक ही बंडल में इनलाइन करती है. इससे यह पक्का होता है कि ब्राउज़र में एक बार के दौरे के बाद, ज़रूरी सभी कोड मौजूद हों और वह कोड को तेज़ी से चलाना शुरू कर सके. हालांकि, इससे उपयोगकर्ता को बहुत सारा ऐसा कोड डाउनलोड करना पड़ता है जिसकी ज़रूरत नहीं होती. इसलिए, बैंडविड्थ और समय बर्बाद हो जाता है. इसके अलावा, हमारे किसी भी ओरिजनल मॉड्यूल में किया गया हर बदलाव, बंडल में बदलाव कर देगा. इससे बंडल के कैश मेमोरी में सेव किए गए किसी भी वर्शन को अमान्य कर दिया जाएगा. उपयोगकर्ताओं को फिर से पूरी चीज़ डाउनलोड करनी होगी.
कोड को अलग-अलग हिस्सों में बांटना
कोड को अलग-अलग हिस्सों में बांटना, इन दोनों के बीच का विकल्प है. हम नेटवर्क की परफ़ॉर्मेंस को बेहतर बनाने के लिए, ज़्यादा राउंड ट्रिप का इस्तेमाल करने को तैयार हैं. इसके लिए, हम सिर्फ़ ज़रूरी कॉन्टेंट डाउनलोड करेंगे. साथ ही, हर बंडल में मॉड्यूल की संख्या को बहुत कम करके, कैश मेमोरी का बेहतर इस्तेमाल करेंगे. अगर बंडलिंग सही तरीके से की गई है, तो लूज़ मॉड्यूल की तुलना में, राउंड ट्रिप की कुल संख्या काफ़ी कम होगी. आखिर में, ज़रूरत पड़ने पर हम link[rel=preload]
जैसे पहले से लोड करने के तरीकों का इस्तेमाल करके, तीनों राउंड में लगने वाले समय को कम कर सकते हैं.
पहला चरण: एंट्री पॉइंट की सूची पाना
यह कई तरीकों में से एक है. हालांकि, इस एपिसोड में हमने अपनी वेबसाइट के एंट्री पॉइंट पाने के लिए, वेबसाइट के sitemap.xml
को पार्स किया है. आम तौर पर, सभी एंट्री पॉइंट की सूची वाली खास JSON फ़ाइल का इस्तेमाल किया जाता है.
JavaScript को प्रोसेस करने के लिए babel का इस्तेमाल करना
Babel का इस्तेमाल आम तौर पर “ट्रांसपाइल करने” के लिए किया जाता है: इसमें नए वर्शन के JavaScript कोड का इस्तेमाल करके, उसे JavaScript के पुराने वर्शन में बदला जाता है, ताकि ज़्यादा ब्राउज़र उस कोड को चला सकें. यहां पहला चरण, किसी पार्स करने वाले टूल (Babel, babylon का इस्तेमाल करता है) की मदद से नए JavaScript को पार्स करना है. यह टूल, कोड को "ऐब्स्ट्रैक्ट सिंटैक्स ट्री" (AST) में बदल देता है. एएसटी जनरेट होने के बाद, प्लगिन की एक सीरीज़ एएसटी का विश्लेषण करती है और उसमें बदलाव करती है.
हम babel का ज़्यादा से ज़्यादा इस्तेमाल करेंगे, ताकि JavaScript मॉड्यूल के इंपोर्ट का पता लगाया जा सके और बाद में उनमें बदलाव किया जा सके. हो सकता है कि आप रेगुलर एक्सप्रेशन का इस्तेमाल करना चाहें, लेकिन रेगुलर एक्सप्रेशन किसी भाषा को सही तरीके से पार्स नहीं कर सकते. साथ ही, इनका रखरखाव करना मुश्किल होता है. Babel जैसे पहले से आज़माए गए टूल का इस्तेमाल करने से, आपको कई समस्याओं से बचने में मदद मिलेगी.
कस्टम प्लग इन के साथ Babel को चलाने का एक आसान उदाहरण यहां दिया गया है:
const plugin = {
visitor: {
ImportDeclaration(decl) {
/* ... */
}
}
}
const {code} = babel.transform(inputCode, {plugins: [plugin]});
प्लग इन, visitor
ऑब्जेक्ट दे सकता है. विज़िटर में, किसी भी तरह के ऐसे नोड के लिए फ़ंक्शन होता है जिसे प्लग इन मैनेज करना चाहता है. एएसटी को ट्रैवर्स करते समय, उस टाइप का कोई नोड मिलने पर, visitor
ऑब्जेक्ट में मौजूद उससे जुड़े फ़ंक्शन को पैरामीटर के तौर पर उस नोड के साथ लागू किया जाएगा. ऊपर दिए गए उदाहरण में, फ़ाइल में हर import
एलान के लिए ImportDeclaration()
तरीका इस्तेमाल किया जाएगा. नोड टाइप और एएसटी के बारे में ज़्यादा जानने के लिए, astexplorer.net पर जाएं.
दूसरा चरण: मॉड्यूल की डिपेंडेंसी निकालना
किसी मॉड्यूल का डिपेंडेंसी ट्री बनाने के लिए, हम उस मॉड्यूल को पार्स करेंगे और उन सभी मॉड्यूल की सूची बनाएंगे जिन्हें वह इंपोर्ट करता है. हमें उन डिपेंडेंसी को भी पार्स करना होगा, क्योंकि हो सकता है कि उनमें भी डिपेंडेंसी हों. यह फ़ंक्शन, बार-बार दोहराए जाने वाले फ़ंक्शन का एक क्लासिक उदाहरण है!
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));
}
तीसरा चरण: सभी एंट्री पॉइंट के बीच शेयर की गई डिपेंडेंसी ढूंढना
हमारे पास डिपेंडेंसी ट्री का एक सेट है – इसे डिपेंडेंसी फ़ॉरेस्ट भी कहा जा सकता है. इसलिए, हर ट्री में दिखने वाले नोड की मदद से, शेयर की गई डिपेंडेंसी ढूंढी जा सकती हैं. हम अपने फ़ॉरेस्ट को फ़्लैट करेंगे और डुप्लीकेट एलिमेंट हटा देंगे. साथ ही, हम सिर्फ़ उन एलिमेंट को फ़िल्टर करेंगे जो सभी ट्री में दिखते हैं.
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)));
}
चौथा चरण: शेयर की गई डिपेंडेंसी को बंडल करना
शेयर की गई डिपेंडेंसी के सेट को बंडल करने के लिए, हम सभी मॉड्यूल फ़ाइलों को जोड़ सकते हैं. इस तरीके का इस्तेमाल करने पर, दो समस्याएं आती हैं: पहली समस्या यह है कि बंडल में अब भी import
स्टेटमेंट मौजूद होंगे. इनकी वजह से, ब्राउज़र संसाधनों को फ़ेच करने की कोशिश करेगा. दूसरी समस्या यह है कि डिपेंडेंसी की डिपेंडेंसी को बंडल नहीं किया गया है. हमने पहले भी ऐसा किया है. इसलिए, हम 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');
}
पांचवां चरण: एंट्री पॉइंट फिर से लिखना
आखिरी चरण के लिए, हम एक और 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);
}
End
यह वाकई एक शानदार अनुभव था, क्या नहीं? कृपया याद रखें कि इस एपिसोड का हमारा मकसद, कोड को अलग-अलग हिस्सों में बांटने के बारे में जानकारी देना और इसे आसान बनाना था. यह तरीका काम करता है – लेकिन यह हमारी डेमो साइट के लिए खास तौर पर है और सामान्य तौर पर, यह काम नहीं करेगा. हमारा सुझाव है कि प्रोडक्शन के लिए, WebPack, RollUp वगैरह जैसे टूल का इस्तेमाल करें.
हमारा कोड, GitHub रिपॉज़िटरी में देखा जा सकता है.
अगली बार मिलते हैं!