PWA چیست؟

در سال ۲۰۱۵ عبارت Progressive Web App یا همان PWA توسط گوگل معرفی شد. PWAها وب اپلیکیشن‌هایی هستند که با استفاده از تکنولوژی‌های (JavaScript، HTML، CSS، WebAssembly) ساخته می‌شوند و تجربه‌ای مشابه با Native App را در کاربران ایجاد می‌کنند. PWAها با وجود داشتن یک Codebase، می‌توانند نصب شوند و روی تمام دستگاه‌ها (Windows, macOS, Android, iOS) اجرا شوند و همچنین کاربران می توانند آن را به home screen دستگاه خود اضافه کنند.

مزایای ‌PWA:

با افزایش حمایت‌ سیستم عامل‌ها(OS) و مرورگرها(browser) از این وب تکنولوژی، PWA‌ها بهترین ویژگی‌های وبسایت‌ها و Native App‌ها را دارند:

  • میزان دسترسی بالا و راحت: آن‌ها میتوانند توسط موتورهای جستجو پیدا شوند، با ‌‌URL باز شوند، به اشتراک گذاشته شوند و با هر دستگاهی که مرورگر دارد اجرا شوند.
  • تجربه کاربری مشابه با Native App: آن‌ها می‌توانند به صورت آفلاین کار کنند، نصب شوند و در 

home screen دستگاه اضافه شود، Push Notification دارند و می‌توان آن‌ها را در ‌Store منتشر کرد.

  • هزینه پایین توسعه و نگهداری نسبت به ‌Native Appها: PWA‌ها با یک Codebase، برای تمام پلتفرم‌ها توسعه داده می‌شوند و نیازمند چند تیم توسعه مختلف برای هر پلتفرم نمی‌باشد که این موضوع باعث کاهش هزینه‌های توسعه و نگهداری می‌شود.

 

محدودیت‌های PWA:

با وجود حمایت‌ سیستم عامل‌ها، همچنان محدودیت‌هایی برای ‌PWA در سیستم عامل iOS وجود دارد:

  •  در سیستم عامل iOS از push notification حمایت نمی‌شود.
  • برخلاف سیستم عامل اندروید، در سیستم عامل iOS در مرورگر Safari محدودیت ۵۰MB برای PWA تعیین می‌شود که اگر این فضای تعیین شده پر شود مرورگر درخواست پاک کردن محتوای ذخیره شده در  Cache را خواهد داشت.

 

البته لازم به ذکر است این محدودیت‌ها ممکن است در ورژن‌های جدیدتر سیستم عامل ‌iOS برطرف شود.

یک ‌PWA خوب چه ویژگی‌هایی دارد؟

  • قابلیت دسترسی: می‌توان PWA را در App Store و یا از طریق جستجو در Search Engineها پیدا کرد.
  • قابلیت نصب: می‌توان آن را نصب کرد و به home screen اضافه کرد (مانند Native Appها)
  • قابلیت تعامل با کاربر: می‌توان نوتیفیکیشن دریافت کرد (حتی اگر PWA در حال استفاده نباشد)  
  • تجربه کاربری offline: تجربه کاربری مناسب برای زمانی که اینترنت قطع می‌باشد.(مانند Native Appها)
  • امنیت: PWA از شبکه ایمن برای تبادل اطلاعات استفاده می‌کند.
  • طراحی Responsive: مطابق اندازه اسکرین دستگاه باشد.
  •  Linkability: بتوان آن را با URL باز کرد، به اشتراک گذاشت و یا Bookmark کرد (مانند وبسایت‌ها)

 

PWA از چه کامپوننت‌هایی تشکیل می‌شود؟

 PWA از این سه مولفه اصلی تشکیل می‌شوند:

  •  HTTPS: باعث می‌شود PWA ایمن باشد.
  •  Service Worker: باعث می‌شود ‌PWA قابل اعتماد و مستقل از Network باشد. 
  •  Manifest: باعث می‌شود ‌PWA قابل نصب باشد.

۱-  HTTPS: پروتکل ‌HTTPS یا HyperText Transfer Protocol Secure نسخه ایمن HTTP است که تبادلات بین Client و Server در Web App را رمزگذاری میکند. PWA باید حتما روی پروتکل HTTPS پیاده‌سازی شود و وجود HTTPS برای Service Worker‌ها الزامی است.

 

