Các phương pháp hay nhất để sử dụng IndexedDB

Tìm hiểu các phương pháp hay nhất để đồng bộ hoá trạng thái ứng dụng giữa IndexedDB một thư viện quản lý trạng thái phổ biến.

Khi người dùng tải một trang web hoặc ứng dụng lần đầu tiên, thường họ sẽ mất một lượng công việc tương đối lớn để tạo trạng thái ban đầu của ứng dụng dùng để hiển thị giao diện người dùng. Ví dụ: đôi khi ứng dụng cần xác thực phía máy khách của người dùng, sau đó thực hiện một số yêu cầu API trước khi có tất cả dữ liệu cần thiết để hiển thị trên trang.

Việc lưu trữ trạng thái ứng dụng trong IndexedDB có thể là một cách tuyệt vời để tăng tốc thời gian tải cho các lượt truy cập lặp lại. Sau đó, ứng dụng có thể đồng bộ hoá với bất kỳ dịch vụ API nào trong nền và cập nhật giao diện người dùng bằng dữ liệu mới từng phần, sử dụng chiến lược đã lỗi thời trong khi xác thực lại.

Một cách hữu hiệu khác cho IndexedDB là để lưu trữ nội dung do người dùng tạo, dưới dạng một kho lưu trữ tạm thời trước khi được tải lên máy chủ hoặc dưới dạng bộ nhớ đệm phía máy khách của dữ liệu từ xa - hoặc tất nhiên là cả hai.

Tuy nhiên, khi sử dụng IndexedDB, có nhiều điểm quan trọng cần xem xét mà có thể không rõ ràng ngay lập tức với các nhà phát triển mới sử dụng API. Bài viết này giải đáp những câu hỏi thường gặp và thảo luận một số điều quan trọng nhất cần lưu ý khi duy trì dữ liệu trong IndexedDB.

Giúp ứng dụng của bạn dễ dự đoán

Rất nhiều điểm phức tạp xung quanh IndexedDB xuất phát từ việc có quá nhiều yếu tố mà bạn (nhà phát triển) không có quyền kiểm soát. Phần này khám phá nhiều vấn đề mà bạn phải lưu ý khi làm việc với IndexedDB.

Không phải mọi thứ đều có thể được lưu trữ trong IndexedDB trên mọi nền tảng

Nếu đang lưu trữ các tệp có kích thước lớn, do người dùng tạo, chẳng hạn như hình ảnh hoặc video, thì bạn có thể cố gắng lưu trữ các tệp đó dưới dạng đối tượng File hoặc Blob. Cách này sẽ hoạt động trên một số nền tảng nhưng không thành công trên những nền tảng khác. Cụ thể, Safari trên iOS, không thể lưu trữ Blob trong IndexedDB.

May mắn là không quá khó để chuyển đổi Blob thành ArrayBuffer và ngược lại. Việc lưu trữ ArrayBuffer trong IndexedDB được hỗ trợ rất tốt.

Tuy nhiên, hãy nhớ rằng Blob có loại MIME còn ArrayBuffer thì không. Bạn sẽ cần lưu trữ loại này cùng với vùng đệm để thực hiện chuyển đổi chính xác.

Để chuyển đổi ArrayBuffer thành Blob, bạn chỉ cần sử dụng hàm khởi tạo Blob.

function arrayBufferToBlob(buffer, type) {
  return new Blob([buffer], { type: type });
}

Hướng còn lại có liên quan nhiều hơn một chút và là một quá trình không đồng bộ. Bạn có thể sử dụng đối tượng FileReader để đọc blob dưới dạng ArrayBuffer. Khi quá trình đọc hoàn tất, sự kiện loadend được kích hoạt trên trình đọc. Bạn có thể gói quy trình này trong một Promise như dưới đây:

function blobToArrayBuffer(blob) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.addEventListener('loadend', () => {
      resolve(reader.result);
    });
    reader.addEventListener('error', reject);
    reader.readAsArrayBuffer(blob);
  });
}

