एसपीए के अलावा - आपके PWA के लिए अन्य आर्किटेक्चर

क्या हम... आर्किटेक्चर के बारे में बात करें?

हम एक अहम विषय के बारे में बात करने वाले हैं. हालांकि, ऐसा हो सकता है कि आपको यह विषय समझ न आए: आपके वेब ऐप्लिकेशन के लिए इस्तेमाल किया गया आर्किटेक्चर. खास तौर पर, प्रोग्रेसिव वेब ऐप्लिकेशन बनाते समय, आर्किटेक्चर से जुड़े फ़ैसले किस तरह काम करते हैं.

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

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

आपके पास अपना PWA बनाते समय, मेरे तरीके का इस्तेमाल करने का विकल्प है. हालांकि, इसके अलावा, अन्य मान्य विकल्प भी मौजूद हैं. हमें उम्मीद है कि इन सभी चीज़ों को एक साथ देखकर आपको प्रेरणा मिलेगी. साथ ही, आपको इसे अपनी ज़रूरतों के हिसाब से बनाने में मदद मिलेगी.

Stack Overflow PWA

इस लेख के साथ, मैंने एक Stack Overflow PWA बनाया है. मैं Stack Overflow पर काफ़ी समय बिताता हूं. इस दौरान, मैं सवालों के जवाब पढ़ता हूं और जवाब देता हूं. मुझे एक ऐसा वेब ऐप्लिकेशन बनाना था जिससे किसी विषय से जुड़े अक्सर पूछे जाने वाले सवालों को आसानी से ब्राउज़ किया जा सके. यह Stack Exchange API पर आधारित है. यह ओपन सोर्स है. इसके बारे में ज़्यादा जानने के लिए, GitHub प्रोजेक्ट पर जाएं.

मल्टी-पेज ऐप्लिकेशन (एमपीए)

इससे पहले कि हम खास जानकारी पर आएं, आइए कुछ शब्दों को परिभाषित करें और टेक्नोलॉजी के बारे में बताएं. सबसे पहले, हम "मल्टी पेज ऐप्लिकेशन" या "एमपीए" के बारे में बात करेंगे.

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

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

भरोसेमंद और तेज़

आपने मुझे (और कई अन्य लोगों को) "प्रोग्रेसिव वेब ऐप्लिकेशन" या PWA वाक्यांश का इस्तेमाल करते हुए सुना होगा. ऐसा हो सकता है कि आपको बैकग्राउंड की कुछ जानकारी इस साइट पर कहीं और पहले से पता हो.

PWA को एक वेब ऐप्लिकेशन के तौर पर देखा जा सकता है. यह उपयोगकर्ताओं को बेहतरीन अनुभव देता है. साथ ही, यह उपयोगकर्ताओं की होम स्क्रीन पर अपनी जगह बना लेता है. "FIRE" एक एक्रोनियम है. इसका मतलब है Fास्ट (तेज़), Iंटीग्रेटेड (इंटिग्रेट किया गया), Rिलायबल (भरोसेमंद), और Eंगेजिंग (दिलचस्प). यह PWA बनाते समय ध्यान रखने वाली सभी विशेषताओं के बारे में बताता है.

इस लेख में, मैं उन एट्रिब्यूट के सबसेट पर फ़ोकस करने जा रहा हूं: तेज़ और भरोसेमंद.

तेज़: अलग-अलग कॉन्टेक्स्ट में "तेज़" का मतलब अलग-अलग होता है. हालांकि, मैं नेटवर्क से कम से कम डेटा लोड करने के फ़ायदों के बारे में बताऊंगी.

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

तेज़ी से काम करने वाला: आखिर में, मैं PWA की परिभाषा को थोड़ा बदलकर यह देखूंगा कि तेज़ी से काम करने वाला ऐप्लिकेशन बनाने का क्या मतलब है. सिर्फ़ कम लेटेंसी वाले नेटवर्क पर तेज़ और भरोसेमंद होना काफ़ी नहीं है. तेज़ी से काम करने का मतलब है कि आपका वेब ऐप्लिकेशन, नेटवर्क की स्थिति चाहे जैसी भी हो, हमेशा एक जैसी स्पीड से काम करता है.

चालू करने वाली टेक्नोलॉजी: सर्विस वर्कर + Cache Storage API