۲-  Service Worker: 

 Service Worker‌ها نوعی از Web Worker‌ها هستند که در Thread جداگانه کار می‌کنند و با استفاده از ‌Fetch API می‌توانند هر Network Request را رهگیری، تغییر و پاسخ دهند. همچنین Service Worker‌ها با استفاده از

 Cache API می‌توانند به Cache سمت Client و Asynchronous Store سمت Client (مانند IndexedDB) برای ذخیره اطلاعات (برای استفاده در مواقعی که کاربر آفلاین است) دسترسی دارد.

 Service Workerها باعث می‌شود PWA قابل اعتماد و Network-Independent باشد به طوری که تجربه کاربری موثری برای زمانی که کاربر آفلاین است یا اینترنت ضعیف دارد، فراهم می‌آورد.

 

Service Workerها چطور کار می‌کنند؟

برای اینکه بدانیم Service Workerها چطور کار می‌کنند، نیاز داریم دو مفهوم زیر را بدانیم:

  • ثبت سرویس ورکر (Service Worker Registration)
  • چرخه حیات سرویس ورکر (Service Worker Lifecycle)

 

ثبت سرویس ورکر (Service Worker Registration):

برای ثبت Service Worker از کد زیر استفاده می‌کنیم و این کد را در تگ script و در انتهای قسمت body در صفحه html قرار می‌دهیم: 

if(‘serviceWorker’ in navigator) {
// Register the service worker    
navigator.serviceWorker.register(‘/sw.js’, { scope: ‘/’ });
}

 

از آنجایی که Service Worker‌ها فقط می‌توانند تمام Request‌های صفحاتی که در Scope فایل Service Worker قرار می‌گیرند را رهگیری و مدیریت کنند، باید فایل Service Worker را در root برنامه دهیم.

 

چرخه حیات سرویس ورکر (Service Worker Lifecycle):

قسمت Lifecycle Event شامل این سه فاز می‌باشد:

  • ثبت (‌Registeration): در این فاز مرورگر Service Worker را ‌Registerمی‌کند.
  • نصب (Installation): در این فاز مرورگر  Service Worker را نصب می‌کند، همچنین می‌توان از این مرحله برای pre-caching resources استفاده کرد (برای cache کردن اطلاعاتی که تغییر چندانی نمی‌کنند مثل logo ها)
self.addEventListener( “install”, function( event ){
    console.log( “WORKER: install event in progress.” );
});
  • فعالسازی (Activation): در این فاز مرورگر با ارسال activate event نشان می‌دهد که Service Worker نصب شده است و این Service Worker می‌تواند جایگزین Service Worker قبلی شود و

 functional event‌ها را هندل کند.

self.addEventListener( “activate”, function( event ){
    console.log( “WORKER: activation event in progress.” );
    clients.claim();
    console.log( “WORKER: all clients are now controlled by me!” );
});

 

با استفاده از  clients.claim می‌توان نسخه قبلی را فورا با نسخه جدید جایگزین کرد.

توضیح در مورد Functional Eventهای سرویس ورکر:

  • مورد اول fetch event: این Event موقعی اجرا می‌شود که مرورگر تلاش می‌کند تا به صفحه‌ای که در Scope فایل Service Worker قرار دارد، دسترسی پیدا کند. Service Worker به عنوان ‌Interceptor عمل می‌کند و Response را بر اساس Cache Strategy تعیین شده برمی‌گرداند.
self.addEventListener( “fetch”, function( event ){
    console.log( “WORKER: Fetching”, event.request );
});
  • مورد دوم push event: این Event موقعی اجرا می‌شود که مرورگر یک پیام از Server دریافت می‌کند تا به عنوان Notification به کاربر نمایش دهد. با نمایش پیام به کاربر حتی مواقعی که برنامه در دستگاه کاربر باز نمی‌باشد، قابلیت تعامل با کاربر ایجاد می‌شود.
self.addEventListener( “push”, function( event ){
    console.log( “WORKER: Received notification”, event.data );
});

 

۳- Manifest: 

برای اینکه PWA مانند Native Appها قابلیت نصب داشته باشند از Manifest استفاده می‌کنیم. فایل ‌Manifest یک فایل با فرمت JSON می‌باشد که با استفاده از پراپرتی‌های (key-value pair) ظاهر و رفتار PWA را با تعریف نام، توضیحات درباره برنامه، icon و …. مشخص می‌کند.

