🌐 Detecting your location…

কিভাবে 2026 সালে একটি অফলাইন-প্রথম PWA তৈরি করবেন: পরিষেবা কর্মী এবং IndexedDB গাইড

⏱️4 min read  ·  735 words

অফলাইন-প্রথম মানে অ্যাপ্লিকেশনটি ডিফল্টরূপে স্থানীয় ডেটা থেকে কাজ করে এবং নেটওয়ার্কটিকে একটি বর্ধিতকরণ হিসাবে বিবেচনা করে। এটি “অনলাইনে কাজ করে, অফলাইনে থাকলে অবনমিত হয়” থেকে একটি ভিন্ন ডিজাইন এবং এটি এমন একটি অ্যাপ তৈরি করে যা একটি ভাল সংযোগেও তাৎক্ষণিক অনুভব করে৷ এই নির্দেশিকাটি সঠিকভাবে তৈরি করে: পরিষেবা কর্মী ক্যাশিং, স্ট্রাকচার্ড ডেটার জন্য IndexedDB এবং অফলাইনে লেখার জন্য ব্যাকগ্রাউন্ড সিঙ্ক।

📋 Table of Contents

  1. স্থাপত্য
  2. ধাপ 1: ম্যানিফেস্ট
  3. ধাপ 2: পরিষেবা কর্মী নিবন্ধন
  4. ধাপ 3: পরিষেবা কর্মী এবং এর ক্যাশিং কৌশলগুলি
  5. ধাপ 4: স্ট্রাকচার্ড ডেটার জন্য IndexedDB
  6. ধাপ 5: ব্যাকগ্রাউন্ড সিঙ্ক
  7. অফলাইন লেখাগুলি দ্বন্দ্ব তৈরি করে, এবং অন্যথায় ভান করলে নীরব ডেটা ক্ষতি হয়। স্পষ্টভাবে একটি নীতি নির্ধারণ করুন.
  8. ব্রাউজার চাপের মধ্যে স্টোরেজ উচ্ছেদ করে। একটি অ্যাপের জন্য যার স্থানীয় ডেটা সিঙ্ক না হওয়া পর্যন্ত সত্যের উত্স, অধ্যবসায়ের অনুরোধ করুন৷
  9. DevTools এর অফলাইন চেকবক্স একটি সূচনা বিন্দু, একটি পরীক্ষা নয়। এছাড়াও চেক করুন: অফলাইনে থাকা অবস্থায় পুনরায় লোড হচ্ছে, একটি পরিষ্কারভাবে অনুপস্থিত একটির পরিবর্তে একটি ধীর এবং ফ্ল্যাকি সংযোগ, যখন পরিষেবা কর্মী মাঝামাঝি সেশন আপডেট করে তখন কী ঘটে এবং একটি সারিবদ্ধ লেখা একটি সম্পূর্ণ ব্রাউজার রিস্টার্টে বেঁচে থাকে কিনা।
  10. POST অনুরোধ ক্যাশে করা হচ্ছে।
  11. অফলাইন-প্রথম একটি স্থাপত্য সিদ্ধান্তের উপর নির্ভর করে:

স্থাপত্য

  1. সেবা কর্মী নেটওয়ার্ক অনুরোধ আটকায় এবং ক্যাশে থেকে পরিবেশন করে
  2. ক্যাশে স্টোরেজ অ্যাপ্লিকেশন শেল ধারণ করে — HTML, CSS, JavaScript, ফন্ট
  3. IndexedDB স্ট্রাকচার্ড ডেটা ধারণ করে — ব্যবহারকারীর পড়া ও লেখা রেকর্ড করে
  4. ব্যাকগ্রাউন্ড সিঙ্ক সংযোগ ফিরে আসার পরে রিপ্লে লিখে অফলাইন করা হয়

মূল মানসিক পরিবর্তন: UI 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: পরিষেবা কর্মী এবং এর ক্যাশিং কৌশলগুলি

বিভিন্ন সংস্থান বিভিন্ন কৌশল চায়, এবং সবকিছুর জন্য একটি কৌশল ব্যবহার করা স্বাভাবিক ভুল।

কৌশল জন্য ব্যবহার করুন
প্রথমে ক্যাশে হ্যাশড স্ট্যাটিক সম্পদ — তারা কখনই পরিবর্তন হয় না
নেটওয়ার্ক প্রথম API ডেটা যেখানে সতেজতা গুরুত্বপূর্ণ
পুনঃপ্রমাণ করার সময় বাসি কন্টেন্ট যা সামান্য পুরানো হতে পারে — অবতার, তালিকা
শুধুমাত্র নেটওয়ার্ক পার্শ্বপ্রতিক্রিয়া সহ যে কোন কিছু
// 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 ব্যবহার করুন। কাঁচা API ভার্বোস, তাই একটি পাতলা মোড়ক এটি মূল্যবান।

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

অর্ডারটি নোট করুন: প্রথমে স্থানীয় স্টোরেজে লিখুন, তারপর সিঙ্ক সারি করুন। UI তাৎক্ষণিকভাবে আপডেট হয় এবং নেটওয়ার্ক উপলব্ধ কিনা তা চিন্তা করে না। এটি অফলাইন-প্রথম পুরো পয়েন্ট।

ধাপ 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দ্বন্দ্ব পরিচালনা

অফলাইন লেখাগুলি দ্বন্দ্ব তৈরি করে, এবং অন্যথায় ভান করলে নীরব ডেটা ক্ষতি হয়। স্পষ্টভাবে একটি নীতি নির্ধারণ করুন.

শেষ লেখার জয়

সবচেয়ে সহজ – একটি টাইমস্ট্যাম্প পাঠান এবং সার্ভারটিকে নতুনটি নিতে দিন৷ ব্যক্তিগত নোটের জন্য পর্যাপ্ত, সহযোগী কিছুর জন্য ভুল।দ্বন্দ্বে সার্ভার জয়ী হয়

নিরাপদ এবং ব্যবহারকারীকে জানানো প্রয়োজন যে তাদের অফলাইন পরিবর্তন প্রত্যাখ্যান করা হয়েছে। চুপচাপ কখনোই ফেলে দেবেন না।মার্জ

যেখানে ডেটা স্ট্রাকচার এটিকে সমর্থন করে সেটিই সর্বোত্তম, যার অর্থ সাধারণত সত্যিকারের সহযোগিতামূলক নথিগুলির জন্য একটি CRDT।সঞ্চয় সীমা

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() চলমান সেশনের অধীনে কোড পরিবর্তন হয়।UI এ নেটওয়ার্ক থেকে পড়া।

IndexedDB থেকে পড়ুন এবং আলাদাভাবে সিঙ্ক করুন, অথবা অ্যাপটি অফলাইন-প্রথম নয়।অফলাইনে লেখা ড্রপিং ব্যর্থ হয়েছে৷

নীরব তথ্য হারানো সবচেয়ে খারাপ সম্ভাব্য ফলাফল – সর্বদা পৃষ্ঠ দ্বন্দ্ব।উপসংহার

অফলাইন-প্রথম একটি স্থাপত্য সিদ্ধান্তের উপর নির্ভর করে:

অফলাইন-প্রথম একটি স্থাপত্য সিদ্ধান্তের উপর নির্ভর করে:UI স্থানীয় স্টোরেজ পড়ে এবং লেখে এবং পটভূমিতে সিঙ্ক্রোনাইজেশন আলাদাভাবে ঘটে। একটি সংস্করণযুক্ত পরিষেবা কর্মীর সাথে অ্যাপ্লিকেশন শেলটি ক্যাশে করুন, 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🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা