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 có một lượng lớn công việc liên quan đến việc xây dựng 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 rồi 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à cách hay để 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 ở chế độ nền và cập nhật từng phần dữ liệu mới cho giao diện người dùng, sử dụng chiến lược đã lỗi thời trong khi xác thực lại.

Một cách hay khác để dùng IndexedDB là lưu trữ nội dung do người dùng tạo, ở dạng cửa hàng tạm thời trước khi 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ều quan trọng cần cân nhắc có thể chưa 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 các 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 sự phức tạp xung quanh IndexedDB xuất phát từ thực tế là có rất 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 tìm hiểu 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 nội dung đều có thể được lưu trữ trong IndexedDB trên tất cả các 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ể thử lưu trữ những 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 hoạt động trên các nền tảng khác. Cụ thể, Safari trên iOS không thể lưu trữ các Blob trong IndexedDB.

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

Tuy nhiên, hãy nhớ rằng Blob có loại MIME trong khi 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 việc chuyển đổi một cách 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 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 bạn đọc xong, một sự kiện loadend sẽ đượ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ư sau:

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);
  });
}

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 với tư cách là nhà phát triển của bạ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 một thiết bị sắp hết dung lượng ổ đĩa và trình duyệt sẽ hạn chế bạn lưu trữ bất kỳ nội dung nào.

Do đó, điều tối quan trọng là bạn phải luôn triển khai cách xử lý lỗi thích hợp trong mã IndexedDB. Điều này cũng có nghĩa là bạn nên giữ trạng thái của ứng dụng trong bộ nhớ (ngoài việc lưu trữ) để 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 có bộ nhớ sẽ không hoạt động).

Bạn có thể phát hiện lỗi trong các thao tác 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 đố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);
};

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

Không giống như các 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 có thể truy cập vào các tiện ích của trình duyệt và công cụ cho nhà phát triển. Đồ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ể không sửa đổi dữ liệu được lưu trữ cục bộ của họ, nhưng việc xoá dữ liệu đó là khá phổ biến. Điều 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ể đã lỗi thời

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ó thể dữ liệu họ có trong bộ nhớ được viết bởi một phiên bản mã cũ của bạn, 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).

Phương thức kiểm thử đơn vị có thể rất hữu ích trong trường hợp này, vì thường bạn không thể kiểm thử thủ công tất cả các trường hợp và đường dẫn 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 để điều đó lừa bạn nghĩ rằng mình không cần lo lắng về hiệu suất khi sử dụng API đó. Có một số trường hợp mà việc sử dụng không đúng cách vẫn có thể chặn luồng chính, dẫn đến hiện tượng giật và không phản hồi.

Theo quy tắc chung, số lượt đọc và ghi vào IndexedDB không được lớn hơn kích thước yêu cầu đối với dữ liệu đang được truy cập.

Mặc dù IndexedDB giúp lưu trữ các đối tượng lớn, lồng nhau dưới dạng một bản ghi duy nhất (và việc này được công nhận là 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à vì khi IndexedDB lưu trữ một đối tượng, trước tiên, đối tượng này 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ì thời gian chặn sẽ càng dài.

Điều này gây 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 bạn dễ dàng giải thích và gỡ lỗi mã), đồng thời việc chỉ lưu trữ toàn bộ cây trạng thái dưới dạng một bản ghi duy nhất trong IndexedDB có thể hấp dẫn và thuận tiện, nhưng việc thực hiện việc này sau mỗi thay đổi (ngay cả khi được điều tiết/bị loại bỏ) sẽ dẫn đến việc chặn luồng chính không cần thiết, điều này sẽ làm tăng khả năng lỗi ghi và thậm chí trình duyệt sẽ gặp sự cố trong một số trường hợp.

Thay vì lưu trữ toàn bộ cây trạng thái trong một bản ghi, 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 đúng nếu bạn lưu trữ các mụ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 đố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ư hầu hết các phương pháp hay nhất, đây không phải là một 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ỉ viết tập hợp thay đổi tối thiểu, hãy chia dữ liệu thành các cây con và chỉ viết các cây đó là tốt nhất luôn viết toàn bộ cây trạng thái. Không cải thiện gì nhiều vẫn sẽ tốt hơn.

Cuối cùng, bạn nên 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à việc ghi nhỏ vào IndexedDB sẽ hoạt động tốt hơn các lượt ghi lớn, nhưng điều này chỉ quan trọng nếu việc ghi vào IndexedDB mà ứng dụng của bạn đang thực hiện 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à bạn phải đo lường để hiểu mình đang tối ưu hoá mục tiêu gì.

Kết luận

Các nhà phát triển có thể tận dụng các 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 trong các phiên mà còn giảm thời gian cần thiết để 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 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ì bộ nhớ của ứ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 quan trọng là mã của bạn phải được kiểm thử kỹ và xử lý lỗi đúng cách, ngay cả những lỗi ban đầu có vẻ như không xảy ra.