Có thể không ghi được vào bộ nhớ

Lỗi khi ghi vào IndexedDB có thể xảy ra vì nhiều lý do và trong một số trường hợp, những lý do này nằm ngoài tầm kiểm soát của bạn với tư cách là nhà phát triển. Ví dụ: một số trình duyệt hiện không cho phép ghi vào IndexedDB khi ở chế độ duyệt web riêng tư. Cũng có khả năng người dùng đang sử dụng thiết bị gần hết dung lượng ổ đĩa và trình duyệt sẽ hạn chế bạn lưu trữ mọi thứ.

Do đó, điều tối quan trọng là bạn phải luôn triển khai cách xử lý lỗi đúng cách trong mãIndexedDB của mình. Điều này cũng có nghĩa là bạn nên giữ trạng thái ứng dụng trong bộ nhớ (ngoài việc lưu trữ ứng dụng) để giao diện người dùng không bị hỏng khi chạy ở chế độ duyệt web riêng tư hoặc khi không có không gian lưu trữ (ngay cả khi một số tính năng khác của ứng dụng cần đến bộ nhớ sẽ không hoạt động).

Bạn có thể phát hiện lỗi trong hoạt động IndexedDB bằng cách thêm trình xử lý sự kiện cho sự kiện error bất cứ khi nào bạn tạo một đối tượng IDBDatabase, IDBTransaction hoặc IDBRequest.

