ServiceWorker: 수명 주기, 업데이트 및 알림

If you have tried CRA (create react app), have you ever wonder what does this file - `/src/serviceWorker.js` do? In this article, I will demonstrate what we can do by implementing the service worker into our application.


시작하기 전에 서비스 작업자가 제대로 처리하지 않으면 버그가 있을 수 있으므로 이 기사를 확인하여 몇 가지 중요한 지식을 미리 확인하는 것이 좋습니다. - Offline-First Considerations

의제


  • Register Service Worker
  • Event: install
  • Event: activate
  • Event: fetch
  • Event: message
  • Event: updatefound & statechange
  • Web Push Notification
  • Event: push

  • 서비스 워커 등록

    At first, we need to register service worker.

      navigator.serviceWorker
        .register("/sw.js")
        .then((reg) => {
          // no controller exist, page wasn't loaded via a service worker
          if (!navigator.serviceWorker.controller) {
            return;
          }
    
          if (reg.waiting) {
            // If we have a new version of the service worker is waiting,
            // we can display the message to the user and allow them
            // to trigger updates manually.
            // Otherwise, the browser will replace the service worker
            // when the user closes or navigate away from all tabs using
            // the current service worker.
            return;
          }
          if (reg.installing) {
            // If we have a new service worker is installing, we can
            // tracking the status and display the message once the
            // installation is finished.
            return;
          }
        });
    

    이벤트: 설치

    The install event is the first event a service worker gets, and it only happens once.

    We can cache the pages here.

    const urlsToCache = ["/faq", "/contact"];
    self.addEventListener("install", (event) => {
      event.waitUntil(
        caches.open(CACHE_NAME).then((cache) => {
          return cache.addAll(urlsToCache);
        })
      );
    });
    

    이벤트: 활성화

    Once your service worker is ready to control clients, we'll get an activate event.

    It's common to delete the old caches here.

    self.addEventListener("activate", (event) => {
      event.waitUntil(
        caches.keys().then((cacheNames) => {
          const promiseArr = cacheNames.map((item) => {
            if (item !== CACHE_NAME) {
              return caches.delete(item);
            }
          });
          return Promise.all(promiseArr);
        })
      );
    });
    

    이벤트: 가져오기

    We can intercept the request and custom the response in the fetch event.

    self.addEventListener("fetch", (event) => {
      // hijacking path and return a mock HTML content
      if (event.request.url.includes("/faq")) {
        event.respondWith(
          new Response("<div>Mock FAQ Page</div>", {
            headers: { "Content-Type": "text/html" },
          })
        );
      }
      // hijacking API request and return mock response in JSON format
      if (event.request.url.includes("/api/users")) {
        const data = [
          {
            id: "0001",
            name: "andrew",
          },
        ];
        const blob = new Blob(
          [JSON.stringify(data, null, 2)],
          { type: "application/json" }
        );
        const init = { status: 200, statusText: "default mock response" };
        const defaultResponse = new Response(blob, init);
        event.respondWith(defaultResponse);
      }
    
      // Stale-while-revalidate:
      // return the cached version if it exists. At the same time,
      // send a request to get the latest version and update the cache
      const requestUrl = new URL(event.request.url);
      if (requestUrl.pathname.startsWith("/avatars/")) {
        const response = caches.open(CACHE_NAME).then((cache) => {
          return cache.match(event.request).then((response) => {
            const networkFetch = fetch(event.request)
              .then((networkResponse) => {
                cache.put(event.request, networkResponse.clone());
                return networkResponse;
              });
    
            return response || networkFetch;
          });
        });
    
        event.respondWith(response);
        return;
      }
    });
    

    이벤트: 메시지

    We can use postMessage to communicate with the service worker.
    Here, we bind a click event to send a postMessage to the service worker.
    In the service worker, we can listen to the message event to received the postMessage.

    • send message to service worker
    function handleClickEven() {
      worker.postMessage({ action: "skipWaiting" });
    }
    
    • receive message
    self.addEventListener("message", (event) => {
      if (event.data.action === "skipWaiting") {
        // skip waiting to apply the new version of service worker
        self.skipWaiting(); 
      }
    });
    

    이벤트: updatefound & statechange

    We can listen to the updatefound event to see if we have a new service worker.
    If there is a service worker is installing, we listen to the statechange event,
    once the install is finished, we can display a message to notify our users.

    self.addEventListener("updatefound", () => {
      if (reg.installing) {
        reg.installing.addEventListener("statechange", () => {
          if (worker.state == "installed") {
            // display a message to tell our users that
            // there's a new service worker is installed
          }
        });
      }
    });
    

    웹 푸시 알림

    We can use a service worker to handle the notification.
    Here, we ask permission to display the notification, if the user agrees,
    then we can get the subscription information.

    • Get permission & subscription
    if (Notification && Notification.permission === "default") {
      Notification.requestPermission().then((result) => {
        if (result === "denied") {
          return;
        }
    
        if (result === "granted") {
          if (navigator && navigator.serviceWorker) {
            navigator.serviceWorker.ready.then((reg) => {
              reg.pushManager
                .getSubscription()
                .then((subscription: any) => {
                  if (!subscription) {
                    // we need to encrypt the data for web push notification,
                    // I use web-push to generate the public and private key.
                    // You can check their documentation for more detail.
                    // https://github.com/web-push-libs/web-push
                    const vapidPublicKey = "xxxxx";
                    const applicationServerKey = 
                      urlBase64ToUint8Array(vapidPublicKey);
                    return reg.pushManager.subscribe({
                      userVisibleOnly: true,
                      applicationServerKey,
                    });
                  }
    
                  return subscription;
                })
                .then((sub) => {
                  // Get the subscription information here, we need to
                  // create an API and save them into our database
                  /*
                    {
                      endpoint:
                        "https://fcm.googleapis.com/fcm/send/xxx",
                      keys: {
                        auth: "xxx",
                        p256dh: "xxx",
                      },
                    };
                  */
                });
            });
          }
        }
      });
    }
    
    • send notification
    const webpush = require('web-push');
    
    webpush.setVapidDetails("mailto:[email protected]", "my_private_key");
    const pushConfig = {
      endpoint: sub.endpoint,
      keys: {
        auth: sub.keys.auth,
        p256dh: sub.keys.p256dh,
      },
    };
    webpush
      .sendNotification(
        pushConfig,
        JSON.stringify({ title: "Test Title", content: "Test Content" })
      )
      .catch((err) => {
        console.log(err);
      });
    

    이벤트: 푸시

    We can receive the web push message by listening to the push event.

    self.addEventListener("push", (event) => {
      if (event.data) {
        try {
          data = JSON.parse(event.data.text());
          event.waitUntil(
            self.registration.showNotification(data.title, {
              body: data.content,
              icon: "/icon-192.png",
              badge: "/badge-192.png",
            })
          );
        } catch (e) {
          console.error('push event data parse fail');
        }
      }
    });
    

    결론



    이상입니다. 이 기사가 서비스 워커에 익숙해지는 데 도움이 되었으면 합니다.


    참조


  • Annie SprattUnsplash에 의해 사진
  • Offline Web Applications
  • The Service Worker Event
  • Making a Progressive Web App
  • The Service Worker Lifecycle
  • web-push
  • 좋은 웹페이지 즐겨찾기