PWAs, तेज़ी से काम करने और बेहतर परफ़ॉर्मेंस देने के लिए जाने जाते हैं. अच्छी बात यह है कि वेब प्लैटफ़ॉर्म, इस तरह की परफ़ॉर्मेंस को बेहतर बनाने के लिए कुछ बुनियादी चीज़ें उपलब्ध कराता है. मेरा मतलब सर्विस वर्कर और Cache Storage API से है.

एक ऐसा सर्विस वर्कर बनाया जा सकता है जो आने वाले अनुरोधों को सुनता है. साथ ही, कुछ अनुरोधों को नेटवर्क पर भेजता है और Cache Storage API के ज़रिए, जवाब की एक कॉपी को आने वाले समय में इस्तेमाल करने के लिए सेव करता है.

यह एक ऐसा सर्विस वर्कर है जो नेटवर्क रिस्पॉन्स की कॉपी सेव करने के लिए, Cache Storage API का इस्तेमाल करता है.

जब वेब ऐप्लिकेशन अगली बार वही अनुरोध करता है, तो उसका सर्विस वर्कर अपनी कैश मेमोरी की जांच कर सकता है. साथ ही, पहले से कैश मेमोरी में सेव किया गया जवाब वापस दे सकता है.

यह एक ऐसा सर्विस वर्कर है जो नेटवर्क को बायपास करके, Cache Storage API का इस्तेमाल करके जवाब देता है.

जब भी हो सके, नेटवर्क से बचना, भरोसेमंद तरीके से तेज़ परफ़ॉर्मेंस देने का एक अहम हिस्सा है.

"आइसोमॉर्फ़िक" JavaScript

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

इस तरह से कोड शेयर करने के कई मान्य तरीके हैं. हालांकि, मेरा तरीका, ES मॉड्यूल को सोर्स कोड के तौर पर इस्तेमाल करना था. इसके बाद, मैंने Babel और Rollup का इस्तेमाल करके, सर्वर और सर्विस वर्कर के लिए उन मॉड्यूल को ट्रांसपाइल और बंडल किया. मेरे प्रोजेक्ट में, .mjs फ़ाइल एक्सटेंशन वाली फ़ाइलें ऐसा कोड होती हैं जो ES मॉड्यूल में मौजूद होता है.

सर्वर

इन कॉन्सेप्ट और शब्दावली को ध्यान में रखते हुए, आइए जानें कि मैंने Stack Overflow PWA को कैसे बनाया. हमारा बैकएंड सर्वर, पूरे आर्किटेक्चर में कैसे काम करता है, इसके बारे में हम आपको बताएंगे.

मुझे स्टैटिक होस्टिंग के साथ-साथ डाइनैमिक बैकएंड की सुविधा चाहिए थी. इसलिए, मैंने Firebase प्लैटफ़ॉर्म का इस्तेमाल करने का फ़ैसला किया.

Firebase Cloud Functions, अनुरोध मिलने पर अपने-आप Node पर आधारित एनवायरमेंट शुरू कर देगा. साथ ही, यह लोकप्रिय Express HTTP फ़्रेमवर्क के साथ इंटिग्रेट हो जाएगा. इसके बारे में मुझे पहले से पता था. यह मेरी साइट के सभी स्टैटिक संसाधनों के लिए, आउट-ऑफ़-द-बॉक्स होस्टिंग भी उपलब्ध कराता है. आइए, देखते हैं कि सर्वर अनुरोधों को कैसे हैंडल करता है.

जब कोई ब्राउज़र हमारे सर्वर के ख़िलाफ़ नेविगेशन का अनुरोध करता है, तो वह इस फ़्लो से गुज़रता है:

सर्वर-साइड पर नेविगेशन रिस्पॉन्स जनरेट करने की खास जानकारी.

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

इस तस्वीर में दो चीज़ें हैं जिनके बारे में ज़्यादा जानकारी दी गई है: राउटिंग और टेंप्लेटिंग.

रूटिंग

राउटिंग के लिए, मैंने Express फ़्रेमवर्क के नेटिव राउटिंग सिंटैक्स का इस्तेमाल किया. यह इतना फ़्लेक्सिबल है कि यह सामान्य यूआरएल प्रीफ़िक्स के साथ-साथ, उन यूआरएल से भी मैच कर सकता है जिनमें पाथ के हिस्से के तौर पर पैरामीटर शामिल होते हैं. यहां, मैंने एक मैपिंग बनाई है. इसमें, रूट के नामों को Express के पैटर्न से मैच किया जाता है.

