लाइव स्ट्रीम को सुपरचार्ज किया गया ब्लॉग - कोड को अलग-अलग हिस्सों में बांटना

हाल ही की अपनी सुपरचार्ज्ड लाइव स्ट्रीम में, हमने कोड को अलग-अलग हिस्सों में बांटने और रूट के हिसाब से छोटे-छोटे हिस्सों में भेजने की सुविधा लागू की. एचटीटीपी/2 और नेटिव ES6 मॉड्यूल के साथ, ये तकनीकें स्क्रिप्ट संसाधनों को बेहतर तरीके से लोड करने और कैश मेमोरी में सेव करने के लिए ज़रूरी हो जाएंगी.

इस एपिसोड में बताए गए अलग-अलग सुझाव और तरकीबें

  • error.stack के साथ asyncFunction().catch(): 9:55
  • <script> टैग पर मॉड्यूल और nomodule एट्रिब्यूट: 7:30
  • नोड 8 में promisify(): 17:20

बहुत ज़्यादा शब्द हैं, पढ़ा नहीं गया

रूट के हिसाब से चंकिंग की मदद से कोड को बांटने का तरीका:

  1. अपने एंट्री पॉइंट की सूची पाएं.
  2. इन सभी एंट्री पॉइंट की मॉड्यूल डिपेंडेंसी निकालें.
  3. सभी एंट्री पॉइंट के बीच, शेयर की गई डिपेंडेंसी खोजें.
  4. शेयर की गई डिपेंडेंसी को इकट्ठा करें.
  5. एंट्री पॉइंट फिर से लिखें.

कोड को अलग-अलग हिस्सों में बांटना बनाम रूट के हिसाब से चंकिंग करना

कोड के अलग-अलग हिस्सों में और रूट के हिसाब से एक-दूसरे से जुड़े हुए हैं. इन्हें अक्सर अलग-अलग तरीके से इस्तेमाल किया जाता है. इससे कुछ हद तक उलझन होती है. आइए, इसे साफ़ करने की कोशिश करते हैं:

  • कोड को अलग-अलग हिस्सों में बांटना: कोड को अलग-अलग ग्रुप में बांटने की प्रक्रिया में, आपके कोड को एक से ज़्यादा बंडल में बांटा जाता है. अगर आपको अपने सभी JavaScript के साथ एक बड़ा बंडल क्लाइंट को भेजना नहीं है, तो इसका मतलब है कि कोड को अलग-अलग किया जा रहा है. कोड को अलग-अलग हिस्सों में बांटने का एक खास तरीका है, रूट के हिसाब से चंकिंग करना.
  • रूट के हिसाब से चंकिंग: रूट के हिसाब से चंकिंग करने पर, ऐसे बंडल बनाए जाते हैं जो आपके ऐप्लिकेशन के रूट से जुड़े होते हैं. आपके रूट और उनकी डिपेंडेंसी का विश्लेषण करके, हम यह बदल सकते हैं कि कौनसे मॉड्यूल किस बंडल में जाएं.

कोड को अलग-अलग हिस्सों में क्यों बांटा जाता है?

लूज़ मॉड्यूल

नेटिव ES6 मॉड्यूल के साथ, हर JavaScript मॉड्यूल अपनी डिपेंडेंसी इंपोर्ट कर सकता है. ब्राउज़र को कोई मॉड्यूल मिलने पर, सभी import स्टेटमेंट अतिरिक्त फ़ेच ट्रिगर करेंगे, ताकि कोड को चलाने के लिए ज़रूरी मॉड्यूल रोके जा सकें. हालांकि, ये सभी मॉड्यूल अपनी खुद की ज़रूरतों पर निर्भर हो सकते हैं. जोखिम यह है कि ब्राउज़र में कई बार फ़ेच हो जाते हैं, जो कोड के काम करने से पहले ही कई राउंड ट्रिप तक चलते हैं.

बंडलिंग

