JavaScript Service Worker cơ bản

Cập nhật ngày 17/10/2021

Xin chào bạn! Không biết bạn đã từng nghe về Offline Web App chưa? Có thể bạn đã biết hoặc chưa. Hiểu một cách đơn giản, Offline Web App là một ứng dụng web có thể chạy ngay cả khi không có kết nối internet. Bây giờ, mình phân tích chút xíu nhé. Có 2 thứ cần thiết để web app có thể chạy được là: resources (html, js, css, ảnh,...) và định tuyến. Thì thằng mà chúng ta sẽ tìm hiểu sau đây là JavaScript Service Worker có thể đáp ứng được cả hai việc đó. Vậy...

JavaScript Service Worker là gì?

Service Worker là một script mà trình duyệt chạy ở dưới background, tách khỏi trang web và giúp thực hiện các tính năng không cần đến trang web, hay tương tác người dùng.

Service Worker có một số đặc điểm quan trọng là:

  • Không liên kết với một trang cụ thể
  • Không truy cập đến DOM
  • Có thể dừng khi không sử dụng và chạy khi cần thiết.
  • Chỉ hỗ trợ HTTPS

Với Service Worker chúng ta có thể:

Có lẽ đến đây thì bạn đã hiểu phần nào về JavaScript Service Worker rồi. Tiếp theo, chúng ta sẽ tìm hiểu về cách sử dụng nó.

Đăng ký JavaScript Service Worker

Giả sử, mình có một trang web đơn giản và cấu trúc các tệp tin như sau:

index.html
service_worker.js
main.js
style.css

Lúc này, mình sẽ đăng ký Service Worker trong file main.js:

if ("serviceWorker" in navigator) {
  navigator.serviceWorker
    .register("service_worker.js")
    .then((reg) => {
      console.log("Registered service worker");
    })
    .catch((err) => {
      console.log("Register service worker failed", err);
    });
}

Có một số lưu ý quan trọng khi đăng ký Service Worker là:

  • Vị trí của file service_worker.js quyết định đến URL của các trang mà nó điều khiển. Ở đây, mình đặt file service_worker.js cùng thư mục với index.html. Có nghĩa là Service Worker sẽ quản lý URL của toàn bộ trang web. Ngược lại, nếu mình đặt nó ở một thư mục khác, giả sử là: /apps/service_worker.js thì Service Worker sẽ chỉ quản lý các URL bắt đầu từ /apps/.
  • Hàm đăng ký .register trả về một Promise.
  • Trang đăng ký Service Worker phải là HTTPS.
  • Service Worker phải nằm trên cùng trang web đăng ký nó (same origin). Vì vậy, bạn không thể đặt file service_worker.js ở một nơi khác, rồi sử dụng importScripts() được.

Mình có tham khảo được một ví dụ mẫu về cách triển khai JavaScript Service Worker như sau (mình không nhớ mình tham khảo được ở đâu, chỉ nhớ là của Google, nên mong tác giả của script thông cảm):

/*
 * Names of the two caches used in this version of the service worker.
 * Change to v2, etc. when you update any of the local resources, which will
 * in turn trigger the install event again.
 */
const PRECACHE = "my-precache-v1";
const RUNTIME = "my-runtime";

// A list of local resources we always want to be cached.
const PRECACHE_URLS = [
  "index.html",
  "./", // Alias for index.html
  "style.css",
  "main.js",
];

// The install handler takes care of precaching the resources we always need.
self.addEventListener("install", (event) => {
  event.waitUntil(
    caches
      .open(PRECACHE)
      .then((cache) => cache.addAll(PRECACHE_URLS))
      .then(self.skipWaiting())
  );
});

// The activate handler takes care of cleaning up old caches.
self.addEventListener("activate", (event) => {
  const currentCaches = [PRECACHE, RUNTIME];
  event.waitUntil(
    caches
      .keys()
      .then((cacheNames) => {
        return cacheNames.filter(
          (cacheName) => !currentCaches.includes(cacheName)
        );
      })
      .then((cachesToDelete) => {
        return Promise.all(
          cachesToDelete.map((cacheToDelete) => {
            return caches.delete(cacheToDelete);
          })
        );
      })
      .then(() => self.clients.claim())
  );
});

/*
 * The fetch handler serves responses for same-origin resources from a cache.
 * If no response is found, it populates the runtime cache with the response
 * from the network before returning it to the page.
 */
self.addEventListener("fetch", (event) => {
  // Skip cross-origin requests, like those for Google Analytics.
  if (event.request.url.startsWith(self.location.origin)) {
    event.respondWith(
      caches.match(event.request).then((cachedResponse) => {
        if (cachedResponse) {
          return cachedResponse;
        }

        return caches.open(RUNTIME).then((cache) => {
          return fetch(event.request).then((response) => {
            // Put a copy of the response in the runtime cache.
            return cache.put(event.request, response.clone()).then(() => {
              return response;
            });
          });
        });
      })
    );
  }
});

Sau khi đăng ký thành công, Service Worker sẽ được download về phía client và thực hiện những việc sau đây.

JavaScript Service Worker - Install

Đoạn code dùng để install Service Worker:

const PRECACHE = "my-precache-v1";
const RUNTIME = "my-runtime"; // A list of local resources want to be cached.
const PRECACHE_URLS = [
  "index.html",
  "./", // Alias for index.html
  "style.css",
  "main.js",
];