برای ایجاد فایل Manifest فقط کافیست اطلاعات لازم را بصورت پراپرتی (key-value) در یک فایل JSON قرار داده و لینک آن را در قسمت head صفحه html قرار دهیم.

<link rel=“manifest” href=“/manifest.json”>

 

برای فایل Manifest حداقل باید این سه پراپرتی را قرار دهیم:

{
  “name”: “My PWA”,
  “lang”: “en-US”,
  “start_url”: “/”
}

 

که به ترتیب شامل نام برنامه، زبان آن و URL ای که باید موقع launch برنامه به آن navigate شود، می‌باشد.

 

منظور از Offline Support بودن PWA چیست؟

تفاوت کلیدی بین Native App و Web وابستگی به Network است. وقتی که Network در دسترس نیست، Web کارایی ندارد. این درحالیست که در PWA با استفاده از قابلیت Caching که توسط Service Worker‌ها از طریق Request Intercepting پیاده‌سازی می‌شود، تجربه کاربری مناسب به کاربر ارائه می‌شود.

Service Worker‌‌ها برای Cache کردن اطلاعات روی دستگاه کابر دو گزینه دارد:

  • ‌‌CacheStorage: یک API که برای ذخیره کردن network request/response استفاده می‌شود. این API توسط Service Worker و Thread اصلی جاوا اسکریپت برنامه قابل دسترس است و اطلاعات ذخیره شده در Cache را می‌توان Delete و Update کرد.
  •  IndexedDB: یک API برای ذخیره کردن حجم زیادی از structured data شامل فایل‌ها می‌باشد. IndexedDB یک object-oriented دیتابیس است که از (key-value pairs) استفاده می‌کند و  ایده‌آل برای ذخیره سازی ‌assetها می‌باشد.

برای cache کردن با سه سوال مواجه می‌شویم:

  •  چه چیزی را باید cache کرد؟
  • چه زمانی باید cache کرد؟ (در زمان نصب، فعال کردن و یا fetch event)
  • چطور fetch request را باید هندل کرد؟ (cache first, network first , a combination)

استراتژی های مختلف برای cache کردن وجود دارد که چند نمونه آن را به همراه کد بررسی می‌کنیم:

  • cache کردن در زمان نصب:
// named cache in Cache Storage
const CACHE_NAME = ‘devtools-tips-v3’;

// list of requests whose responses will be pre-cached at install
const INITIAL_CACHED_RESOURCES = [
  ‘/’,
  ‘/offline/’,
  ‘/assets/style.css’,
  ‘/assets/share.js’,
  ‘/assets/logo.png’,
  ];

// install event handler (note async operation)
// opens named cache, pre-caches identified resources above
self.addEventListener(‘install’, event => {
  event.waitUntil((async () => {
      const cache = await caches.open(CACHE_NAME);
      cache.addAll(INITIAL_CACHED_RESOURCES);
  })());
});

 

  • استراتژی cache-first : در این مورد Service Worker پاسخ مورد نظر را در اطلاعات ذخیره شده در cache جستجو می‌کند و اگر پاسخ را یافت آن را برمی‌گرداند و اگر پاسخ مورد نظر را پیدا نکند، سراغ network می‌رود.( به‌روزرسانی اطلاعات cache برای request‌های آینده اختیاری است.)

