توسعه فرانت‌اند

PWA از صفر: ساخت اپلیکیشن وب پیشرو

تفکیک نصب، آفلاین و Push؛ سه فایل کامل نمونه صفحه عمومی cache‌شده، روش سرو محلی، چرخه عمر و حدود نگهداری داده.

1404/11/14 3 دقیقه مطالعه 2,201 بازدید

کاربر صفحه‌ای را باز کرده و بعد اتصالش قطع می‌شود. اگر محتوای لازم قبلاً ذخیره شده باشد، وب می‌تواند بخشی از تجربه را ادامه دهد. اما وجود یک Service Worker به‌تنهایی همه قابلیت‌های اپ بومی، نصب و Push را فراهم نمی‌کند. ساخت PWA باید از نیاز مشخص شروع شود: چه چیزی بدون شبکه قابل استفاده است، چه کاری نیاز به ارتباط دارد و نتیجه هر وضعیت چگونه توضیح داده می‌شود.

قابلیت نصب، کار آفلاین و اعلان سه موضوع مرتبط اما مستقل‌اند. پشتیبانی آن‌ها به مرورگر و سیستم‌عامل وابسته است. سایت می‌تواند بدون نصب قابل استفاده باشد و بعد قابلیت‌های بیشتری اضافه کند. تصمیم محصول نباید به یک تعریف ساده «سه فایل و همه امکانات» تکیه کند. برای نمونه آموزشی این مقاله، هدف محدود است: نگهداری یک صفحه عمومی و manifest برای مشاهده دوباره در نبود اتصال.

Manifest چه چیزی را معرفی می‌کند؟

راهنمای نصب‌پذیری در MDN اطلاعات manifest و تفاوت محیط‌ها را توضیح می‌دهد. نام، نشانی شروع و شکل نمایش بخشی از قرارداد معرفی برنامه‌اند. manifest به خودی خود فایل‌ها را cache نمی‌کند. بعضی معیارهای پیشنهاد نصب نیز به iconهای واقعی نیاز دارند. نمونه این راهنما icon ندارد و بسته کامل تضمین‌شده برای پیشنهاد نصب محسوب نمی‌شود.

نشانی شروع و scope باید با محل برنامه هماهنگ باشند. یک نمونه در زیرپوشه نباید بی‌دلیل همه سایت را حوزه خود معرفی کند. نام برنامه نیز باید با چیزی که مخاطب دریافت می‌کند سازگار باشد. اگر صفحه نصب یک میانبر یا تجربه متفاوت می‌سازد، آن را اپ دارای قابلیت‌های ناموجود معرفی نکنید. پشتیبانی نصب را از مستندات محیط هدف بخوانید و رابط را بدون آن هم قابل استفاده نگه دارید.

Service Worker در زمینه جدا اجرا می‌شود

مرجع Service Worker درباره ثبت، حوزه و چرخه عمر توضیح می‌دهد. Worker به DOM صفحه دسترسی مستقیم ندارد و برای بعضی درخواست‌ها می‌تواند پاسخ جایگزین فراهم کند. در تولید، secure context لازم است؛ محیط توسعه localhost استثنای مناسب دارد. باز کردن فایل HTML به صورت مستقیم، همان قرارداد سرویس از localhost را ایجاد نمی‌کند.

ثبت worker با کنترل فوری همه صفحه‌های باز یکسان نیست. نسخه تازه ممکن است در مرحله انتظار باشد تا صفحه‌های وابسته به قبلی بسته شوند. اجبار به فعال‌سازی سریع می‌تواند فایل‌های نسخه‌های مختلف را مخلوط کند. سیاست update را بر اساس سازگاری برنامه تعیین کنید. نمونه زیر skipWaiting یا clients.claim به‌صورت اجباری ندارد و قرار نیست کاربر را وسط یک عمل حساس به نسخه تازه منتقل کند.

فایل‌های نمونه و روش سرو

سه فایل بعدی را به نام index.html، manifest.webmanifest و sw.js در یک پوشه آموزشی مستقل ذخیره کنید. هیچ تصویر، فونت یا API خارجی لازم نیست. برای سرو محلی، در همان پوشه npm init -y و سپس npm install -D http-server را اجرا کنید. با npx http-server . -p 8080 -c-1 فایل‌ها از localhost سرو می‌شوند. پیش‌نیاز ابزار، Node.js سازگار مانند نسخه 24 است.

صفحه را از http://localhost:8080/index.html مشاهده کنید. نخستین مراجعه نیاز به شبکه محلی دارد تا فایل‌ها دریافت و worker نصب شود. قابلیت آفلاین برای همان فایل‌هایی است که نصب موفق ذخیره کرده است، نه هر صفحه یا داده بیرونی. سرویس محلی فقط ابزار توسعه است و راه انتشار HTTPS محسوب نمی‌شود. این راهنما فایل‌ها و روش اجرا را معرفی می‌کند و ادعای نتیجه یک اجرای انجام‌شده ندارد.

