🌐 Detecting your location…

2026 में ऑफ़लाइन-प्रथम PWA कैसे बनाएं: सर्विस वर्कर्स और IndexedDB गाइड

⏱️4 min read  ·  735 words

ऑफ़लाइन-प्रथम का अर्थ है कि एप्लिकेशन डिफ़ॉल्ट रूप से स्थानीय डेटा से काम करता है और नेटवर्क को एक एन्हांसमेंट के रूप में मानता है। यह “ऑनलाइन काम करता है, ऑफ़लाइन होने पर ख़राब हो जाता है” से भिन्न डिज़ाइन है, और यह एक ऐसा ऐप तैयार करता है जो अच्छे कनेक्शन पर भी तुरंत महसूस होता है। यह मार्गदर्शिका एक को ठीक से बनाती है: सेवा कार्यकर्ता कैशिंग, संरचित डेटा के लिए IndexedDB, और ऑफ़लाइन रहते हुए किए गए लेखन के लिए पृष्ठभूमि सिंक।

📋 Table of Contents

  1. वास्तुकला
  2. चरण 1: प्रकट
  3. चरण 2: सेवा कर्मी का पंजीकरण
  4. चरण 3: सेवा कार्यकर्ता और इसकी कैशिंग रणनीतियाँ
  5. कैश संग्रहण संपूर्ण HTTP प्रतिक्रियाएँ रखता है। उन रिकॉर्ड्स के लिए जिन्हें आपको अलग-अलग क्वेरी करने, सॉर्ट करने और अपडेट करने की आवश्यकता है, IndexedDB का उपयोग करें। कच्चा एपीआई वर्बोज़ है, इसलिए एक पतला आवरण इसके लायक है।
  6. बैकग्राउंड सिंक कनेक्टिविटी वापस आने पर ब्राउज़र को कतारबद्ध लेखन को फिर से चलाने की सुविधा देता है, भले ही पेज बंद कर दिया गया हो।
  7. ऑफ़लाइन लिखने से टकराव पैदा होता है, और अन्यथा दिखावा करने से चुपचाप डेटा हानि होती है। एक नीति स्पष्ट रूप से तय करें.
  8. ब्राउज़र दबाव में भंडारण हटा देते हैं। ऐसे ऐप के लिए जिसका स्थानीय डेटा सिंक होने तक सत्य का स्रोत है, दृढ़ता का अनुरोध करें।
  9. DevTools का ऑफ़लाइन चेकबॉक्स एक प्रारंभिक बिंदु है, परीक्षण नहीं। यह भी जांचें: ऑफ़लाइन होने पर पुनः लोड करना, स्पष्ट रूप से अनुपस्थित होने के बजाय धीमा और परतदार कनेक्शन, जब सेवा कार्यकर्ता मध्य सत्र को अपडेट करता है तो क्या होता है, और क्या कतारबद्ध लेखन पूर्ण ब्राउज़र पुनरारंभ से बच जाता है।
  10. POST अनुरोधों को कैशिंग करना.
  11. ऑफ़लाइन-प्रथम एक वास्तुशिल्प निर्णय पर आधारित है:

वास्तुकला

  1. सेवा कर्मी नेटवर्क अनुरोधों को रोकता है और कैश से सेवा प्रदान करता है
  2. कैश संग्रहण एप्लिकेशन शेल रखता है – HTML, CSS, JavaScript, फ़ॉन्ट्स
  3. IndexedDB संरचित डेटा रखता है – उपयोगकर्ता द्वारा पढ़े और लिखे गए रिकॉर्ड
  4. बैकग्राउंड सिंक कनेक्शन वापस आने पर रिप्ले ऑफ़लाइन लिख दिया जाता है

मुख्य मानसिक बदलाव: यूआई IndexedDB से पढ़ता है, कभी भी सीधे नेटवर्क से नहीं। एक अलग प्रक्रिया IndexedDB को चालू रखती है। वह एक निर्णय ही है जो ऐप को ऑनलाइन और ऑफ पर समान रूप से काम करता है।

चरण 1: प्रकट