// The install handler takes care of precaching the resources we always need.
self.addEventListener("install", (event) => {
  event.waitUntil(
    caches
      .open(PRECACHE)
      .then((cache) => cache.addAll(PRECACHE_URLS))
      .then(self.skipWaiting())
  );
});

Mục đích của của việc install là để lưu một số resources được định nghĩa ở array PRECACHE_URLS vào bộ nhớ đệm cache với tên định nghĩa bởi PRECACHE.

Sau khi lưu xong hết tất cả các resources cần thiết, hàm self.skipWaiting() dùng để dừng công việc hiện tại lại và chuyển ngay sang công việc tiếp theo.

JavaScript Service Worker - activate

Đoạn code dùng để active Service Worker là:

// The activate handler takes care of cleaning up old caches.
self.addEventListener("activate", (event) => {
  const currentCaches = [PRECACHE, RUNTIME];
  event.waitUntil(
    caches
      .keys()
      .then((cacheNames) => {
        return cacheNames.filter(
          (cacheName) => !currentCaches.includes(cacheName)
        );
      })
      .then((cachesToDelete) => {
        return Promise.all(
          cachesToDelete.map((cacheToDelete) => {
            return caches.delete(cacheToDelete);
          })
        );
      })
      .then(() => self.clients.claim())
  );
});

Mục đích của công việc activate này là để xóa đi bộ nhớ đệm cache cũ, và giữ lại cache mới nhất, cuối cùng là kích hoạt Service Worker.

Giả sử ban đầu bạn có hai cache là: PRECACHE = my-precache-v1RUNTIME. Bây giờ, bạn muốn thay đổi resources.

Làm sao để cập nhật được những resources mới này vào cache?

Bạn cần thay đổi tên của PRECACHE, ví dụ là: my-precache-v2. Lúc này currentCaches = ['my-precache-v2', RUNTIME]. Bây giờ chỉ cần dùng filter để lọc ra tên của cache không có trong currentCaches:

cacheNames.filter((cacheName) => !currentCaches.includes(cacheName));

Đó chính là my-precache-v1. Tiếp theo, xóa cache này đi:

.then(cachesToDelete => {
  return Promise.all(cachesToDelete.map(cacheToDelete => {
    return caches.delete(cacheToDelete);
  }));
}

Cuối cùng là kích hoạt Service Worker sử dụng self.clients.claim().

Bạn có thể kiểm tra lại bằng cách nhấn Ctrl Shift I hoặc F12 (tùy thuộc trình duyệt) để vào phần Công cụ dành cho nhà phát triển:

Google Dev Tool - Check Service Worker

Theo như hình trên thì bạn có thể thấy là Service Worker đã được kích hoạt (activated) và đang chạy (running).

JavaScript Service Worker - fetch

Đoạn code xử lý lệnh fetch:

/*
 * The fetch handler serves responses for same-origin resources from a cache.
 * If no response is found, it populates the runtime cache with the response
 * from the network before returning it to the page.
 */
self.addEventListener("fetch", (event) => {
  // Skip cross-origin requests, like those for Google Analytics.
  if (event.request.url.startsWith(self.location.origin)) {
    event.respondWith(
      caches.match(event.request).then((cachedResponse) => {
        if (cachedResponse) {
          return cachedResponse;
        }

        return caches.open(RUNTIME).then((cache) => {
          return fetch(event.request).then((response) => {
            // Put a copy of the response in the runtime cache.
            return cache.put(event.request, response.clone()).then(() => {
              return response;
            });
          });
        });
      })
    );
  }
});

Đoạn code trên có thể mô tả thành lời như sau:

Bước 1: Khi có request từ phía client (trình duyệt): Nếu url không thuộc cùng origin (không cùng trang web) thì bỏ qua.

Bước 2: Ngược lại, kiểm tra trong bộ nhớ đệm cache xem đã có response tương ứng với request chưa. Nếu tồn tại, thì trả về response đó cho trình duyệt.

Bước 3: Ngược lại, gửi request đó lên server, rồi lấy response trả về.

Bước 4: Clone response đó và lưu vào RUNTIME cache để phục vụ cho lần request tiếp theo, rồi sau đó trả về response đó cho trình duyệt.

Kết luận

Trên đây là những kiến thức cơ bản về JavaScript Service Worker mà mình tìm hiểu được, và còn rất nhiều vấn đề liên quan khác cần nghiên cứu thêm.

Đến đây có thể đã giải đáp được cho vấn đề đưa ra ở đầu bài viết: có 2 thứ cần thiết để web app có thể chạy được là: resources (html, js, css, ảnh,...) và định tuyến.

  • Dùng listener installactivate để lưu resources vào bộ nhớ đệm cache.
  • Dùng listener fetch để định tuyến.

Nếu bạn có thắc mắc gì, vui lòng để lại bình luận để mọi người có thể cùng trao đổi. Mình không hứa là sẽ giải đáp ngay cho bạn. Nhưng mình hứa sẽ tìm hiểu để giải quyết cùng bạn.

Xin chào và hẹn gặp lại bạn trong bài viết tiếp theo, thân ái!

Tham khảo

★ Nếu bạn thấy bài viết này hay thì hãy theo dõi mình trên Facebook để nhận được thông báo khi có bài viết mới nhất nhé:

JavaScript Web Worker - chạy JavaScript ở background
3x7 = 21 thủ thuật JavaScript hữu ích
Chia sẻ:

Bình luận