const routes = new Map([
  ['about', '/about'],
  ['questions', '/questions/:questionId'],
  ['index', '/'],
]);

export default routes;

इसके बाद, मैं इस मैपिंग को सीधे सर्वर के कोड से रेफ़रंस कर सकता हूं. जब किसी एक्सप्रेस पैटर्न से मैच होता है, तो सही हैंडलर, मैच होने वाले रूट के लिए खास टेंप्लेटिंग लॉजिक के साथ जवाब देता है.

import routes from './lib/routes.mjs';
app.get(routes.get('index'), as>ync (req, res) = {
  // Templating logic.
});

सर्वर-साइड टेंप्लेटिंग

टेम्पलेट बनाने का लॉजिक कैसा दिखता है? मैंने एक ऐसा तरीका अपनाया जिसमें एचटीएमएल के कुछ हिस्सों को क्रम से एक के बाद एक जोड़ा गया. यह मॉडल, स्ट्रीमिंग के लिए सबसे सही है.

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

मेरा मतलब समझने के लिए, हमारी किसी एक रूट के लिए Express कोड देखें:

app.get(routes.get('index'), async (req>, res) = {
  res.write(headPartial + navbarPartial);
  const tag = req.query.tag || DEFAULT_TAG;
  const data = await requestData(...);
  res.write(templates.index(tag, data.items));
  res.write(footPartial);
  res.end();
});

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

हमारे पेज का अगला हिस्सा, Stack Exchange API से मिले डेटा का इस्तेमाल करता है. उस डेटा को पाने का मतलब है कि हमारे सर्वर को नेटवर्क अनुरोध करना होगा. जब तक वेब ऐप्लिकेशन को जवाब नहीं मिल जाता और वह उसे प्रोसेस नहीं कर लेता, तब तक वह कुछ और रेंडर नहीं कर सकता. हालांकि, कम से कम उपयोगकर्ता इंतज़ार करते समय खाली स्क्रीन को नहीं देख रहे हैं.

जब वेब ऐप्लिकेशन को Stack Exchange API से जवाब मिल जाता है, तो वह कस्टम टेंप्लेटिंग फ़ंक्शन को कॉल करता है. इससे एपीआई से मिले डेटा को उसके एचटीएमएल में बदला जा सकता है.

टेंप्लेटिंग लैंग्वेज

टेंप्लेटिंग एक ऐसा विषय है जिस पर काफ़ी बहस हो सकती है. मैंने जो तरीका इस्तेमाल किया है वह कई तरीकों में से सिर्फ़ एक है. आपको अपने समाधान का इस्तेमाल करना चाहिए. खास तौर पर, अगर आपका मौजूदा टेंप्लेटिंग फ़्रेमवर्क से पुराना संबंध है.

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

इसलिए, यहां एक उदाहरण दिया गया है कि मैं अपने वेब ऐप्लिकेशन के इंडेक्स के डाइनैमिक एचटीएमएल हिस्से को कैसे टेंप्लेट कर रहा हूं. मेरे रास्तों की तरह, टेंप्लेटिंग लॉजिक को ES मॉड्यूल में सेव किया जाता है. इसे सर्वर और सर्विस वर्कर, दोनों में इंपोर्ट किया जा सकता है.