{
  "name": "Field Notes",
  "short_name": "Notes",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#1a1a2e",
  "icons": [
    { "src": "/icons/192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icons/512.png", "sizes": "512x512", "type": "image/png" },
    { "src": "/icons/512-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
  ]
}
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#1a1a2e">

एंड्रॉइड पर मास्क करने योग्य आइकन मायने रखता है, जहां एक गैर-मास्क करने योग्य आइकन एक ऐसे आकार में क्रॉप हो जाता है जो अक्सर आपके लोगो का हिस्सा काट देता है।

चरण 2: सेवा कर्मी का पंजीकरण

if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    try {
      const reg = await navigator.serviceWorker.register('/sw.js');

      // Notify the user when a new version is waiting.
      reg.addEventListener('updatefound', () => {
        const newWorker = reg.installing;
        newWorker.addEventListener('statechange', () => {
          if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
            showUpdateBanner(() => {
              newWorker.postMessage({ type: 'SKIP_WAITING' });
            });
          }
        });
      });
    } catch (err) {
      console.error('Service worker registration failed:', err);
    }
  });
}

// Reload once the new worker takes control.
let refreshing = false;
navigator.serviceWorker?.addEventListener('controllerchange', () => {
  if (refreshing) return;
  refreshing = true;
  window.location.reload();
});

उपयोगकर्ता को बताए बिना कभी भी किसी नए कर्मचारी को जबरदस्ती सक्रिय न करें। सत्र के मध्य में चल रहे एप्लिकेशन के नीचे कोड को स्वैप करने से भ्रमित करने वाली विफलताएं होती हैं – इसके बजाय पुनः लोड करने की पेशकश करें।

चरण 3: सेवा कार्यकर्ता और इसकी कैशिंग रणनीतियाँ

विभिन्न संसाधन अलग-अलग रणनीतियाँ चाहते हैं, और हर चीज़ के लिए एक रणनीति का उपयोग करना सामान्य गलती है।

रणनीति के लिए उपयोग करें पहले कैश करें
हैश की गई स्थैतिक संपत्तियाँ – वे कभी नहीं बदलतीं नेटवर्क पहले
एपीआई डेटा जहां ताजगी मायने रखती है पुनर्वैधीकरण करते समय बासी
सामग्री जो थोड़ी पुरानी हो सकती है – अवतार, लिस्टिंग केवल नेटवर्क
दुष्प्रभाव वाली कोई भी चीज़ आवश्यक है क्योंकि प्रतिक्रिया निकाय को केवल एक बार पढ़ा जा सकता है। मूल को कैशिंग करने और उसे पृष्ठ पर वापस करने पर उसका दो बार उपभोग होता है, और पृष्ठ को एक खाली भाग प्राप्त होता है।
// sw.js
const VERSION = 'v3';
const SHELL_CACHE = `shell-${VERSION}`;
const DATA_CACHE  = `data-${VERSION}`;

const SHELL_ASSETS = [
  '/',
  '/index.html',
  '/offline.html',
  '/styles.css',
  '/app.js',
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(SHELL_CACHE).then(c => c.addAll(SHELL_ASSETS))
  );
});

self.addEventListener('activate', (event) => {
  event.waitUntil((async () => {
    const keys = await caches.keys();
    await Promise.all(
      keys.filter(k => !k.endsWith(VERSION)).map(k => caches.delete(k))
    );
    await self.clients.claim();
  })());
});

self.addEventListener('message', (event) => {
  if (event.data?.type === 'SKIP_WAITING') self.skipWaiting();
});

self.addEventListener('fetch', (event) => {
  const { request } = event;

  // Never cache anything that changes server state.
  if (request.method !== 'GET') return;

  const url = new URL(request.url);

  // Navigations: network first, fall back to the offline page.
  if (request.mode === 'navigate') {
    event.respondWith(
      fetch(request).catch(() => caches.match('/offline.html'))
    );
    return;
  }

  // API: network first, fall back to the cached copy.
  if (url.pathname.startsWith('/api/')) {
    event.respondWith(networkFirst(request));
    return;
  }

  // Static assets: cache first.
  event.respondWith(cacheFirst(request));
});

async function networkFirst(request) {
  const cache = await caches.open(DATA_CACHE);
  try {
    const response = await fetch(request);
    if (response.ok) cache.put(request, response.clone());
    return response;
  } catch {
    const cached = await cache.match(request);
    if (cached) return cached;
    return new Response(
      JSON.stringify({ error: 'offline' }),
      { status: 503, headers: { 'Content-Type': 'application/json' } }
    );
  }
}