<!doctype html>
<html lang="fa" dir="rtl"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="manifest" href="./manifest.webmanifest"><meta name="theme-color" content="#173f78"><title>صفحه عمومی آفلاین</title>
<style>body{font-family:Tahoma,Arial,sans-serif;line-height:1.9;background:#f2f4f7;color:#17243b;margin:0}main{max-width:38rem;margin:auto;padding:2rem 1rem}</style></head><body><main><h1>صفحه عمومی نمونه</h1><p>این متن ثابت پس از دریافت اولیه می‌تواند از cache همین برنامه خوانده شود.</p><p id="status" role="status">در حال بررسی امکان ثبت Service Worker.</p></main>
<script>
const status=document.getElementById('status');
if('serviceWorker' in navigator){navigator.serviceWorker.register('./sw.js').then(()=>navigator.serviceWorker.ready).then(()=>{status.textContent='Service Worker فعال ثبت شده است. کنترل صفحه و دریافت آفلاین به چرخه عمر آن وابسته است.';}).catch(()=>{status.textContent='ثبت Service Worker انجام نشد؛ متن صفحه همچنان قابل خواندن است.';});}else{status.textContent='این محیط Service Worker ارائه نمی‌کند؛ متن صفحه قابل خواندن است.';}
</script></body></html>

پیام صفحه وضعیت ثبت را می‌گوید، نه اینکه نصب روی دستگاه انجام شده است. محتوا عمومی و ثابت است و عملیات خصوصی ندارد. اگر Service Worker در محیط وجود نداشته باشد، متن اصلی همچنان خوانا می‌ماند. در محصول واقعی، اطلاعات ضروری را به ثبت worker وابسته نکنید؛ مسیر پایه وب باید بتواند کار اصلی را با امکانات موجود انجام دهد.

{"name":"صفحه عمومی نمونه","short_name":"نمونه","lang":"fa","dir":"rtl","start_url":"./index.html","scope":"./","display":"standalone","theme_color":"#173f78","background_color":"#f2f4f7"}

Manifest نمونه نام و نشانی نسبی شروع را مشخص می‌کند. display برابر standalone یک ترجیح نمایش است و تضمین نمی‌کند همه محیط‌ها همان تجربه را ارائه دهند. icon عمداً جزو فایل‌های این نمونه نیست، بنابراین مسیر تصویر معرفی‌نشده وجود ندارد. برای تکمیل نصب‌پذیری در محیط هدف، assetهای واقعی و معیارهای همان محیط باید جدا آماده شوند. قابلیت cache این مثال به داشتن icon وابسته نیست.

const PREFIX = 'tara-offline-demo-';
const CACHE = PREFIX + 'v1';
const INDEX = new URL('./index.html', self.location.href).href;
const MANIFEST = new URL('./manifest.webmanifest', self.location.href).href;
self.addEventListener('install', event => {
  event.waitUntil(caches.open(CACHE).then(cache => cache.addAll([INDEX, MANIFEST])));
});
self.addEventListener('activate', event => {
  event.waitUntil(caches.keys().then(keys => Promise.all(keys.filter(key => key.startsWith(PREFIX) && key !== CACHE).map(key => caches.delete(key)))));
});
self.addEventListener('fetch', event => {
  if (event.request.method !== 'GET') return;
  const url = new URL(event.request.url);
  if (url.origin !== self.location.origin || ![INDEX, MANIFEST].includes(url.href)) return;
  if (url.href === INDEX) {
    event.respondWith((async () => {
      const cache = await caches.open(CACHE);
      try {
        const response = await fetch(event.request);
        if (response.ok) await cache.put(INDEX, response.clone());
        return response;
      } catch {
        return (await cache.match(INDEX)) || new Response('Offline content is unavailable.', { status: 503, headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
      }
    })());
  } else {
    event.respondWith((async () => {
      const cache = await caches.open(CACHE);
      return (await cache.match(MANIFEST)) || fetch(event.request);
    })());
  }
});

دامنه cache را محدود نگه دارید

Worker فقط GETهای هم‌origin و نشانی‌های دقیق معرفی‌شده را مدیریت می‌کند. برای صفحه index، ابتدا شبکه را می‌خواهد و در شکست دریافت، نسخه قبلی را برمی‌گرداند. برای manifest، نسخه cache می‌تواند پاسخ اولیه باشد. فایل‌های خصوصی، درخواست POST و مقصدهای ناشناخته وارد این مسیر نشده‌اند. این دامنه کوچک کمک می‌کند قرارداد مثال روشن باشد؛ تعمیم آن به همه URLهای سایت تصمیم جداگانه‌ای لازم دارد.

مرجع Cache API قرارداد نگهداری Request و Response را توضیح می‌دهد. پاسخ برای ذخیره clone می‌شود چون بدنه جریان مصرفی دارد. عمر cache و حذف نسخه‌ها باید توسط برنامه مدیریت شود. تنظیم‌های کش HTTP را با رفتار خودکار ذخیره‌سازی این API یکسان فرض نکنید. نام نسخه در نمونه برای تشخیص cache متعلق به همین برنامه استفاده شده است.

استراتژی دریافت را بر اساس داده انتخاب کنید

Cache First برای فایل عمومی ثابت می‌تواند دریافت سریع‌تری بدهد، ولی تغییر محتوا به سیاست update نیاز دارد. Network First برای داده‌ای که تازگی مهم است یک انتخاب ممکن است، اما در شبکه کند می‌تواند انتظار ایجاد کند. Stale-While-Revalidate ابتدا نسخه قبلی را می‌دهد و همزمان تازه‌سازی می‌کند؛ مخاطب باید بداند داده قدیمی چه محدودیتی دارد. هیچ استراتژی بهترین تعادل برای همه درخواست‌ها نیست.

برای قیمت، ظرفیت و پرداخت، نسخه cache ممکن است برای تصمیم قطعی کافی نباشد. اگر داده قبلی نمایش داده می‌شود، زمان یا وضعیت تازگی را بیان کنید. راهنمای Checkout درباره نتیجه معتبر پرداخت توضیح دارد. امکان دیدن یک صفحه آفلاین نباید به وعده ثبت موفق سفارش بدون ارتباط تبدیل شود. هر عملیات اثرگذار نیاز به قرارداد تأیید و پیگیری خودش دارد.

خطا و داده گمشده

در نمونه، شکست نصب یکی از فایل‌های لازم می‌تواند نصب worker را متوقف کند. این رفتار باعث می‌شود بسته ناقص به عنوان آماده معرفی نشود. در پاسخ شبکه، وضعیت ناموفق مانند 404 بی‌دلیل به نسخه موفق قدیمی تبدیل نشده است؛ fallback فقط در شکست دریافت به کار می‌رود. این تفاوت مهم است چون منبع حذف‌شده با قطع ارتباط یکسان نیست و باید پیام متفاوتی داشته باشد.

مرورگر می‌تواند ذخیره محلی را محدود یا حذف کند، پس cache را نسخه تضمین‌شده همیشگی داده معرفی نکنید. صفحه‌ای که قبلاً دیده نشده معمولاً بدون قرارداد prefetch در دسترس نیست. برای محتوای قابل ذخیره، رابط باید معلوم کند چه چیز آماده است و چه چیز نیاز به اتصال دارد. اعلان مناسب درباره بیان وضعیت انتظار و نتیجه نامعلوم مکمل همین تصمیم‌هاست.

به‌روزرسانی و خروج از حساب

در activate نمونه، فقط cacheهایی با پیشوند مخصوص خود برنامه و نام نسخه متفاوت حذف می‌شوند. حذف همه cacheهای origin می‌تواند برنامه دیگر را مختل کند. در پروژه مشترک، نام‌گذاری و scope باید با سایر بخش‌ها هماهنگ باشد. تغییر فایل‌های cache‌شده نیاز به تغییر نسخه و قرارداد سازگاری دارد. کاربر نباید بدون خبر با ترکیب HTML قدیمی و کد ناسازگار تازه مواجه شود.

در برنامه دارای حساب، خروج باید اثر ذخیره داده خصوصی را هم در نظر بگیرد. cache عمومی و داده کاربر را با یک سیاست نگه ندارید. دستگاه مشترک می‌تواند نسخه باقی‌مانده اطلاعات را به فرد بعدی نشان دهد. رمزنگاری با کلید در دسترس همان برنامه نیز به‌تنهایی این مرز را حل نمی‌کند. انتخاب storage و دسترسی باید به نوع داده و نیاز محصول مربوط باشد.

قابلیت‌ها را تدریجی اضافه کنید

Push به مجوز، زیرساخت ارسال و رفتار محیط هدف نیاز دارد و در این نمونه وجود ندارد. همگام‌سازی پس‌زمینه هم برای همه محیط‌ها قرارداد یکسانی ندارد. افزودن هرکدام باید با دلیل کاربردی و راه fallback همراه باشد. برای محدودیت‌های محیط، راهنمای PWA در Safari را فقط از مسیر واقعی فهرست مجله دنبال کنید؛ ادعای پشتیبانی کامل را برای همه APIها تعمیم ندهید.

در تحویل، فایل‌های ذخیره‌شده، دامنه worker، عمر cache و رفتار update را ثبت کنید. جدا توضیح دهید چه کاری آفلاین ممکن است و کدام نتیجه به سرور وابسته است. وب پیشرو از پایه قابل استفاده و قابلیت‌های اضافه ساخته می‌شود. یک نمونه کوچک می‌تواند سازوکار را روشن کند، اما محصول قابل اعتماد به قرارداد داده، امنیت و وضعیت‌های واقعی نیاز دارد و با شمار فایل‌ها کامل نمی‌شود.

واکنش به این مقاله

اشتراک‌گذاری این مقاله:
دیدگاه‌ها 0
۰ / ۲۰۰۰

هنوز دیدگاهی ثبت نشده

اولین نفری باشید که نظر می‌دهد!