Dữ liệu ngoại tuyến

Để xây dựng trải nghiệm ngoại tuyến vững chắc, PWA của bạn cần quản lý bộ nhớ. Trong chương về bộ nhớ đệm, bạn đã tìm hiểu 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 tôi sẽ hướng dẫn bạn cách quản lý dữ liệu ngoại tuyến, bao gồm cả việc lưu trữ cố định dữ liệu, các giới hạn và những công cụ hiện có.

Dung lượng lưu trữ

Bộ nhớ không chỉ bao gồm các tệp và tài sản mà còn có thể bao gồm cả các loại dữ liệu khác. Trên tất cả trình duyệt hỗ trợ PWA, có 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ị, sử dụng bộ nhớ cục bộ hoặc bộ nhớ phiên. Không dùng được trong ngữ cảnh service worker. API này có tính đồng bộ nên bạn không nên sử dụng để lưu trữ dữ liệu phức tạp.
  • Dung lượng lưu trữ vào 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ớ của thiết bị bằng API Trình quản lý bộ nhớ 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ể được truy cập từ luồng chính, trình thực thi 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 ổn định khi mạng không ổn định hoặc không tồn tại. Nhưng khi nào thì bạn nên sử dụng?

Sử dụng Cache Storage API cho tài nguyên mạng, những thứ bạn có thể truy cập bằng cách yêu cầu chúng qua 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. Trong đó bao gồm những dữ liệu cần tìm kiếm hoặc kết hợp được theo cách giống với NoSQL, hoặc những dữ liệu khác như dữ liệu riêng của người dùng mà 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 phải thực hiện 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, một trình bao bọc Promise nhỏ cho IndexedDB. Bạn không bắt buộc phải sử dụng thư viện trợ giúp IndexedDB, nhưng nếu muốn sử dụng cú pháp Promise, bạn có thể chọn thư viện idb.

Ví dụ sau đây tạo một cơ sở dữ liệu để lưu giữ 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. 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 tạo phiên bản, bạn cần tăng số phiên bản mỗi khi thực hiện thay đổi đối vớ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 được đặt là 1.
  2. Đối tượng khởi tạo chứa lệnh gọi lại upgrade() được truyền đến openDB(). Hàm callback đượ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 được nâng cấp lên phiên bản mới. Hàm này là nơi duy nhất có thể thực hiện các hành động. Các thao tác có thể bao gồm tạo kho lưu trữ đối tượng mới (cấu trúc mà IndexedDB sử dụng để sắp xếp dữ liệu) hoặc lập chỉ mục (mà bạn muốn tìm kiếm). Đây cũng là nơi quá trình di chuyển dữ liệu sẽ diễn ra. Thông thường, hàm upgrade() chứa câu lệnh switch không có câu lệnh break để cho phép thực hiện từng bước 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 sẽ tạo một cửa hàng đối tượng bên trong cơ sở dữ liệu cookbook có tên là recipes, trong đó thuộc tính id được đặt làm khoá chỉ mục của cửa hàng 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 cùng xem kho lưu trữ đối tượng vừa được tạo. Sau khi thêm công thức vào kho lưu trữ đối tượng và mở Công cụ cho nhà phát triển trên trình duyệt dựa trên Chromium hoặc Trình kiểm tra web trên Safari, đây là những gì bạn sẽ thấy:

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

Đang thêm dữ liệu

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

  1. Bắt đầu giao dịch với mode được đặt thành readwrite.
  2. Tải kho lưu trữ đối tượng để bạn thêm dữ liệu.
  3. Gọi add() bằng dữ liệu 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ặp khoá/giá trị) và thêm dữ liệu đó vào kho lưu trữ đối tượng. Phải sao chép được từ điển bằng tính năng 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 nên gọi phương thức put().

Giao dịch có lời hứa done sẽ xử lý khi giao dịch hoàn tất thành công hoặc bị từ chối do có 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 thấy 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 hoạt động 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 nấu ăn này sẽ nằm trong cơ sở dữ liệu của các công thức khác. Mã nhận dạng được tự động đặt và tăng dần theo indexDB. Nếu chạy mã này hai lần, bạn sẽ có hai mục nhập cookie giống hệt nhau.

Truy xuất dữ liệu

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

  1. Bắt đầu giao dịch và chỉ định kho lưu trữ đối tượng hoặc cửa hàng, cũng như loại giao dịch (không bắt buộc).
  2. Gọi objectStore() từ giao dịch đó. Hãy đảm bảo bạn chỉ định tên kho lưu trữ đối tượng.
  3. Gọi get() bằng khoá bạn muốn lấy. Theo mặc định, cửa hàng sử dụng khoá của mình 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 đóng vai trò đặc biệt quan trọng trong việc lưu trữ và truyền trực tuyến phản hồi của mạng một cách chính xác.

Dung lượng lưu trữ được dùng chung giữa tất cả các tuỳ chọn bộ nhớ, bao gồm Bộ nhớ đệm, IndexedDB, Bộ nhớ web và thậm chí cả tệp trình chạy dịch vụ cũng như các phần phụ thuộc của tệp đó. Tuy nhiên, mức dung lượng lưu trữ có sẵn sẽ khác nhau tuỳ theo trình duyệt. Bạn không có khả năng hết; các trang web có thể lưu trữ megabyte và thậm chí gigabyte dữ liệu trên một số trình duyệt. Ví dụ: Chrome cho phép trình duyệt sử dụng đến 80% tổng dung lượng ổ đĩa và một nguồn gốc có thể sử dụng tới 60% toàn bộ dung lượng ổ đĩa. Đối với các trình duyệt hỗ trợ Storage API (API Bộ nhớ), bạn có thể biết lượng bộ nhớ còn trống cho ứng dụng, hạn mức ứng dụng và mức sử dụng ứ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 tỷ lệ phần trăm đã sử dụng và các 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à rất dễ khiến người dùng nhầm lẫn.

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 Công cụ của Chromium cho nhà phát triển, bạn có thể xem hạn mức của trang web và lượng bộ nhớ được sử dụng được chia nhỏ theo những gì đang sử dụng trang bằng cách mở phần Bộ nhớ trong thẻ Ứng dụng.

Công cụ của Chrome cho nhà phát triển trong ứng dụng, phần 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 bộ nhớ và mức sử dụng đối với 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ớ ổn định trên các nền tảng tương thích để tránh tự động loại bỏ dữ liệu sau khi không hoạt động hoặc do áp lực về bộ nhớ. Nếu đã được cấp quyền, 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 chịu trách nhiệm và họ có thể xoá bộ nhớ bất kỳ lúc nào, ngay cả khi trình duyệt đã cấp bộ nhớ ổn định.

Để yêu cầu bộ nhớ ổn định, hãy gọi StorageManager.persist(). Như trước đó, giao diện StorageManager là quyền truy cập 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ớ lâu dài đã được cấp trong nguồn hiện tại hay chưa bằng cách gọi StorageManager.persisted(). Firefox yêu cầu người dùng cho phép 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 bền vững dựa trên 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 cho họ bộ nhớ ổn định.

Mozilla Firefox yêu cầu người dùng cấp quyền lưu trữ cố định.

Hỗ trợ Trình duyệt API

Bộ nhớ trên web

Hỗ trợ trình duyệt

  • 4
  • 12
  • 3,5
  • 4

Nguồn

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

Hỗ trợ trình duyệt

  • 86
  • 86
  • 111
  • 15,2

Nguồn

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

Hỗ trợ trình duyệt

  • 55
  • 79
  • 57
  • 15,2

Nguồn

Tài nguyên