async function cacheFirst(request) {
  const cached = await caches.match(request);
  if (cached) return cached;

  const response = await fetch(request);
  if (response.ok) {
    const cache = await caches.open(SHELL_CACHE);
    cache.put(request, response.clone());
  }
  return response;
}

response.clone()चरण 4: संरचित डेटा के लिए IndexedDB

कैश संग्रहण संपूर्ण HTTP प्रतिक्रियाएँ रखता है। उन रिकॉर्ड्स के लिए जिन्हें आपको अलग-अलग क्वेरी करने, सॉर्ट करने और अपडेट करने की आवश्यकता है, IndexedDB का उपयोग करें। कच्चा एपीआई वर्बोज़ है, इसलिए एक पतला आवरण इसके लायक है।

क्रम पर ध्यान दें: पहले स्थानीय भंडारण पर लिखें, फिर सिंक को कतारबद्ध करें। यूआई तुरंत अपडेट हो जाता है और इससे कोई फर्क नहीं पड़ता कि नेटवर्क उपलब्ध है या नहीं। ऑफ़लाइन-प्रथम का संपूर्ण बिंदु यही है।

import { openDB } from 'idb';

const db = await openDB('field-notes', 2, {
  upgrade(db, oldVersion) {
    if (oldVersion < 1) {
      const notes = db.createObjectStore('notes', { keyPath: 'id' });
      notes.createIndex('by-updated', 'updatedAt');
      notes.createIndex('by-synced', 'synced');
    }
    if (oldVersion < 2) {
      db.createObjectStore('outbox', { keyPath: 'id', autoIncrement: true });
    }
  },
});

export async function saveNote(note) {
  const record = { ...note, updatedAt: Date.now(), synced: false };
  await db.put('notes', record);
  await db.add('outbox', { type: 'saveNote', payload: record });
  await requestSync();
  return record;
}

export async function listNotes() {
  return db.getAllFromIndex('notes', 'by-updated');
}

चरण 5: बैकग्राउंड सिंक

बैकग्राउंड सिंक कनेक्टिविटी वापस आने पर ब्राउज़र को कतारबद्ध लेखन को फिर से चलाने की सुविधा देता है, भले ही पेज बंद कर दिया गया हो।

बैकग्राउंड सिंक हर ब्राउज़र में उपलब्ध नहीं है. हमेशा एक फ़ॉलबैक रखें जो पृष्ठ लोड होने पर और

// In the page
async function requestSync() {
  const reg = await navigator.serviceWorker.ready;
  if ('sync' in reg) {
    await reg.sync.register('sync-outbox');
  } else {
    await flushOutbox();   // fall back to an immediate attempt
  }
}
// In sw.js
self.addEventListener('sync', (event) => {
  if (event.tag === 'sync-outbox') {
    event.waitUntil(flushOutbox());
  }
});

async function flushOutbox() {
  const db = await openDB('field-notes', 2);
  const items = await db.getAll('outbox');

  for (const item of items) {
    try {
      const res = await fetch('/api/notes', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(item.payload),
      });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);

      await db.delete('outbox', item.id);
      await db.put('notes', { ...item.payload, synced: true });
    } catch {
      // Leave it queued; the sync event fires again later.
      return;
    }
  }
}

होने पर आउटबॉक्स को फ्लश कर देता है घटना आग.onlineसंघर्षों को संभालना

ऑफ़लाइन लिखने से टकराव पैदा होता है, और अन्यथा दिखावा करने से चुपचाप डेटा हानि होती है। एक नीति स्पष्ट रूप से तय करें.

अंतिम लेखन जीतता है

सबसे सरल है – एक टाइमस्टैम्प भेजें और सर्वर को नया टाइमस्टैम्प लेने दें। व्यक्तिगत नोट्स के लिए पर्याप्त, सहयोगात्मक किसी भी चीज़ के लिए ग़लत।संघर्ष पर सर्वर जीतता है

सुरक्षित है और उपयोगकर्ता को यह बताना आवश्यक है कि उनका ऑफ़लाइन परिवर्तन अस्वीकार कर दिया गया है। इसे कभी भी चुपचाप न त्यागें.मर्ज