// We have a cache-first strategy,
// where we look for resources in the cache first
// and only on the network if this fails.
self.addEventListener(‘fetch’, event => {
    event.respondWith((async () => {
        const cache = await caches.open(CACHE_NAME);
        // Try the cache first.
        const cachedResponse = await cache.match(event.request);
        if (cachedResponse !== undefined) {
            // Cache hit, let’s send the cached resource.
            return cachedResponse;
        } else {
            // Nothing in cache, let’s go to the network.

            return fetch(event.request)
        }
    }
}

  • استراتژی network-first: این مورد برعکس مورد قبل  Service Worker ابتدا سراغ network می‌رود و اگر network قطع باشد، سراغ cache می‌رود.

self.addEventListener(“fetch”, event => {
  event.respondWith(
    fetch(event.request)
    .catch(error => {
      return caches.match(event.request) ;
    })
  );
});

 

  • استراتژی ترکیب (network-first, cache-first): در این مورد فورا پاسخ از cache برگردانده می‌شود، سپس network برای به روزرسانی محتوای cache چک می‌شود و اگر پاسخ موردنظر در cache یافت شود، با محتوای جدید گرفته شده از network جایگزین می‌شود تا برای request بعدی از محتوای به‌روزشده استفاده شود. بنابراین این استراتژی مزیت پاسخ سریع از cache و به‌روزرسانی آن در پس زمینه را دارد.

self.addEventListener(“fetch”, event => {
event.respondWith(
  caches.match(event.request).then(cachedResponse => {
      const networkFetch = fetch(event.request).then(response => {
        // update the cache with a clone of the network response
        caches.open(CACHE_NAME).then(cache => {
            cache.put(event.request, response.clone());
        });
      });
      // prioritize cached response over network
      return cachedResponse || networkFetch;
  }
)
)
});

 

نحوه تبدیل یک وبسایت معمولی به PWA:

برای این کار من یک پروژه که شامل (html, css, JavaScript) است و حدود سه سال پیش برای تمرین html و css انجام داده بودم را به PWA تبدیل می‌کنم و مراحل آن را توضیح می‌دهم.  لازم به ذکر است که PWA هیچ وابستگی به UI Framework مثل Vue.js ،Reactjs و یا Angular ندارد و تنها کافیست کامپوننت‌های مذکور موجود باشند. (لینک گیت‌هاب پروژه: https://github.com/NaserAhadi/food-recipe)

مرحله اول: ایجاد فایل Manifest

ابتدا یک فایل Manifest به فرمت json در root پروژه ایجاد می‌کنیم و پراپرتی‌های آن را تعریف می‌کنیم. فایل Manifest پروژه مذکور به قرار زیر است:

{
  “lang”: “en-us”,
  “name”: “Food Recipes App”,
  “short_name”: “Food-Recipes”,
  “description”: “A basic Food Recipes application Which show how cook delicious food”,
  “start_url”: “/”,
  “background_color”: “#۲f3d58”,
  “theme_color”: “#۲f3d58”,
  “orientation”: “any”,
  “display”: “standalone”,
  “icons”: [
      {
          “src”: “./images/chef-icon.png”,
          “sizes”: “۶۰۰×۶۰۰”
      }
  ]
}

 

لینک فایل Manifest را در صفحه html در قسمت تگ head قرار می‌دهیم.

<link rel=“manifest” href=“/manifest.json”>

 

مرحله دوم: ایجاد Service Worker و Register آن

همانطور که گفته شد فایل Service Worker را با نام sw.js در root پروژه ایجاد می‌کنیم تا تمام Request‌های صفحاتی که در Scope خود قرار دارند را رهگیری و مدیریت کند. فایل sw.js در install event صفحه html، فایل main.js، فایل style.css و عکس‌ها را cache می‌کند. همچنین با fetch event هر بار که request به سمت Server فرستاده می‌شود آن را Intercept می‌کند و با استفاده از استراتژی cache-first که در کد پیاده‌سازی شده، 

Service Worker اطلاعات Cache شده را برمی‌گرداند، بنابراین برنامه در مواقع آفلاین تجربه کاربری مناسب ارائه می‌دهد.

const CACHE_NAME = `food-recipes`;

// Use the install event to pre-cache all initial resources.
self.addEventListener(‘install’, event => {
event.waitUntil((async () => {
  const cache = await caches.open(CACHE_NAME);
  cache.addAll([
    ‘/’,
    ‘/main.js’,
    ‘/style.css’,
    ‘/images/chef-favicon.png’,
    ‘/images/chef-icon.png’,
    ‘/images/curry.png’,
    ‘/images/noodles.png’,
    ‘/images/stew.png’,
  ]);
})());
});

self.addEventListener(‘fetch’, event => {
event.respondWith((async () => {
  const cache = await caches.open(CACHE_NAME);

  // Get the resource from the cache.
  const cachedResponse = await cache.match(event.request);
  if (cachedResponse) {
    return cachedResponse;
  } else {
      try {
        // If the resource was not in the cache, try the network.
        const fetchResponse = await fetch(event.request);

        // Save the resource in the cache and return it.
        cache.put(event.request, fetchResponse.clone());
        return fetchResponse;
      } catch (e) {
        // The network failed.
      }
  }
})());
});

برای ثبت Service Worker کد زیر را در انتهای تگ body در صفحه html قرار می‌دهیم:

<script>
  if(‘serviceWorker’ in navigator) {
    navigator.serviceWorker.register(‘/sw.js’, { scope: ‘/’ });
  }
</script>

 

مرحله سوم: استفاده از local web server

من برای این برنامه به جای Deploy آن روی Web Server از http-server لایبرری Node.js که local web server است، استفاده کردم. برنامه در URL هایی local web server در اختیار ما می‌گذارد قابل دسترس است. برای اجرای برنامه از دستور زیر استفاده می‌کنیم.

npx http-server  

 

برنامه در http://localhost:8080  و دیگر URL هایی که به ما می‌دهد قابل اجراست. برای هدف Debugging مرورگرها اجازه می‌دهند localhost web server از PWA استفاده کنند حتی بدون پروتکل HTTPS.

 

درخواست نصب برنامه به محض اجرای برنامه در مرورگر:

شکل ۵- اجرای برنامه در مرورگر

تبدیل ‌PWA به TWA:

قبل از هر چیز لازم به ذکر است Trusted Web Activities یا TWA راهی است برای انتشار PWA به عنوان اپلیکیشن APK در App Store. برای ایجاد اپلیکیشن APK از PWA ما می‌توانیم از ابزار آنلاین استفاده کنیم که آماده برای انتشار آن در App Store می‌باشد و نیازی به نصب Android Studio روی سیستم نداریم.

انتشار PWA در App Store قابلیت اکتشاف برنامه را بالا می‌برد بطوریکه کاربر علاوه بر دسترسی به برنامه با URL در مرورگرها، ‌‌می‌تواند آن را با جستجو در App Storeها پیدا و نصب کند.

مراحل ایجاد ‌TWA:

  1. به سایت https://www.pwabuilder.com می‌رویم و URL برنامه PWA را وارد می‌کنیم و دکمه start را می‌زنیم. که سایت PWA را ارزیابی می‌کند و پیشنهاداتی برای بهبود Service Worker و Manifest می‌دهد.
  2. روی دکمه Package For Stores کلیک می‌کنیم و پکیج مربوط به Android را دانلود می‌کنیم.
  3. پکیج دانلود شده را می‌توان روی گوشی اندروید نصب و تست کرد.
  4. برای انتشار TWA در App Store، فایل assetlinks.json را در وبسایت در مسیر زیر Deploy کنید. 
<HOST_URL>/.well-known/assetlinks.json

این فایل با انطباق fingerprints نشان می‌دهد شما صاحب PWA و  TWA هستید و همچنین اجازه می‌دهد TWA در حالت fullscreen و بدون browser UI اجرا شود. نمونه‌ای از فایل assetlinks.json :

[{
  “relation”: [“delegate_permission/common.handle_all_urls”],
  “target” : {
              “namespace”: “android_app”,
              “package_name”: “app.webboard.twa”,
                “sha256_cert_fingerprints”: [“۹۲:۱C:08:E0:A6:4D:29:87:DF:70:3E:B4:0F:E9:0C:6D:D1:70:D0:8F:AD:97:29:64:EA:0A:69:A2:3F:27:C7:06”]
              }
}
]

 

بررسی تاثیر (PWA, TWA) در جذب کاربر برای استفاده از برنامه:

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

وبسایت پروژه در تاریخ June 1, 2021 (معادل ۱۱ خرداد سال ۱۴۰۰) انتشار یافته است و تا تاریخ

 October 19, 2022 (معادل ۲۷ مهر سال ۱۴۰۱) به (PWA, TWA) تبدیل نشده است که حدودا در عرض ۱۶ ماه مطابق تصویر زیر ۵۵۵ کاربر از این وبسایت استفاده می‌کردند. 

از تاریخ October 20, 2022 (معادل ۲۸ مهر سال ۱۴۰۱) این وبسایت به PWA و ‌TWA تبدیل و در بازار منتشر شده است. همانطور که مطابق تصویر زیر میبینیم، تعداد کاربران تا به امروز، در عرض حدودا ۴ ماه، از ۵۵۵ کاربر به ۱۱۲۹ کاربر رسیده است که نشان دهنده رشد دو برابری در مدت زمان کم می‌باشد.