बंडलिंग, जो आपके सभी मॉड्यूल को एक बंडल में इनलाइन करता है, इससे यह पक्का होगा कि ब्राउज़र के पास एक राउंड ट्रिप के बाद ज़रूरी सभी कोड मौजूद हों और वह कोड को ज़्यादा तेज़ी से चलाना शुरू कर सके. हालांकि, इससे उपयोगकर्ता को बहुत सारा कोड डाउनलोड करना पड़ता है, जिसकी ज़रूरत नहीं होती. इस वजह से, बैंडविथ और समय की बर्बादी होती है. इसके अलावा, हमारे मूल मॉड्यूल में किए गए हर बदलाव से, बंडल में बदलाव होगा. इससे, बंडल का कैश मेमोरी में सेव किया गया वर्शन भी अमान्य हो जाएगा. उपयोगकर्ताओं को पूरी फ़ाइल फिर से डाउनलोड करनी होगी.

कोड विभाजन

कोड को अलग-अलग करना, बीच का हिस्सा है. हम सिर्फ़ अपनी ज़रूरत की चीज़ें डाउनलोड करके नेटवर्क को बेहतर बनाने और कैश मेमोरी में सेव करने की बेहतर तरीके से काम करने के लिए, अतिरिक्त राउंड ट्रिप में निवेश करने के लिए तैयार हैं. अगर बंडलिंग सही तरीके से की गई हो, तो राउंड ट्रिप की कुल संख्या ढीले मॉड्यूल की तुलना में बहुत कम होगी. आखिर में, ज़रूरत पड़ने पर हम link[rel=preload] जैसी पहले से लोड करने के तरीके का इस्तेमाल कर सकते हैं, ताकि तीन बार और समय से ज़्यादा समय बचाया जा सके.

पहला चरण: अपने एंट्री पॉइंट की सूची पाना

यह कई तरीकों में से एक है. हालांकि, एपिसोड में हमने अपनी वेबसाइट के एंट्री पॉइंट पाने के लिए, वेबसाइट के sitemap.xml को पार्स किया है. आम तौर पर, एक खास JSON फ़ाइल का इस्तेमाल किया जाता है, जिसमें सभी एंट्री पॉइंट की सूची होती है.

JavaScript को प्रोसेस करने के लिए babel का इस्तेमाल करना

आम तौर पर, बेबल का इस्तेमाल “ट्रांसपाइलिंग” के लिए किया जाता है: ब्लड प्रेशर वाले JavaScript कोड का इस्तेमाल करना और इसे JavaScript के पुराने वर्शन में बदलना, ताकि ज़्यादा ब्राउज़र कोड को एक्ज़ीक्यूट कर सकें. यहां सबसे पहला चरण है, नए JavaScript को पार्सर (बैबेल का इस्तेमाल babylon) से पार्स करना है. यह कोड को एक तथाकथित “ऐब्स्ट्रैक्ट सिंटैक्स ट्री” (AST) में बदल देता है. AST जनरेट हो जाने के बाद, प्लगिन की एक सीरीज़ AST का विश्लेषण करके उसे व्यवस्थित करती है.

हम JavaScript मॉड्यूल के इंपोर्ट का पता लगाने (और बाद में उसमें हेर-फेर करने) के लिए, हम 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 स्टेटमेंट होंगे, जिसकी वजह से ब्राउज़र रिसॉर्स को फ़ेच करने की कोशिश करेगा. दूसरी समस्या यह है कि डिपेंडेंसी की डिपेंडेंसी को बंडल नहीं किया गया है. हम यह पहले भी कर चुके हैं. इसलिए, हम एक और बैबल प्लगिन लिखने जा रहे हैं.

यह कोड काफ़ी हद तक हमारे पहले प्लगिन से मिलते-जुलते हैं, लेकिन हम सिर्फ़ इंपोर्ट को एक्सट्रैक्ट करने के बजाय, उन्हें हटा देंगे और इंपोर्ट की गई फ़ाइल का बंडल किया गया वर्शन भी देंगे:

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');
}

पांचवां चरण: एंट्री पॉइंट फिर से लिखना

आखिरी चरण के लिए, हम एक और Nearby प्लगिन लिखेंगे. इसका काम शेयर किए गए बंडल में मौजूद मॉड्यूल के सभी इंपोर्ट को हटाना है.

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 रिपॉज़िटरी में मिल जाएगा.

अगली बार मिलते हैं!