const request = db.open('example-db', 1);
request.addEventListener('error', (event) => {
  console.log('Request error:', request.error);
};

Người dùng có thể đã sửa đổi hoặc xoá dữ liệu được lưu trữ

Không giống như cơ sở dữ liệu phía máy chủ (nơi bạn có thể hạn chế quyền truy cập trái phép), cơ sở dữ liệu phía máy khách cho phép các tiện ích của trình duyệt và công cụ cho nhà phát triển truy cập, đồng thời người dùng có thể xoá các cơ sở dữ liệu này.

Mặc dù người dùng có thể ít khi sửa đổi dữ liệu được lưu trữ cục bộ của họ, nhưng việc người dùng xoá dữ liệu đó là khá phổ biến. Quan trọng là ứng dụng của bạn có thể xử lý cả hai trường hợp này mà không gặp lỗi.

Dữ liệu được lưu trữ có thể đã cũ

Tương tự như phần trước, ngay cả khi người dùng chưa tự sửa đổi dữ liệu, thì cũng có khả năng dữ liệu họ có trong bộ nhớ đã được viết bằng một phiên bản mã cũ, có thể là một phiên bản có lỗi.

IndexedDB tích hợp sẵn tính năng hỗ trợ cho các phiên bản giản đồ và nâng cấp thông qua phương thức IDBOpenDBRequest.onupgradeneeded(); tuy nhiên, bạn vẫn cần viết mã nâng cấp theo cách có thể xử lý người dùng đến từ phiên bản trước (bao gồm cả phiên bản có lỗi).

Việc kiểm thử đơn vị có thể rất hữu ích trong trường hợp này, vì thường không thể kiểm thử thủ công tất cả các đường dẫn và trường hợp nâng cấp có thể có.

Duy trì hiệu suất của ứng dụng

Một trong những tính năng chính của IndexedDB là API không đồng bộ, nhưng đừng đánh lừa rằng bạn không cần lo lắng về hiệu suất khi sử dụng. Có một số trường hợp việc sử dụng không đúng cách vẫn có thể chặn luồng chính, điều này có thể dẫn đến hiện tượng giật và không phản hồi.

Theo nguyên tắc chung, việc đọc và ghi vào IndexedDB không được lớn hơn mức cần thiết đối với dữ liệu đang được truy cập.

Mặc dù IndexedDB có thể lưu trữ các đối tượng lớn, được lồng dưới dạng một bản ghi (và việc làm như vậy khá thuận tiện từ góc độ nhà phát triển), nhưng bạn nên tránh phương pháp này. Lý do là khi IndexedDB lưu trữ một đối tượng, trước tiên, IndexedDB phải tạo một bản sao có cấu trúc của đối tượng đó và quá trình sao chép có cấu trúc diễn ra trên luồng chính. Đối tượng càng lớn, thời gian chặn sẽ càng dài.

Điều này đặt ra một số thách thức khi lên kế hoạch duy trì trạng thái ứng dụng cho IndexedDB, vì hầu hết các thư viện quản lý trạng thái phổ biến (như Redux) đều hoạt động bằng cách quản lý toàn bộ cây trạng thái dưới dạng một đối tượng JavaScript duy nhất.

Mặc dù việc quản lý trạng thái theo cách này có nhiều lợi ích (ví dụ: giúp mã của bạn dễ dàng xử lý và gỡ lỗi), đồng thời việc chỉ cần lưu trữ toàn bộ cây trạng thái dưới dạng một bản ghi trong IndexedDB có thể rất hấp dẫn và thuận tiện, nhưng thực hiện việc này sau mỗi lần thay đổi (ngay cả khi bị điều tiết/giải quyết) sẽ dẫn đến việc chặn luồng chính không cần thiết, đồng thời làm tăng khả năng ghi lỗi thẻ và thậm chí trong một số trường hợp sẽ gây ra sự cố hoặc khiến trình duyệt gặp sự cố.

Thay vì lưu trữ toàn bộ cây trạng thái trong một bản ghi duy nhất, bạn nên chia nhỏ thành các bản ghi riêng lẻ và chỉ cập nhật những bản ghi thực sự thay đổi.

Điều này cũng xảy ra nếu bạn lưu trữ các mục có kích thước lớn như hình ảnh, nhạc hoặc video trong IndexedDB. Lưu trữ từng mục bằng khoá riêng thay vì bên trong một đối tượng lớn hơn để bạn có thể truy xuất dữ liệu có cấu trúc mà không phải trả chi phí truy xuất tệp nhị phân.

Giống như với hầu hết các phương pháp hay nhất, đây không phải là quy tắc tất cả. Trong trường hợp không thể chia nhỏ một đối tượng trạng thái và chỉ ghi tập hợp thay đổi tối thiểu, chia dữ liệu thành các cây con và chỉ viết các cây đó vẫn nên luôn ghi toàn bộ cây trạng thái. Có cải thiện nhỏ thì tốt hơn là không có cải thiện nào cả.

Cuối cùng, bạn phải luôn đo lường tác động về hiệu suất của mã bạn viết. Mặc dù đúng là các lượt ghi nhỏ vào IndexedDB sẽ hoạt động tốt hơn so với các lần ghi lớn, nhưng điều này chỉ quan trọng nếu việc ghi vào IndexedDB rằng ứng dụng của bạn đang thực sự dẫn đến các tác vụ dài chặn luồng chính và làm giảm trải nghiệm người dùng. Điều quan trọng là cần đo lường để bạn hiểu được mình đang tối ưu hoá điều gì.

Kết luận

Nhà phát triển có thể tận dụng cơ chế lưu trữ ứng dụng như IndexedDB để cải thiện trải nghiệm người dùng của ứng dụng bằng cách không chỉ duy trì trạng thái qua các phiên mà còn bằng cách giảm thời gian tải trạng thái ban đầu trong các lượt truy cập lặp lại.

Mặc dù việc sử dụng IndexedDB đúng cách có thể cải thiện đáng kể trải nghiệm người dùng, nhưng việc sử dụng mã này không đúng cách hoặc không xử lý được các trường hợp lỗi có thể khiến ứng dụng bị hỏng và người dùng không hài lòng.

Vì việc lưu trữ ứng dụng liên quan đến nhiều yếu tố nằm ngoài tầm kiểm soát của bạn, nên điều tối quan trọng là mã của bạn được kiểm thử kỹ và xử lý lỗi đúng cách, ngay cả những lỗi ban đầu dường như khó xảy ra.