सबसे अच्छा है जहां डेटा संरचना इसका समर्थन करती है, जिसका मतलब आमतौर पर वास्तव में सहयोगी दस्तावेजों के लिए सीआरडीटी होता है।भंडारण सीमाएँ

const res = await fetch('/api/notes/' + note.id, {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'If-Unmodified-Since': new Date(note.serverUpdatedAt).toUTCString(),
  },
  body: JSON.stringify(note),
});

if (res.status === 412) {
  await queueConflictForReview(note);   // surface it, never drop it
}

ब्राउज़र दबाव में भंडारण हटा देते हैं। ऐसे ऐप के लिए जिसका स्थानीय डेटा सिंक होने तक सत्य का स्रोत है, दृढ़ता का अनुरोध करें।

ऑफ़लाइन परीक्षण ठीक से

if (navigator.storage?.persist) {
  const persisted = await navigator.storage.persist();
  console.log('Persistent storage:', persisted);
}

const { usage, quota } = await navigator.storage.estimate();
console.log(`Using ${(usage / 1024 / 1024).toFixed(1)}MB of ${(quota / 1024 / 1024).toFixed(0)}MB`);

DevTools का ऑफ़लाइन चेकबॉक्स एक प्रारंभिक बिंदु है, परीक्षण नहीं। यह भी जांचें: ऑफ़लाइन होने पर पुनः लोड करना, स्पष्ट रूप से अनुपस्थित होने के बजाय धीमा और परतदार कनेक्शन, जब सेवा कार्यकर्ता मध्य सत्र को अपडेट करता है तो क्या होता है, और क्या कतारबद्ध लेखन पूर्ण ब्राउज़र पुनरारंभ से बच जाता है।

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

# Verify the manifest, service worker, and installability
npx lighthouse https://your-app.com --only-categories=pwa --view

POST अनुरोधों को कैशिंग करना.

कैश स्टोरेज केवल GET को संभालता है, और साइड-इफेक्टिंग अनुरोधों को कैश करना वैसे भी गलत होगा।भूल जाना

पृष्ठ को एक खाली बॉडी प्राप्त होती है.response.clone().पुराने कैश को कभी भी अमान्य न करें.

उपयोगकर्ताओं को अनिश्चित काल के लिए पुरानी संपत्तियां मिलती हैं। संस्करण कैश नाम और सक्रिय होने पर पुराने हटा दें।कॉलिंग

बिना शर्त.skipWaiting() चल रहे सत्र के अंतर्गत कोड बदलता है।यूआई में नेटवर्क से पढ़ना।

IndexedDB से पढ़ें और अलग से सिंक करें, अन्यथा ऐप ऑफ़लाइन-प्रथम नहीं है।असफल ऑफ़लाइन लेखन को हटाना।

मौन डेटा हानि सबसे खराब संभावित परिणाम है – हमेशा सतही टकराव।निष्कर्ष

ऑफ़लाइन-प्रथम एक वास्तुशिल्प निर्णय पर आधारित है:

ऑफ़लाइन-प्रथम एक वास्तुशिल्प निर्णय पर आधारित है:यूआई स्थानीय भंडारण को पढ़ता और लिखता है, और पृष्ठभूमि में सिंक्रनाइज़ेशन अलग से होता है। एक संस्करणित सेवा कार्यकर्ता के साथ एप्लिकेशन शेल को कैश करें, संरचित डेटा को IndexedDB में रखें, कतार एक आउटबॉक्स में लिखें और उन्हें बैकग्राउंड सिंक के साथ फिर से चलाएं, और गलती से किसी एक की खोज करने के बजाय जानबूझकर एक संघर्ष नीति चुनें। साफ़ ऑफ़लाइन टॉगल के बजाय परतदार कनेक्शन के साथ परीक्षण करें, क्योंकि उपयोगकर्ताओं को वास्तव में रुक-रुक कर होने वाली कनेक्टिविटी का अनुभव होता है।

MD Rafikul Islam

Written by

MD Rafikul Islam is a software developer and the editor of TechPulse. He writes about developer tooling, hardware, and the practical decisions that come up in day-to-day engineering work — which laptop to buy, which framework to commit to, why a build broke at 2am. He tests the tools he writes about and says plainly when something is not worth the money. Corrections and corrections requests are welcome at rony.yf25@gmail.com.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *

🌐 Read in:🇬🇧 English🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা