Vượt xa Loading Spinner: Xây dựng ứng dụng Offline-First bền bỉ với PouchDB và CouchDB

Database tutorial - IT technology blog
Database tutorial - IT technology blog

Tại sao Offline-First trở thành tiêu chuẩn mới

Tôi đã dành một thập kỷ để khắc phục các trạng thái dữ liệu bị lỗi trong các hệ thống phân tán. Dù làm việc với MySQL, PostgreSQL hay MongoDB, điểm gây đau đầu nhất luôn giống nhau: hiệu ứng “Lie-Fi”. Điều này xảy ra khi thiết bị hiển thị đầy sóng, nhưng kết nối thực tế đã chết. Nếu ứng dụng của bạn phụ thuộc vào việc gửi yêu cầu API liên tục, người dùng sẽ bị kẹt lại với biểu tượng vòng xoay loading. Đó là một cách gây ức chế khiến bạn mất đi khách hàng.

Cách tiếp cận offline-first thay đổi cuộc chơi này. Thay vì gọi API từ xa cho mọi hành động, ứng dụng của bạn sẽ giao tiếp với một cơ sở dữ liệu nằm ngay trong trình duyệt. Nó mang tính nội bộ, nhanh chóng và luôn sẵn sàng. Khi thiết bị tìm thấy tín hiệu 4G hoặc Wi-Fi, dữ liệu sẽ được đồng bộ hóa ở chế độ nền. PouchDB và CouchDB là tiêu chuẩn vàng cho quy trình làm việc này.

Sự dịch chuyển kiến trúc: REST đối đầu với Sync

Để đánh giá cao bộ công cụ này, bạn phải xem cách nó thay thế chu trình yêu cầu-phản hồi (request-response) tiêu chuẩn mà chúng ta đã sử dụng trong suốt hai mươi năm qua.

Mô hình Online-Only truyền thống

Trong một thiết lập React hoặc Vue điển hình, người dùng nhấp vào “Lưu” và frontend gửi một yêu cầu POST. Server xử lý nó, cập nhật cơ sở dữ liệu như PostgreSQL và gửi phản hồi 200 OK. Nếu một nhân viên giao hàng đi vào kho bê tông và mất tín hiệu, yêu cầu đó sẽ thất bại. Sau đó, bạn phải tự xây dựng các logic thử lại (retry) phức tạp và các trạng thái “Bản nháp” một cách thủ công.

Mô hình Offline-First (PouchDB + CouchDB)

Với PouchDB, ứng dụng của bạn tương tác với IndexedDB trên ổ đĩa cục bộ. Việc ghi dữ liệu gần như tức thời, thường mất chưa đến 10ms. Không có vòng xoay loading vì dữ liệu được lưu cục bộ trước. Đồng bộ hóa trở thành một tiến trình chạy ngầm. Nếu người dùng ngoại tuyến trong hai giờ, họ vẫn có thể tiếp tục làm việc mà không gặp bất kỳ thông báo lỗi nào.

Ưu và nhược điểm: Những gì cần kỳ vọng

Không có bộ công cụ nào là “viên đạn bạc”. Mặc dù sự kết hợp này rất mạnh mẽ, bạn cần biết những giới hạn của nó nằm ở đâu.

Những lợi ích vượt trội

  • Độ trễ bằng không: Giao diện người dùng mang lại cảm giác cực kỳ mượt mà vì bạn không phải chờ đợi 300ms cho một hành động gửi lên server và phản hồi về.
  • Đồng bộ hóa gốc: PouchDB sử dụng giao thức sao chép của CouchDB một cách tự nhiên. Bạn không cần viết một dòng code nào để xử lý việc truyền tải dữ liệu thực tế.
  • Quản lý xung đột thông minh: CouchDB sử dụng hệ thống dựa trên phiên bản (revision) tương tự như Git. Nếu hai người cùng chỉnh sửa một tài liệu, nó sẽ lưu cả hai phiên bản và cho phép bạn giải quyết sự khác biệt sau đó.

Các điểm cần đánh đổi

  • Giới hạn lưu trữ: Các trình duyệt thường giới hạn IndexedDB ở mức khoảng 60% dung lượng đĩa trống. Nó hoàn hảo cho hàng ngàn bản ghi, nhưng đừng cố lưu trữ một thư viện video 50GB trên client.
  • Tính nhất quán cuối cùng (Eventual Consistency): Các thay đổi không đến server ngay mili giây chúng xảy ra. Giao diện của bạn cần phản ánh rằng dữ liệu đã được “lưu cục bộ” thay vì “đã đồng bộ lên đám mây”.
  • Chi phí bảo mật: Vì PouchDB giao tiếp trực tiếp với cơ sở dữ liệu, bạn phải cấu hình CORS và bảo mật cấp độ tài liệu (thông qua Validation Functions) một cách chính xác để tránh rò rỉ dữ liệu.