export function index(tag, items) {
  const title = `<h3>Top "${escape(tag)}"< Qu>estions/h3`;
  cons<t form = `form me>tho<d=&qu>ot;GET".../form`;
  const questionCards = i>tems
    .map(item =
      questionCard({
        id: item.question_id,
        title: item.title,
      })
    )
    .join('&<#39;);
  const que>stions = `div id<=&qu>ot;questions"${questionCards}/div`;
  return title + form + questions;
}

ये टेंप्लेट फ़ंक्शन, प्योर JavaScript होते हैं. इसलिए, लॉजिक को छोटे-छोटे हेल्पर फ़ंक्शन में बांटना फ़ायदेमंद होता है. यहां, मैंने एपीआई से मिले रिस्पॉन्स में दिखाए गए हर आइटम को ऐसे फ़ंक्शन में पास किया है जो सभी ज़रूरी एट्रिब्यूट सेट करके, स्टैंडर्ड एचटीएमएल एलिमेंट बनाता है.

function questionCard({id, title}) {
  return `<a class="card"
             href="/questions/${id}"
             data-cache-url=>"${<qu>estionUrl(id)}"${title}/a`;
}

खास तौर पर ध्यान देने वाली बात यह है कि डेटा एट्रिब्यूट को हर लिंक में जोड़ा जाता है. data-cache-url को Stack Exchange API के उस यूआरएल पर सेट किया जाता है जिसकी ज़रूरत मुझे सवाल दिखाने के लिए होती है. इस बात का ध्यान रखें. मैं इसे बाद में देखूंगा/देखूंगी.

अपने रूट हैंडलर पर वापस जाकर, टेंप्लेटिंग पूरी होने के बाद, मैं अपने पेज के एचटीएमएल के आखिरी हिस्से को ब्राउज़र पर स्ट्रीम करता हूँ और स्ट्रीम को बंद कर देता हूँ. यह ब्राउज़र को यह सूचना देने वाला क्यू है कि प्रोग्रेसिव रेंडरिंग पूरी हो गई है.

app.get(routes.get('index'), async (req>, res) = {
  res.write(headPartial + navbarPartial);
  const tag = req.query.tag || DEFAULT_TAG;
  const data = await requestData(...);
  res.write(templates.index(tag, data.items));
  res.write(footPartial);
  res.end();
});

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

सर्विस वर्कर

इस इमेज में, सर्विस वर्कर में नेविगेशन रिस्पॉन्स जनरेट करने के बारे में खास जानकारी दी गई है.

यह डायग्राम आपको जाना-पहचाना लग सकता है. इसमें, मैंने पहले जिन चीज़ों के बारे में बताया था उनमें से कई चीज़ें शामिल हैं. हालांकि, उन्हें थोड़ा अलग तरीके से व्यवस्थित किया गया है. आइए, सर्विस वर्कर को ध्यान में रखते हुए, अनुरोध के फ़्लो के बारे में जानें.

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

यह तरीका पहले जैसा ही है, लेकिन इसमें fetch() और Cache Storage API जैसे अलग-अलग लो-लेवल प्रिमिटिव का इस्तेमाल किया गया है. मैं इन डेटा सोर्स का इस्तेमाल करके एचटीएमएल रिस्पॉन्स बनाता हूं. इसके बाद, सर्विस वर्कर इस रिस्पॉन्स को वेब ऐप्लिकेशन को वापस भेज देता है.

Workbox

मैं अपनी सेवा को कम लेवल के प्रिमिटिव से शुरू करने के बजाय, Workbox नाम की हाई-लेवल लाइब्रेरी के सेट के ऊपर अपना सर्विस वर्कर बनाऊंगा. यह किसी भी सर्विस वर्कर की कैश मेमोरी, राउटिंग, और जवाब जनरेट करने के लॉजिक के लिए एक मज़बूत आधार देता है.

रूटिंग

मेरे सर्वर-साइड कोड की तरह ही, मेरे सर्विस वर्कर को यह पता होना चाहिए कि आने वाले अनुरोध को सही जवाब के लॉजिक से कैसे मैच किया जाए.

मैंने हर Express रूट को उससे जुड़े रेगुलर एक्सप्रेशन में बदलने के लिए, regexparam नाम की लाइब्रेरी का इस्तेमाल किया. अनुवाद हो जाने के बाद, मैं Workbox में पहले से मौजूद रेगुलर एक्सप्रेशन राउटिंग की सुविधा का इस्तेमाल कर सकता हूं.

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

import regExpRoutes from './regexp-routes.mjs';

workbox.routing.registerRoute(
  regExpRoutes.get('index')
  // Templating logic.
);

स्टैटिक ऐसेट को कैश मेमोरी में सेव करना

टेंप्लेट बनाने की प्रोसेस का एक अहम हिस्सा यह पक्का करना है कि मेरे आंशिक एचटीएमएल टेंप्लेट, Cache Storage API के ज़रिए स्थानीय तौर पर उपलब्ध हों. साथ ही, जब मैं वेब ऐप्लिकेशन में बदलावों को डिप्लॉय करूं, तब उन्हें अप-टू-डेट रखा जाए. मैन्युअल तरीके से कैश मेमोरी को मैनेज करने में गड़बड़ियां हो सकती हैं. इसलिए, मैं Workbox का इस्तेमाल करके, अपनी बिल्ड प्रोसेस के तहत प्रीकैशिंग को मैनेज करता हूं.

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

module.exports = {
  globDirectory: 'build',
  globPatterns: ['**/*.{html,js,svg}'],
  // Other options...
};

Workbox, हर फ़ाइल के कॉन्टेंट का स्नैपशॉट लेता है. साथ ही, यूआरएल और वर्शन की उस सूची को मेरी फ़ाइनल सर्विस वर्कर फ़ाइल में अपने-आप डाल देता है. Workbox के पास अब वे सभी चीज़ें हैं जिनकी मदद से, पहले से कैश मेमोरी में सेव की गई फ़ाइलों को हमेशा उपलब्ध कराया जा सकता है. साथ ही, उन्हें अप-टू-डेट रखा जा सकता है. इसके बाद, आपको service-worker.js फ़ाइल मिलेगी. इसमें इस तरह की जानकारी होगी:

workbox.precaching.precacheAndRoute([
  {
    url: 'partials/about.html',
    revision: '518747aad9d7e',
  },
  {
    url: 'partials/foot.html',
    revision: '69bf746a9ecc6',
  },
  // etc.
]);

जो लोग ज़्यादा जटिल बिल्ड प्रोसेस का इस्तेमाल करते हैं उनके लिए Workbox में, webpack प्लगिन और सामान्य नोड मॉड्यूल, दोनों उपलब्ध हैं. इसके अलावा, इसमें कमांड लाइन इंटरफ़ेस भी है.

स्ट्रीमिंग

इसके बाद, मुझे यह चाहिए कि सर्विस वर्कर, पहले से कैश किए गए उस एचटीएमएल को तुरंत वेब ऐप्लिकेशन पर वापस स्ट्रीम करे. यह "तेज़ी से भरोसेमंद जवाब देने" की सुविधा का एक अहम हिस्सा है. मुझे हमेशा स्क्रीन पर तुरंत काम की जानकारी मिलती है. अच्छी बात यह है कि हमारी सेवा में काम करने वाले वर्कर में Streams API का इस्तेमाल करके ऐसा किया जा सकता है.

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

पहले, सिर्फ़ Chrome पर Streams काम करता था. हालांकि, अब Streams API ज़्यादा ब्राउज़र पर काम करता है. कुल मिलाकर, यह सुविधा अच्छी है. साथ ही, फ़ॉलबैक कोड का सही तरीके से इस्तेमाल करके, आज ही सर्विस वर्कर में स्ट्रीम का इस्तेमाल किया जा सकता है.

ठीक है... शायद एक चीज़ आपको रोक रही है. वह है Streams API के काम करने के तरीके को समझना. यह प्रिमिटिव का एक बहुत ही पावरफ़ुल सेट दिखाता है. साथ ही, इसका इस्तेमाल करने में सहज डेवलपर, जटिल डेटा फ़्लो बना सकते हैं. जैसे:

const stream = new ReadableStream({
  pull(controller) {
    return sources[0]
      .then(r => r.read())
      .then(result => {
        if (result.done) {
          sources.shift();
          if (sources.length === 0) return controller.close();
          return this.pull(controller);
        } else {
          controller.enqueue(result.value);
        }
      });
  },
});

हालांकि, इस कोड के सभी पहलुओं को समझना हर किसी के बस की बात नहीं है. इस लॉजिक को पार्स करने के बजाय, आइए सर्विस वर्कर स्ट्रीमिंग के लिए मेरे तरीके के बारे में बात करते हैं.

मैं एक नया और बेहतर रैपर workbox-streams इस्तेमाल कर रहा/रही हूं. इसकी मदद से, मैं इसे स्ट्रीमिंग सोर्स के मिक्स में पास कर सकता/सकती हूं. इसमें कैश और रनटाइम डेटा, दोनों शामिल हैं. यह डेटा नेटवर्क से मिल सकता है. Workbox, अलग-अलग सोर्स को मैनेज करता है और उन्हें एक साथ जोड़कर, स्ट्रीमिंग रिस्पॉन्स बनाता है.

इसके अलावा, Workbox अपने-आप यह पता लगाता है कि Streams API काम करता है या नहीं. अगर यह काम नहीं करता है, तो यह स्ट्रीम नहीं होने वाला जवाब बनाता है. इसका मतलब है कि आपको फ़ॉलबैक लिखने की चिंता करने की ज़रूरत नहीं है, क्योंकि स्ट्रीम, ब्राउज़र के साथ 100% काम करती हैं.

रनटाइम के दौरान कैश मेमोरी का इस्तेमाल करना

आइए, देखते हैं कि मेरा सर्विस वर्कर, Stack Exchange API से मिले रनटाइम डेटा को कैसे मैनेज करता है. मैं Workbox में पहले से मौजूद, stale-while-revalidate caching strategy का इस्तेमाल कर रहा हूं. साथ ही, मैं यह पक्का करने के लिए समयसीमा का इस्तेमाल कर रहा हूं कि वेब ऐप्लिकेशन का स्टोरेज बहुत ज़्यादा न बढ़ जाए.

मैंने Workbox में दो रणनीतियां सेट अप की हैं, ताकि स्ट्रीमिंग रिस्पॉन्स बनाने वाले अलग-अलग सोर्स को मैनेज किया जा सके. Workbox की मदद से, हम कुछ फ़ंक्शन कॉल और कॉन्फ़िगरेशन कर सकते हैं. इसके लिए, हमें सैकड़ों लाइनों का कोड लिखना पड़ता.

const cacheStrategy = workbox.strategies.cacheFirst({
  cacheName: workbox.core.cacheNames.precache,
});

const apiStrategy = workbox.strategies.staleWhileRevalidate({
  cacheName: API_CACHE_NAME,
  plugins: [new workbox.expiration.Plugin({maxEntries: 50})],
});

पहली रणनीति, पहले से कैश किए गए डेटा को पढ़ती है. जैसे, हमारे आंशिक एचटीएमएल टेंप्लेट.

दूसरी रणनीति में, stale-while-revalidate कैश मेमोरी सेव करने की सुविधा का इस्तेमाल किया जाता है. साथ ही, 50 एंट्री होने पर, सबसे कम इस्तेमाल की गई कैश मेमोरी की समयसीमा खत्म हो जाती है.

अब जब मेरे पास ये रणनीतियां हैं, तो मुझे सिर्फ़ Workbox को यह बताना है कि इनका इस्तेमाल करके, पूरा और स्ट्रीमिंग वाला जवाब कैसे बनाया जाए. मैंने फ़ंक्शन के तौर पर सोर्स की एक ऐरे पास की है. इनमें से हर फ़ंक्शन तुरंत लागू हो जाएगा. Workbox, हर सोर्स से मिले नतीजे को वेब ऐप्लिकेशन पर क्रम से स्ट्रीम करता है. अगर ऐरे में मौजूद अगला फ़ंक्शन पूरा नहीं हुआ है, तो ही स्ट्रीम करने में देरी होती है.

workbox.streams.strategy([
  () => cacheStrategy.makeRequest({request: '/head.html'})>,
  () = cacheStrategy.makeRequest({request: '/navbar.html'}),
  async >({event, url}) = {
    const tag = url.searchParams.get('tag') || DEFAULT_TAG;
    const listResponse = await apiStrategy.makeRequest(...);
    const data = await listResponse.json();
    return templates.index(tag, >data.items);
  },
  () = cacheStrategy.makeRequest({request: '/foot.html'}),
]);

पहले दो सोर्स, पहले से कैश किए गए ऐसे टेंप्लेट हैं जिन्हें सीधे Cache Storage API से पढ़ा जाता है. इसलिए, ये हमेशा तुरंत उपलब्ध रहेंगे. इससे यह पक्का होता है कि हमारा सर्विस वर्कर, अनुरोधों का जवाब देने में तेज़ी से काम करेगा. ठीक वैसे ही जैसे सर्वर-साइड कोड करता है.

हमारा अगला सोर्स फ़ंक्शन, Stack Exchange API से डेटा फ़ेच करता है. साथ ही, यह वेब ऐप्लिकेशन के लिए ज़रूरी एचटीएमएल में जवाब को प्रोसेस करता है.

स्टेल-व्हाइल-रीवैलिडेट रणनीति का मतलब है कि अगर मेरे पास इस एपीआई कॉल के लिए, पहले से कैश मेमोरी में सेव किया गया कोई जवाब है, तो मैं उसे तुरंत पेज पर स्ट्रीम कर पाऊंगा. साथ ही, अगली बार अनुरोध किए जाने पर, कैश मेमोरी की एंट्री को "बैकग्राउंड में" अपडेट कर पाऊंगा.

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

शेयरिंग कोड की मदद से, सभी डिवाइसों पर एक जैसा कॉन्टेंट दिखता है

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

डाइनैमिक, प्रोग्रेसिव एन्हैंसमेंट

मैंने अपने PWA के लिए सर्वर और सर्विस वर्कर, दोनों के बारे में जानकारी दी है. हालांकि, एक और लॉजिक के बारे में बताना बाकी है: थोड़ी मात्रा में JavaScript होती है, जो मेरे हर पेज पर पूरी तरह से स्ट्रीम होने के बाद चलती है.

यह कोड, उपयोगकर्ता अनुभव को बेहतर बनाता है. हालांकि, यह ज़रूरी नहीं है. अगर इसे नहीं चलाया जाता है, तो भी वेब ऐप्लिकेशन काम करेगा.

पेज का मेटाडेटा

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

टेंप्लेटिंग कोड के तौर पर, मेरा तरीका यह है कि मैं एक स्क्रिप्ट टैग शामिल करूं, जिसमें सही तरीके से एस्केप की गई स्ट्रिंग हो.

const metadataScript = `<script>
  self._title = '${escape(item.title)<}';>
/script`;

इसके बाद, जब मेरा पेज लोड हो जाता है, तो मैं उस स्ट्रिंग को पढ़ता हूं और दस्तावेज़ के टाइटल को अपडेट करता हूं.

if (self._title) {
  document.title = unescape(self._title);
}

अगर आपको पेज के हिसाब से अन्य मेटाडेटा को अपने वेब ऐप्लिकेशन में अपडेट करना है, तो इसी तरीके का इस्तेमाल किया जा सकता है.

ऑफ़लाइन यूज़र एक्सपीरियंस

मैंने प्रोग्रेसिव एनहांसमेंट की एक और सुविधा जोड़ी है. इसका इस्तेमाल, ऑफ़लाइन सुविधाओं पर लोगों का ध्यान खींचने के लिए किया जाता है. मैंने एक भरोसेमंद PWA बनाया है. मुझे उपयोगकर्ताओं को यह बताना है कि ऑफ़लाइन होने पर भी, वे पहले देखे गए पेजों को लोड कर सकते हैं.

सबसे पहले, मैं Cache Storage API का इस्तेमाल करके, पहले से कैश मेमोरी में सेव किए गए सभी एपीआई अनुरोधों की सूची बनाता हूं. इसके बाद, मैं उसे यूआरएल की सूची में बदलता हूं.

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

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

const apiCache = await caches.open(API_CACHE_NAME);
const cachedRequests = await apiCache.keys();
const cachedUrls = cachedRequests.map(request => request.url);

const cards = document.querySelectorAll('.card');
const uncachedCards = [...cards].filte>r(card = {
  return !cachedUrls.includes(card.dataset.cacheUrl);
});

const offlineHandle>r = () = {
  for (const uncachedCard of uncachedCards) {
    uncachedCard.style.opacity = '0.3';
  }
};

const onli>neHandler = () = {
  for (const uncachedCard of uncachedCards) {
    uncachedCard.style.opacity = '1.0';
  }
};

window.addEventListener('online', onlineHandler);
window.addEventListener('offline', offlineHandler);

आम तौर पर होने वाली गलतियां

अब मैंने कई पेजों वाला PWA बनाने के तरीके के बारे में जानकारी दे दी है. अपना तरीका तय करते समय, आपको कई बातों का ध्यान रखना होगा. ऐसा हो सकता है कि आपके फ़ैसले मेरे फ़ैसलों से अलग हों. वेब के लिए ऐप्लिकेशन बनाने की यह सुविधा, इसकी सबसे अच्छी बातों में से एक है.

आर्किटेक्चर से जुड़े फ़ैसले लेते समय, आपको कुछ सामान्य समस्याओं का सामना करना पड़ सकता है. हम चाहते हैं कि आपको इन समस्याओं से न जूझना पड़े.

पूरे एचटीएमएल को कैश मेमोरी में सेव न करें

हमारा सुझाव है कि आप अपनी कैश मेमोरी में पूरे एचटीएमएल दस्तावेज़ों को सेव न करें. पहली बात, यह जगह की बर्बादी है. अगर आपका वेब ऐप्लिकेशन, अपने हर पेज के लिए एक ही बेसिक एचटीएमएल स्ट्रक्चर का इस्तेमाल करता है, तो आपको एक ही मार्कअप की कॉपी को बार-बार सेव करना होगा.

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

सर्वर / सर्विस वर्कर में अंतर

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

आर्किटेक्चर से जुड़े जो भी फ़ैसले लिए जाते हैं, उनके लिए आपके पास सर्वर और सर्विस वर्कर में, राउटिंग और टेंप्लेटिंग के बराबर कोड चलाने की कोई रणनीति होनी चाहिए.

सबसे खराब स्थितियां

लेआउट / डिज़ाइन में अंतर होना

इन कमियों को अनदेखा करने पर क्या होता है? वैसे, हर तरह की गड़बड़ियां हो सकती हैं. हालांकि, सबसे खराब स्थिति यह होती है कि कोई लौटने वाला उपयोगकर्ता, कैश मेमोरी में सेव किए गए ऐसे पेज पर जाता है जिसका लेआउट बहुत पुराना है. ऐसा हो सकता है कि उसमें हेडर का टेक्स्ट पुराना हो या उसमें ऐसे सीएसएस क्लास नेम का इस्तेमाल किया गया हो जो अब मान्य नहीं हैं.

सबसे बुरा यह हो सकता है कि: राउटिंग काम न करे

इसके अलावा, ऐसा भी हो सकता है कि उपयोगकर्ता को कोई ऐसा यूआरएल मिले जिसे आपका सर्वर हैंडल करता है, लेकिन आपका सर्विस वर्कर नहीं. ज़ॉम्बी लेआउट और डेड एंड से भरी साइट, भरोसेमंद PWA नहीं होती.

सफलता के लिए युक्तियां

हालांकि, इस काम को आपको अकेले नहीं करना है! यहां दी गई सलाह अपनाकर, इन समस्याओं से बचा जा सकता है:

ऐसी टेंप्लेटिंग और राउटिंग लाइब्रेरी का इस्तेमाल करें जो कई भाषाओं में काम करती हैं

ऐसी टेंप्लेटिंग और राउटिंग लाइब्रेरी का इस्तेमाल करें जिनमें JavaScript लागू की गई हो. अब, हमें पता है कि हर डेवलपर के पास, आपके मौजूदा वेब सर्वर और टेंप्लेटिंग भाषा से माइग्रेट करने का विकल्प नहीं होता.

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

नेस्ट किए गए टेंप्लेट के बजाय, क्रम वाले टेंप्लेट का इस्तेमाल करें

इसके बाद, मेरा सुझाव है कि आप क्रम से लगाए गए टेंप्लेट की सीरीज़ का इस्तेमाल करें. इन्हें एक के बाद एक स्ट्रीम किया जा सकता है. अगर आपके पेज के बाद वाले हिस्सों में टेंप्लेटिंग के ज़्यादा जटिल लॉजिक का इस्तेमाल किया जाता है, तो कोई समस्या नहीं है. हालांकि, यह ज़रूरी है कि आप अपने एचटीएमएल के शुरुआती हिस्से को जल्द से जल्द स्ट्रीम कर सकें.

अपने सर्विस वर्कर में स्टैटिक और डाइनैमिक, दोनों तरह के कॉन्टेंट को कैश करना

बेहतर परफ़ॉर्मेंस के लिए, आपको अपनी साइट के सभी अहम स्टैटिक संसाधनों को पहले से कैश मेमोरी में सेव करना चाहिए. आपको रनटाइम कैश मेमोरी सेव करने की सुविधा भी सेट अप करनी चाहिए, ताकि एपीआई अनुरोधों जैसे डाइनैमिक कॉन्टेंट को मैनेज किया जा सके. Workbox का इस्तेमाल करने का मतलब है कि आपको स्क्रैच से सब कुछ लागू करने के बजाय, अच्छी तरह से टेस्ट की गई और प्रोडक्शन के लिए तैयार रणनीतियों के आधार पर काम करने का विकल्प मिलता है.

सिर्फ़ तब नेटवर्क पर ब्लॉक करें, जब बहुत ज़रूरी हो

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

संसाधन