Dữ liệu ngoại tuyến

Để tạo trải nghiệm ngoại tuyến ổn định, PWA của bạn cần có tính năng quản lý bộ nhớ. Trong chương về lưu vào bộ nhớ đệm, bạn đã biết rằng bộ nhớ đệm là một lựa chọn để lưu dữ liệu trên thiết bị. Trong chương này, chúng ta sẽ tìm hiểu cách quản lý dữ liệu ngoại tuyến, bao gồm cả tính liên tục của dữ liệu, các giới hạn và những công cụ hiện có.

Bộ nhớ

Bộ nhớ không chỉ lưu trữ các tệp và tài sản mà còn có thể lưu trữ các loại dữ liệu khác. Trên tất cả các trình duyệt hỗ trợ PWA, bạn có thể sử dụng các API sau đây để lưu trữ trên thiết bị:

  • IndexedDB: Một lựa chọn lưu trữ đối tượng NoSQL cho dữ liệu có cấu trúc và blob (dữ liệu nhị phân).
  • WebStorage: Một cách để lưu trữ các cặp chuỗi khoá/giá trị, bằng cách sử dụng bộ nhớ cục bộ hoặc bộ nhớ phiên. Tính năng này không dùng được trong bối cảnh trình chạy dịch vụ. API này là đồng bộ nên không được khuyến khích dùng cho bộ nhớ dữ liệu phức tạp.
  • Bộ nhớ đệm: Như đã đề cập trong mô-đun Lưu vào bộ nhớ đệm.

Bạn có thể quản lý tất cả bộ nhớ thiết bị bằng Storage Manager API trên các nền tảng được hỗ trợ. Cache Storage API và IndexedDB cung cấp quyền truy cập không đồng bộ vào bộ nhớ liên tục cho PWA và có thể truy cập từ luồng chính, trình chạy dịch vụ web và trình chạy dịch vụ. Cả hai đều đóng vai trò thiết yếu trong việc giúp PWA hoạt động một cách đáng tin cậy khi mạng không ổn định hoặc không tồn tại. Nhưng bạn nên sử dụng từng loại khi nào?

Sử dụng Cache Storage API cho các tài nguyên mạng, những nội dung mà bạn sẽ truy cập bằng cách yêu cầu thông qua một URL, chẳng hạn như HTML, CSS, JavaScript, hình ảnh, video và âm thanh.

Sử dụng IndexedDB để lưu trữ dữ liệu có cấu trúc. Điều này bao gồm dữ liệu cần được tìm kiếm hoặc kết hợp theo cách tương tự như NoSQL, hoặc dữ liệu khác như dữ liệu dành riêng cho người dùng không nhất thiết phải khớp với yêu cầu URL. Xin lưu ý rằng IndexedDB không được thiết kế để tìm kiếm toàn bộ văn bản.

IndexedDB

Để sử dụng IndexedDB, trước tiên, hãy mở một cơ sở dữ liệu. Thao tác này sẽ tạo một cơ sở dữ liệu mới nếu chưa có cơ sở dữ liệu nào. IndexedDB là một API không đồng bộ, nhưng API này lấy một lệnh gọi lại thay vì trả về một Promise. Ví dụ sau đây sử dụng thư viện idb của Jake Archibald. Đây là một trình bao bọc Promise nhỏ cho IndexedDB. Bạn không bắt buộc phải sử dụng các thư viện trợ giúp để dùng IndexedDB, nhưng nếu muốn sử dụng cú pháp Promise, bạn có thể dùng thư viện idb.

Ví dụ sau đây tạo một cơ sở dữ liệu để lưu trữ các công thức nấu ăn.

Tạo và mở cơ sở dữ liệu

Cách mở cơ sở dữ liệu:

  1. Sử dụng hàm openDB để tạo một cơ sở dữ liệu IndexedDB mới có tên là cookbook. Vì cơ sở dữ liệu IndexedDB được gắn phiên bản, nên bạn cần tăng số phiên bản mỗi khi thay đổi cấu trúc cơ sở dữ liệu. Tham số thứ hai là phiên bản cơ sở dữ liệu. Trong ví dụ này, giá trị được đặt thành 1.
  2. Một đối tượng khởi tạo chứa lệnh gọi lại upgrade() được truyền đến openDB(). Hàm gọi lại được gọi khi cơ sở dữ liệu được cài đặt lần đầu tiên hoặc khi cơ sở dữ liệu nâng cấp lên phiên bản mới. Hàm này là nơi duy nhất có thể xảy ra các thao tác. Các thao tác có thể bao gồm việc tạo các kho đối tượng mới (cấu trúc mà IndexedDB dùng để sắp xếp dữ liệu) hoặc chỉ mục (mà bạn muốn tìm kiếm). Đây cũng là nơi diễn ra quá trình di chuyển dữ liệu. Thông thường, hàm upgrade() chứa một câu lệnh switch mà không có câu lệnh break để cho phép mỗi bước diễn ra theo thứ tự, dựa trên phiên bản cũ của cơ sở dữ liệu.
