Offline-first là quyết định về mô hình dữ liệu, không phải một tính năng cache
Phần lớn ứng dụng gắn hỗ trợ offline vào phút chót rồi phát hiện đó vốn là câu hỏi kiến trúc. Đây là những thứ thật sự phải thay đổi, và các quy tắc giải quyết xung đột bạn buộc phải chọn giữa.
"Làm cho nó chạy offline" nghe như một tính năng thêm vào trong một sprint. Không phải. Nó thay đổi chỗ dữ liệu của bạn nằm và ai quyết định điều gì là đúng, và điều đó chạm vào mọi thứ.
Tin tốt là công việc này đã được hiểu rõ. Tin xấu là gắn nó vào sau tốn gấp vài lần so với xây từ đầu.
Hai kiểu kiến trúc
Kiến trúc mặc định là máy chủ nắm quyền. Màn hình phản ánh máy chủ. Một hành động gửi request, chờ, và cập nhật khi có phản hồi. Offline nghĩa là hỏng, vì không có gì để hiển thị và không có chỗ để ghi.
Kiểu còn lại là local-first. Thiết bị có cơ sở dữ liệu riêng và giao diện đọc từ đó. Các thao tác ghi đi thẳng vào bộ nhớ cục bộ ngay lập tức và màn hình cập nhật từ đó. Một tiến trình nền đối chiếu với máy chủ khi có kết nối.
Sự đảo ngược đó là toàn bộ vấn đề: mạng thôi nằm giữa người dùng và dữ liệu của họ. Mọi thứ khác đi theo.
Những gì thật sự thay đổi
Mỗi bản ghi cần một định danh sinh được khi offline
Nếu máy chủ cấp ID, thiết bị không thể tạo gì khi offline mà không bịa ra một ID tạm rồi viết lại mọi tham chiếu sau đó. Hãy sinh ID ở client, bằng UUID hoặc tương tự, ngay từ ngày đầu. Đúng một quyết định này rất rẻ lúc đầu và rất đau khi gắn về sau.
Thao tác ghi trở thành hàng đợi, không phải lời gọi
Một hành động ghi ý định vào một hàng đợi cục bộ bền vững rồi trả về ngay. Một tiến trình đồng bộ sẽ rút cạn hàng đợi đó. Nghĩa là hàng đợi phải sống sót qua việc app bị tắt, phải thử lại có giãn cách, và phải bất biến khi lặp, vì bạn sẽ gửi một số thao tác hai lần.
Xoá thôi là xoá
Nếu một thiết bị xoá một bản ghi khi offline, và đồng bộ chỉ gửi những gì đang tồn tại, máy chủ sẽ không bao giờ biết và sẽ nhiệt tình gửi lại bản ghi đó. Bạn cần bia mộ: việc xoá được ghi lại như một dữ kiện có mốc thời gian, giữ đủ lâu để mọi thiết bị nhìn thấy.
Giao diện phải thể hiện trạng thái đồng bộ
Một khi thao tác ghi có thể thành công cục bộ rồi thất bại sau đó, người dùng cần biết. Không phải spinner trên mọi thứ, mà một chỉ báo trung thực: đã lưu cục bộ, đang đồng bộ, đã đồng bộ, thất bại. Giấu điều này tạo ra kết cục tệ nhất, là ai đó tin rằng công việc của mình an toàn trong khi không phải vậy.
Chọn quy tắc xử lý xung đột
Hai thiết bị sửa cùng một bản ghi khi offline. Cả hai quay lại. Phải có gì đó quyết định. Không có đáp án đúng phổ quát; chỉ có đáp án đúng cho dữ liệu của bạn.
| Chiến lược | Cách hoạt động | Hợp với |
|---|---|---|
| Ghi sau thắng | Mốc thời gian lớn nhất ghi đè | Cài đặt, trạng thái theo thiết bị, trường ít rủi ro |
| Trộn theo trường | Mốc thời gian cho từng trường; trộn các trường không xung đột | Bản ghi mà người ta sửa các phần khác nhau |
| Nhật ký thao tác | Lưu ý định, phát lại theo thứ tự | Bộ đếm, danh sách, mọi thứ mang tính cộng dồn |
| CRDT | Cấu trúc trộn được một cách tất định | Văn bản cộng tác và tài liệu chia sẻ |
| Hỏi người dùng | Đưa cả hai ra, để họ chọn | Chỉ dành cho xung đột hiếm và giá trị cao |
Hai cảnh báo từ kinh nghiệm. "Ghi sau thắng" dựa trên đồng hồ thiết bị sẽ âm thầm làm mất dữ liệu, vì đồng hồ thiết bị sai; hãy dùng mốc do máy chủ cấp hoặc đồng hồ logic. Và "hỏi người dùng" không phải mặc định; nó là lối thoát hiểm cho những trường hợp quá tốn kém để tự giải quyết. Bị hỏi đủ nhiều lần, người ta sẽ bấm bừa mà không đọc.
Hãy chọn quy tắc xung đột theo từng loại dữ liệu, không phải theo cả ứng dụng. Tiến độ flashcard của người dùng có thể cộng dồn và trộn sạch sẽ. Tên hiển thị của họ thì "ghi sau thắng" là đủ. Dùng chung một quy tắc cho cả hai là sai với một trong hai.
Bản thân việc đồng bộ
Hình dạng khả dụng cho phần lớn sản phẩm:
- Mỗi bản ghi mang một số phiên bản hoặc mốc cập nhật do máy chủ gán.
- Client lưu một con trỏ: điểm mà nó đã theo kịp.
- Khi đồng bộ, client gửi các thao tác trong hàng đợi cùng con trỏ của mình.
- Máy chủ áp dụng các thao tác, giải quyết xung đột, và trả về mọi thay đổi kể từ con trỏ đó cộng với một con trỏ mới.
- Client áp dụng thay đổi và dịch con trỏ một cách nguyên tử.
Tính nguyên tử ở bước 5 quan trọng hơn vẻ ngoài của nó. Nếu bạn dịch con trỏ rồi thất bại lúc áp dụng, bạn đã âm thầm bỏ qua các thay đổi và sẽ không bao giờ có gì báo cho bạn biết.
Hãy chủ động kiểm thử các tình huống xấu
Lỗi offline không xuất hiện trong kiểm thử bình thường, vì kiểm thử bình thường có wifi. Hãy đưa những thứ này vào kế hoạch test:
- Bật chế độ máy bay một tuần, rồi kết nối lại với hàng trăm thao tác trong hàng đợi.
- Bị giết giữa lúc đồng bộ. Tắt cứng ứng dụng khi một lô đang bay. Không được mất gì và không được áp dụng hai lần.
- Cổng đăng nhập wifi công cộng. Trạng thái mạng tệ nhất: thiết bị báo có kết nối, mọi request trả về trang đăng nhập. Request phải thất bại sạch sẽ, không được làm hỏng trạng thái.
- Lệch đồng hồ. Chỉnh đồng hồ thiết bị lệch một ngày và xác nhận thứ tự vẫn đúng.
- Hai thiết bị, cùng tài khoản, cùng offline, cùng sửa một bản ghi. Đây là nơi quy tắc xung đột chứng minh giá trị.
Khi nào không nên làm
Offline-first là công việc thật, và không phải lúc nào cũng đáng. Hãy bỏ qua khi ứng dụng dù sao cũng vô nghĩa nếu không có mạng, như chat trực tiếp hay thanh toán; khi dữ liệu vốn dĩ được chia sẻ và đồng thời, nơi bạn cần real-time hơn là offline; hoặc khi đó là công cụ nội bộ ít dùng mà chi phí lớn hơn lợi ích.
Hãy làm khi người ta dùng ứng dụng ở nơi kết nối chập chờn: lúc đi làm, trên tàu xe, trên máy bay, vùng nông thôn, trong toà nhà sóng yếu. Với một ứng dụng học tập thì đó là phần lớn số phiên, nên chúng tôi coi nó là giả định khởi đầu chứ không phải một yêu cầu tính năng.
Phiên bản rẻ nhất
Nếu kiến trúc local-first đầy đủ là quá sức ở giai đoạn hiện tại, có một khoảng giữa rất rộng đáng lấy:
- Cache phần đọc thật mạnh để ứng dụng mở ra là có nội dung chứ không phải spinner.
- Xếp hàng các thao tác ghi một cách bền vững ngay cả khi chưa đồng bộ đầy đủ, để một hành động không bao giờ mất vì rớt mạng.
- Sinh ID ở client ngay từ bây giờ, vì hôm nay nó không tốn gì và về sau nó mở khoá mọi thứ.
Ba việc đó mang lại phần lớn lợi ích cảm nhận với một phần nhỏ công sức, và không việc nào bị lãng phí nếu sau này bạn đi xa hơn.