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

चलिए, अब आर्किटेक्चर के बारे में बात करते हैं?

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

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

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

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

Stack Overflow PWA

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

कई पेज वाले ऐप्लिकेशन (MPA)

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

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

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

भरोसेमंद तरीके से तेज़

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

PWA को एक ऐसा वेब ऐप्लिकेशन माना जा सकता है जो बेहतरीन उपयोगकर्ता अनुभव देता है. यह उपयोगकर्ता की होम स्क्रीन पर, सही मायने में एक जगह दिखाता है. संक्षिप्त नाम "FIRE", Fast, Integrated, Reliable, और Engaging का नाम है. इसका मतलब है, PWA बनाते समय जिन एट्रिब्यूट के बारे में जानना ज़रूरी है.

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

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

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

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

टेक्नोलॉजी उपलब्ध कराना: सर्विस वर्कर + कैश स्टोरेज एपीआई

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

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

नेटवर्क रिस्पॉन्स की कॉपी सेव करने के लिए, कैश स्टोरेज एपीआई का इस्तेमाल करता हुआ सर्विस वर्कर.

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

नेटवर्क को बायपास करने के लिए, कैश स्टोरेज एपीआई का इस्तेमाल करने वाला सर्विस वर्कर.

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

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

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

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

सर्वर

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

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

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

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

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

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

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

रूटिंग

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

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

export default routes;

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

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

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

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

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

मेरा मतलब जानने के लिए, हमारे किसी एक रास्ते के लिए एक्सप्रेशन कोड देखें:

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

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

export function index(tag, items) {
  const title = `<h3>Top "${escape(tag)}" Questions</h3>`;
  const form = `<form method="GET">...</form>`;
  const questionCards = items
    .map(item =>
      questionCard({
        id: item.question_id,
        title: item.title,
      })
    )
    .join('');
  const questions = `<div id="questions">${questionCards}</div>`;
  return title + form + questions;
}

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

function questionCard({id, title}) {
  return `<a class="card"
             href="/questions/${id}"
             data-cache-url="${questionUrl(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() और कैश स्टोरेज एपीआई. मैं उन डेटा सोर्स का इस्तेमाल एचटीएमएल रिस्पॉन्स को बनाने के लिए करता हूं, जिन्हें सर्विस वर्कर वेब ऐप्लिकेशन को वापस पास करता है.

Workbox

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

रूटिंग

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

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

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

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

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

स्टैटिक ऐसेट कैश मेमोरी

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

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

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

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

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

रनटाइम कैशिंग

आइए देखते हैं कि मेरा सर्विस वर्कर Stack Exchange API से मिलने वाले रनटाइम डेटा का इस्तेमाल कैसे करता है. मैं दोबारा पुष्टि करने के दौरान पुरानी कैश मेमोरी में सेव करने की रणनीति के लिए, Workspace में पहले से मौजूद सहायता का इस्तेमाल कर रहा हूं. साथ ही, वेब ऐप्लिकेशन का स्टोरेज अनबाउंड न हो, इसके लिए भी मैं इसका इस्तेमाल कर रहा हूं.

मैंने 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})],
});

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

दूसरी रणनीति 50 एंट्री तक पहुंचने के बाद, पुरानी जानकारी को फिर से पुष्टि करने वाले कैश मेमोरी के लॉजिक को लागू करती है. साथ ही, हाल ही में इस्तेमाल हुई कैश मेमोरी की समयसीमा को भी खत्म कर देती है.

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

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

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

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

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

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

शेयर करने वाले कोड से चीज़ें सिंक रहती हैं

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

डाइनैमिक, प्रोग्रेसिव बेहतर बनाने की सुविधा

मैंने अपने PWA के सर्वर और सर्विस वर्कर, दोनों का इस्तेमाल किया है, लेकिन आपको बस एक और वजह बताई गई है: कुछ JavaScript है जो मेरे हर पेज पर पूरी तरह स्ट्रीम होने के बाद चलता है.

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

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

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

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

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

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

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

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

ऑफ़लाइन उपयोगकर्ता अनुभव

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

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

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

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

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].filter(card => {
  return !cachedUrls.includes(card.dataset.cacheUrl);
});

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

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

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

सामान्य गलतियां

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

कुछ ऐसी सामान्य गलतियां हैं जिनका सामना आपको आर्किटेक्चर से जुड़े फ़ैसले लेते समय करना पड़ सकता है. हम आपका कुछ ख्याल रखना चाहते हैं.

फ़ुल एचटीएमएल को कैश मेमोरी में सेव न करना

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

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

सर्वर / सर्विस वर्कर ड्रिफ़्ट

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

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

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

अलग-अलग लेआउट / डिज़ाइन

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

सबसे खराब स्थिति: रूटिंग में गड़बड़ी

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

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

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

टेंप्लेट और रूटिंग लाइब्रेरी का इस्तेमाल करना, जिनमें कई भाषाओं में कॉन्टेंट को लागू करने की सुविधा मौजूद है

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

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

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

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

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

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

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

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

रिसॉर्स