import { openDB } from 'idb';

async function createDB() {
  // Using https://github.com/jakearchibald/idb
  const db = await openDB('cookbook', 1, {
    upgrade(db, oldVersion, newVersion, transaction) {
      // Switch over the oldVersion, *without breaks*, to allow the database to be incrementally upgraded.
    switch(oldVersion) {
     case 0:
       // Placeholder to execute when database is created (oldVersion is 0)
     case 1:
       // Create a store of objects
       const store = db.createObjectStore('recipes', {
         // The `id` property of the object will be the key, and be incremented automatically
           autoIncrement: true,
           keyPath: 'id'
       });
       // Create an index called `name` based on the `type` property of objects in the store
       store.createIndex('type', 'type');
     }
   }
  });
}

Ví dụ này tạo một kho đối tượng bên trong cơ sở dữ liệu cookbook có tên là recipes, với thuộc tính id được đặt làm khoá chỉ mục của kho và tạo một chỉ mục khác có tên là type, dựa trên thuộc tính type.

Hãy xem kho lưu trữ đối tượng vừa được tạo. Sau khi thêm các công thức vào kho đối tượng và mở Công cụ cho nhà phát triển trên các trình duyệt dựa trên Chromium hoặc Trình kiểm tra web trên Safari, bạn sẽ thấy những nội dung sau:

Safari và Chrome hiển thị nội dung IndexedDB.

Thêm dữ liệu

IndexedDB sử dụng các giao dịch. Các giao dịch nhóm các hành động lại với nhau, vì vậy, các hành động này sẽ diễn ra dưới dạng một đơn vị. Chúng giúp đảm bảo rằng cơ sở dữ liệu luôn ở trạng thái nhất quán. Chúng cũng rất quan trọng nếu bạn đang chạy nhiều bản sao của ứng dụng, nhằm ngăn việc ghi đồng thời vào cùng một dữ liệu. Cách thêm dữ liệu:

  1. Bắt đầu một giao dịch với mode được đặt thành readwrite.
  2. Lấy kho lưu trữ đối tượng, nơi bạn sẽ thêm dữ liệu.
  3. Gọi add() bằng dữ liệu mà bạn đang lưu. Phương thức này nhận dữ liệu ở dạng từ điển (dưới dạng các cặp khoá/giá trị) và thêm dữ liệu đó vào kho đối tượng. Từ điển phải có thể sao chép bằng cách dùng Structured Cloning (Sao chép có cấu trúc). Nếu muốn cập nhật một đối tượng hiện có, bạn sẽ gọi phương thức put().

Các giao dịch có một done promise sẽ phân giải khi giao dịch hoàn tất thành công hoặc từ chối bằng một lỗi giao dịch.

Như tài liệu về thư viện IDB giải thích, nếu bạn đang ghi vào cơ sở dữ liệu, thì tx.done là tín hiệu cho biết mọi thứ đã được cam kết thành công vào cơ sở dữ liệu. Tuy nhiên, bạn nên chờ các thao tác riêng lẻ để có thể thấy mọi lỗi khiến giao dịch không thành công.

// Using https://github.com/jakearchibald/idb
async function addData() {
  const cookies = {
      name: "Chocolate chips cookies",
      type: "dessert",
        cook_time_minutes: 25
  };
  const tx = await db.transaction('recipes', 'readwrite');
  const store = tx.objectStore('recipes');
  store.add(cookies);
  await tx.done;
}

Sau khi bạn thêm bánh quy, công thức sẽ nằm trong cơ sở dữ liệu cùng với các công thức khác. Mã nhận dạng được indexedDB tự động đặt và tăng. Nếu chạy đoạn mã này hai lần, bạn sẽ có hai mục nhập cookie giống hệt nhau.

Đang truy xuất dữ liệu

Sau đây là cách bạn lấy dữ liệu từ IndexedDB:

  1. Bắt đầu một giao dịch và chỉ định (các) kho đối tượng, cũng như loại giao dịch (không bắt buộc).
  2. Gọi objectStore() từ giao dịch đó. Đảm bảo rằng bạn chỉ định tên kho lưu trữ đối tượng.
  3. Gọi get() bằng khoá mà bạn muốn nhận. Theo mặc định, kho lưu trữ sẽ dùng khoá làm chỉ mục.
// Using https://github.com/jakearchibald/idb
async function getData() {
  const tx = await db.transaction('recipes', 'readonly')
  const store = tx.objectStore('recipes');
// Because in our case the `id` is the key, we would
// have to know in advance the value of the id to
// retrieve the record
  const value = await store.get([id]);
}

Trình quản lý bộ nhớ

Việc biết cách quản lý bộ nhớ của PWA là đặc biệt quan trọng để lưu trữ và truyền trực tuyến các phản hồi mạng một cách chính xác.

Dung lượng lưu trữ được chia sẻ giữa tất cả các lựa chọn lưu trữ, bao gồm cả Bộ nhớ đệm, IndexedDB, Bộ nhớ trên web và thậm chí cả tệp của worker dịch vụ cũng như các phần phụ thuộc của tệp đó. Tuy nhiên, dung lượng bộ nhớ có sẵn sẽ khác nhau tuỳ theo trình duyệt. Bạn khó có thể hết bộ nhớ này; các trang web có thể lưu trữ dữ liệu có dung lượng lên đến hàng megabyte và thậm chí hàng gigabyte trên một số trình duyệt. Ví dụ: Chrome cho phép trình duyệt sử dụng tối đa 80% tổng dung lượng ổ đĩa và một nguồn riêng lẻ có thể sử dụng tối đa 60% tổng dung lượng ổ đĩa. Đối với những trình duyệt hỗ trợ Storage API, bạn có thể biết dung lượng lưu trữ còn lại cho ứng dụng, hạn mức và mức sử dụng của ứng dụng. Ví dụ sau đây sử dụng Storage API để lấy hạn mức và mức sử dụng ước tính, sau đó tính toán tỷ lệ phần trăm đã sử dụng và số byte còn lại. Xin lưu ý rằng navigator.storage trả về một thực thể của StorageManager. Có một giao diện Storage riêng biệt và bạn có thể dễ dàng nhầm lẫn giữa hai giao diện này.

if (navigator.storage && navigator.storage.estimate) {
  const quota = await navigator.storage.estimate();
  // quota.usage -> Number of bytes used.
  // quota.quota -> Maximum number of bytes available.
  const percentageUsed = (quota.usage / quota.quota) * 100;
  console.log(`You've used ${percentageUsed}% of the available storage.`);
  const remaining = quota.quota - quota.usage;
  console.log(`You can write up to ${remaining} more bytes.`);
}

Trong Chromium DevTools, bạn có thể xem hạn mức của trang web và mức bộ nhớ đã sử dụng (được chia nhỏ theo nội dung đang sử dụng bộ nhớ) bằng cách mở phần Bộ nhớ trong thẻ Ứng dụng.

Chrome DevTools trong phần Ứng dụng, Xoá bộ nhớ

Firefox và Safari không cung cấp màn hình tóm tắt để xem tất cả hạn mức lưu trữ và mức sử dụng cho nguồn gốc hiện tại.

Khả năng lưu trữ cố định dữ liệu

Bạn có thể yêu cầu trình duyệt cung cấp bộ nhớ liên tục trên các nền tảng tương thích để tránh việc tự động loại bỏ dữ liệu sau khi không hoạt động hoặc khi bộ nhớ bị đầy. Nếu được cấp, trình duyệt sẽ không bao giờ loại bỏ dữ liệu khỏi bộ nhớ. Biện pháp bảo vệ này bao gồm việc đăng ký trình chạy dịch vụ, cơ sở dữ liệu IndexedDB và các tệp trong bộ nhớ đệm. Xin lưu ý rằng người dùng luôn có quyền kiểm soát và họ có thể xoá bộ nhớ bất cứ lúc nào, ngay cả khi trình duyệt đã cấp bộ nhớ liên tục.

Để yêu cầu bộ nhớ ổn định, hãy gọi StorageManager.persist(). Như trước đây, bạn có thể truy cập vào giao diện StorageManager thông qua thuộc tính navigator.storage.

async function persistData() {
  if (navigator.storage && navigator.storage.persist) {
    const result = await navigator.storage.persist();
    console.log(`Data persisted: ${result}`);
}

Bạn cũng có thể kiểm tra xem bộ nhớ liên tục đã được cấp trong nguồn gốc hiện tại hay chưa bằng cách gọi StorageManager.persisted(). Firefox yêu cầu người dùng cấp quyền sử dụng bộ nhớ liên tục. Các trình duyệt dựa trên Chromium cho phép hoặc từ chối tính năng duy trì dựa trên một phương pháp phỏng đoán để xác định tầm quan trọng của nội dung đối với người dùng. Ví dụ: một tiêu chí đối với Google Chrome là cài đặt PWA. Nếu người dùng đã cài đặt một biểu tượng cho PWA trong hệ điều hành, thì trình duyệt có thể cấp bộ nhớ liên tục.

Mozilla Firefox yêu cầu người dùng cấp quyền duy trì bộ nhớ.

Hỗ trợ API cho trình duyệt

Bộ nhớ trên web

Browser Support

  • Chrome: 4.
  • Edge: 12.
  • Firefox: 3.5.
  • Safari: 4.

Source

Quyền truy cập vào hệ thống tệp

Browser Support

  • Chrome: 86.
  • Edge: 86.
  • Firefox: 111.
  • Safari: 15.2.

Source

Trình quản lý bộ nhớ

Browser Support

  • Chrome: 55.
  • Edge: 79.
  • Firefox: 57.
  • Safari: 15.2.

Source

Tài nguyên