Thiết lập thực tế

Kiến trúc ưa thích của tôi sử dụng React frontend với PouchDB làm kho lưu trữ dữ liệu chính. Dữ liệu này đồng bộ với một instance CouchDB chạy trong Docker container. Đối với môi trường production, tôi thường thêm một dịch vụ Node.js nhỏ để xử lý xác thực dựa trên JWT trước khi dữ liệu đi tới CouchDB.

[ Thiết bị cục bộ: PouchDB ] <-- (Đồng bộ hai chiều) --> [ Server từ xa: CouchDB ]

Triển khai từng bước

1. Triển khai CouchDB

Docker là cách đáng tin cậy nhất để khởi chạy CouchDB. Cấu hình này đảm bảo dữ liệu của bạn vẫn tồn tại ngay cả khi container khởi động lại.

docker run -d \
  --name couchdb-prod \
  -e COUCHDB_USER=admin \
  -e COUCHDB_PASSWORD=your_secure_password \
  -p 5984:5984 \
  -v $(pwd)/couchdata:/opt/couchdb/data \
  couchdb:latest

Sau khi container khởi động, bạn phải bật CORS. Nếu không có điều này, trình duyệt sẽ chặn mọi nỗ lực đồng bộ hóa. Bạn có thể bật tính năng này trong bảng điều khiển Fauxton tại http://localhost:5984/_utils trong tab Configuration.

2. Khởi tạo PouchDB

Đầu tiên, tải gói từ npm:

npm install pouchdb

Sau đó, thiết lập các instance cơ sở dữ liệu cục bộ và từ xa. Lưu ý cách chúng ta đưa thông tin xác thực vào URL từ xa trong ví dụ này.

import PouchDB from 'pouchdb';

const localDB = new PouchDB('app_records');
const remoteDB = new PouchDB('http://admin:password@localhost:5984/app_records');

3. Tự động hóa việc đồng bộ

Phương thức sync là động cơ của ứng dụng. Thiết lập live: true để duy trì kết nối liên tục, trong khi retry: true đảm bảo ứng dụng tự động kết nối lại sau khi đi qua hầm hoặc vùng mất mạng.

localDB.sync(remoteDB, {
  live: true,
  retry: true
}).on('change', (info) => {
  console.log('Dữ liệu mới đã được gửi từ server!');
}).on('error', (err) => {
  console.error('Đồng bộ thất bại:', err);
});

4. Thao tác với tài liệu

PouchDB là một kho lưu trữ tài liệu NoSQL. Mỗi mục nhập cần một _id duy nhất. Sử dụng chuỗi ISO hoặc UUID là tốt nhất trong trường hợp này.

async function saveNote(content) {
  const doc = {
    _id: `note_${new Date().getTime()}`,
    text: content,
    status: 'bản nháp'
  };
  
  try {
    await localDB.put(doc);
  } catch (err) {
    console.error("Lưu thất bại", err);
  }
}

Làm chủ các xung đột

Xung đột là điều không thể tránh khỏi trong các ứng dụng ngoại tuyến. Nếu hai người dùng cập nhật cùng một hồ sơ khi đang ngoại tuyến, CouchDB sẽ không tự đoán ai thắng. Nó đánh dấu tài liệu bằng một cờ _conflicts. Khi bạn lấy tài liệu, bạn có thể thấy cả hai phiên bản, so sánh dấu thời gian và xóa phiên bản bạn không muốn. Cách này an toàn hơn nhiều so với phương pháp “last-write-wins” (lần ghi cuối cùng sẽ thắng), vốn âm thầm xóa dữ liệu của người dùng.

Lời kết

Chuyển sang offline-first là một sự thay đổi trong tư duy. Bạn ngừng lo lắng về trạng thái mạng và bắt đầu tập trung vào trạng thái dữ liệu. Bằng cách đẩy logic mạng cho PouchDB xử lý, ứng dụng của bạn trở nên bền bỉ hơn đáng kể. Có thể mất thời gian để học về MapReduce views và revision IDs, nhưng kết quả là một trải nghiệm người dùng chuyên nghiệp và không thể phá vỡ. Nếu bạn đang xây dựng ứng dụng cho người dùng di động hoặc nhân viên hiện trường, bộ công cụ này rất đáng để đầu tư.

Share: