Xây dựng PWA tại Google, phần 1

Những điều mà nhóm Bulletin đã tìm hiểu về trình chạy dịch vụ trong quá trình phát triển một PWA.

Douglas Parker
Douglas Parker
Joel Riley
Joel Riley
Dikla Cohen
Dikla Cohen

Đây là bài đăng đầu tiên trong loạt bài đăng trên blog về những bài học mà nhóm Google Bulletin đã học được trong quá trình xây dựng một PWA hướng đến bên ngoài. Trong các bài đăng này, chúng tôi sẽ chia sẻ một số thách thức mà chúng tôi gặp phải, các phương pháp chúng tôi đã áp dụng để vượt qua những thách thức đó và lời khuyên chung để tránh các cạm bẫy. Đây không phải là thông tin tổng quan đầy đủ về PWA. Mục đích là chia sẻ những bài học rút ra từ kinh nghiệm của nhóm chúng tôi.

Trong bài đăng đầu tiên này, trước tiên, chúng ta sẽ đề cập đến một chút thông tin cơ bản rồi sau đó đi sâu vào tất cả những kiến thức đã tìm hiểu về trình chạy dịch vụ.

Thông tin khái quát

Bulletin đang trong quá trình phát triển từ giữa năm 2017 đến giữa năm 2019.

Lý do chúng tôi chọn xây dựng một PWA

Trước khi đi sâu vào quy trình phát triển, hãy cùng xem xét lý do việc xây dựng một PWA là một lựa chọn hấp dẫn cho dự án này:

  • Khả năng lặp lại nhanh chóng. Đặc biệt có giá trị vì Bản tin sẽ được thí điểm ở nhiều thị trường.
  • Một cơ sở mã. Người dùng của chúng tôi được chia gần như đồng đều giữa Android và iOS. PWA có nghĩa là chúng ta có thể tạo một ứng dụng web duy nhất hoạt động trên cả hai nền tảng. Điều này đã làm tăng tốc độ và tác động của nhóm.
  • Được cập nhật nhanh chóng và độc lập với hành vi của người dùng. PWA có thể tự động cập nhật, giúp giảm số lượng ứng dụng đã lỗi thời. Chúng tôi có thể đẩy các thay đổi về phần phụ trợ có thể gây lỗi với thời gian di chuyển rất ngắn cho ứng dụng khách.
  • Dễ dàng tích hợp với ứng dụng của bên thứ nhất và bên thứ ba. Các hoạt động tích hợp như vậy là yêu cầu đối với ứng dụng. Với PWA, điều này thường chỉ có nghĩa là mở một URL.
  • Loại bỏ sự phiền hà khi cài đặt ứng dụng.

Khung của chúng tôi

Đối với Bản tin, chúng tôi đã sử dụng Polymer, nhưng mọi khung hiện đại, được hỗ trợ tốt đều sẽ hoạt động.

Những điều chúng tôi tìm hiểu được về trình chạy dịch vụ

Bạn không thể có PWA nếu không có worker dịch vụ. Trình chạy dịch vụ mang lại cho bạn rất nhiều quyền lực, chẳng hạn như chiến lược lưu vào bộ nhớ đệm nâng cao, tính năng ngoại tuyến, đồng bộ hoá trong nền, v.v. Mặc dù trình chạy dịch vụ đôi khi phức tạp hơn, nhưng chúng tôi nhận thấy lợi ích của trình chạy dịch vụ vượt trội hơn độ phức tạp tăng thêm.

Hãy tạo nếu bạn có thể

Tránh viết tập lệnh trình chạy dịch vụ theo cách thủ công. Việc viết trình chạy dịch vụ theo cách thủ công yêu cầu bạn phải quản lý các tài nguyên được lưu vào bộ nhớ đệm và viết lại logic phổ biến cho hầu hết các thư viện trình chạy dịch vụ, chẳng hạn như Workbox.

Tuy nhiên, do ngăn xếp công nghệ nội bộ, chúng tôi không thể sử dụng thư viện để tạo và quản lý worker dịch vụ. Đôi khi, những điều chúng tôi rút ra được dưới đây sẽ phản ánh điều đó. Hãy truy cập vào bài viết Những cạm bẫy đối với trình chạy dịch vụ không được tạo để đọc thêm.

Không phải thư viện nào cũng tương thích với worker dịch vụ

Một số thư viện JS đưa ra các giả định không hoạt động như mong đợi khi được trình chạy dịch vụ chạy. Ví dụ: giả sử window hoặc document có sẵn hoặc sử dụng một API không có sẵn cho worker dịch vụ (XMLHttpRequest, bộ nhớ cục bộ, v.v.). Đảm bảo mọi thư viện quan trọng mà bạn cần cho ứng dụng đều tương thích với worker dịch vụ. Đối với PWA cụ thể này, chúng tôi muốn sử dụng gapi.js để xác thực, nhưng không thể vì PWA này không hỗ trợ trình chạy dịch vụ. Tác giả thư viện cũng nên giảm hoặc xoá các giả định không cần thiết về ngữ cảnh JavaScript khi có thể để hỗ trợ các trường hợp sử dụng worker dịch vụ, chẳng hạn như tránh các API không tương thích với worker dịch vụ và tránh trạng thái toàn cục.

Tránh truy cập IndexedDB trong quá trình khởi chạy

Đừng đọc IndexedDB khi khởi chạy tập lệnh worker dịch vụ, nếu không bạn có thể gặp phải tình huống không mong muốn này:

  1. Người dùng có ứng dụng web với IndexedDB (IDB) phiên bản N
  2. Ứng dụng web mới được đẩy bằng IDB phiên bản N+1
  3. Người dùng truy cập vào PWA, điều này sẽ kích hoạt quá trình tải trình chạy dịch vụ mới xuống
  4. Trình chạy dịch vụ mới đọc từ IDB trước khi đăng ký trình xử lý sự kiện install, kích hoạt chu kỳ nâng cấp IDB từ N lên N+1
  5. Vì người dùng có ứng dụng cũ với phiên bản N, nên quá trình nâng cấp trình chạy dịch vụ bị treo vì các kết nối đang hoạt động vẫn mở cho phiên bản cũ của cơ sở dữ liệu
  6. Trình chạy dịch vụ bị treo và không bao giờ cài đặt

Trong trường hợp của chúng ta, bộ nhớ đệm đã mất hiệu lực khi cài đặt worker dịch vụ, vì vậy, nếu worker dịch vụ không bao giờ được cài đặt, người dùng sẽ không bao giờ nhận được ứng dụng đã cập nhật.

Tăng khả năng phục hồi

Mặc dù các tập lệnh trình chạy dịch vụ chạy ở chế độ nền, nhưng các tập lệnh này cũng có thể bị chấm dứt bất cứ lúc nào, ngay cả khi đang ở giữa các hoạt động I/O (mạng, IDB, v.v.). Mọi quy trình chạy trong thời gian dài đều có thể được tiếp tục bất cứ lúc nào.

Trong trường hợp quá trình đồng bộ hoá tải các tệp lớn lên máy chủ và lưu vào IDB, giải pháp của chúng tôi cho việc tải lên bị gián đoạn một phần là tận dụng hệ thống có thể tiếp tục của thư viện tải lên nội bộ, lưu URL tải lên có thể tiếp tục vào IDB trước khi tải lên và sử dụng URL đó để tiếp tục tải lên nếu quá trình tải lên không hoàn tất trong lần đầu tiên. Ngoài ra, trước khi thực hiện bất kỳ thao tác I/O nào chạy trong thời gian dài, trạng thái sẽ được lưu vào IDB để cho biết vị trí của chúng ta trong quy trình cho mỗi bản ghi.

Không phụ thuộc vào trạng thái toàn cục

Vì trình chạy dịch vụ tồn tại trong một ngữ cảnh khác, nên nhiều biểu tượng mà bạn có thể mong đợi sẽ không xuất hiện. Rất nhiều mã của chúng tôi chạy trong cả ngữ cảnh window và ngữ cảnh worker dịch vụ (chẳng hạn như ghi nhật ký, cờ, đồng bộ hoá, v.v.). Mã cần phải có khả năng bảo vệ trước các dịch vụ mà mã đó sử dụng, chẳng hạn như bộ nhớ cục bộ hoặc cookie. Bạn có thể sử dụng globalThis để tham chiếu đến đối tượng toàn cục theo cách sẽ hoạt động trên tất cả ngữ cảnh. Ngoài ra, hãy sử dụng dữ liệu được lưu trữ trong các biến toàn cục một cách tiết kiệm, vì không có gì đảm bảo về thời điểm tập lệnh sẽ bị chấm dứt và trạng thái bị loại bỏ.

Phát triển cục bộ

Một thành phần chính của trình chạy dịch vụ là lưu tài nguyên vào bộ nhớ đệm cục bộ. Tuy nhiên, trong quá trình phát triển, điều này hoàn toàn ngược lại với những gì bạn muốn, đặc biệt là khi các bản cập nhật được thực hiện một cách lười biếng. Bạn vẫn muốn cài đặt worker máy chủ để có thể gỡ lỗi sự cố hoặc xử lý các API khác, chẳng hạn như đồng bộ hoá ở chế độ nền hoặc thông báo. Trên Chrome, bạn có thể làm việc này thông qua Công cụ của Chrome cho nhà phát triển bằng cách bật hộp đánh dấu Bỏ qua cho mạng (bảng điều khiển Ứng dụng > ngăn Trình chạy dịch vụ) ngoài việc bật hộp đánh dấu Tắt bộ nhớ đệm trong bảng điều khiển Mạng để tắt bộ nhớ đệm. Để hỗ trợ nhiều trình duyệt hơn, chúng tôi đã chọn một giải pháp khác bằng cách thêm một cờ để tắt tính năng lưu vào bộ nhớ đệm trong worker dịch vụ (được bật theo mặc định trên các bản dựng dành cho nhà phát triển). Điều này đảm bảo rằng các nhà phát triển luôn nhận được những thay đổi gần đây nhất mà không gặp bất kỳ vấn đề nào về việc lưu vào bộ nhớ đệm. Bạn cũng cần thêm tiêu đề Cache-Control: no-cache để ngăn trình duyệt lưu bất kỳ thành phần nào vào bộ nhớ đệm.

Ngọn hải đăng

Lighthouse cung cấp một số công cụ gỡ lỗi hữu ích cho PWA. Công cụ này quét một trang web và tạo báo cáo về PWA, hiệu suất, khả năng tiếp cận, SEO và các phương pháp hay nhất khác. Bạn nên chạy Lighthouse trong quá trình tích hợp liên tục để cảnh báo nếu bạn vi phạm một trong các tiêu chí trở thành PWA. Điều này thực sự đã xảy ra với chúng tôi một lần, khi trình chạy dịch vụ không cài đặt và chúng tôi không nhận ra điều đó trước khi đẩy vào kênh phát hành chính thức. Việc đưa Lighthouse vào quy trình CI sẽ giúp ngăn chặn điều đó.

Áp dụng phương pháp phân phối liên tục

Vì worker dịch vụ có thể tự động cập nhật, nên người dùng không thể giới hạn việc nâng cấp. Điều này giúp giảm đáng kể số lượng ứng dụng đã lỗi thời trong thực tế. Khi người dùng mở ứng dụng của chúng ta, worker dịch vụ sẽ phân phát ứng dụng cũ trong khi tải ứng dụng mới xuống một cách lười biếng. Sau khi tải ứng dụng mới xuống, ứng dụng sẽ nhắc người dùng làm mới trang để truy cập vào các tính năng mới. Ngay cả khi người dùng bỏ qua yêu cầu này, thì vào lần tiếp theo họ làm mới trang, họ sẽ nhận được phiên bản mới của ứng dụng. Do đó, người dùng khá khó từ chối bản cập nhật theo cách tương tự như đối với ứng dụng iOS/Android.

Chúng tôi có thể đẩy các thay đổi về phần phụ trợ có thể gây lỗi với thời gian di chuyển rất ngắn cho các ứng dụng. Thông thường, chúng tôi sẽ cho người dùng một tháng để cập nhật lên ứng dụng mới hơn trước khi thực hiện các thay đổi có thể gây lỗi. Vì ứng dụng sẽ phân phát khi đã lỗi thời, nên các ứng dụng cũ có thể tồn tại trong tự nhiên nếu người dùng không mở ứng dụng trong một thời gian dài. Trên iOS, trình chạy dịch vụ sẽ bị loại bỏ sau vài tuần nên trường hợp này sẽ không xảy ra. Đối với Android, bạn có thể giảm thiểu vấn đề này bằng cách không phân phát nội dung đã lỗi thời hoặc hết hạn nội dung theo cách thủ công sau vài tuần. Trong thực tế, chúng tôi chưa bao giờ gặp phải vấn đề nào từ các ứng dụng cũ. Mức độ nghiêm ngặt mà một nhóm muốn áp dụng tuỳ thuộc vào trường hợp sử dụng cụ thể của họ, nhưng PWA mang lại tính linh hoạt cao hơn đáng kể so với ứng dụng iOS/Android.

Lấy giá trị cookie trong worker dịch vụ

Đôi khi, bạn cần truy cập vào các giá trị cookie trong ngữ cảnh của worker dịch vụ. Trong trường hợp của chúng tôi, chúng tôi cần truy cập vào các giá trị cookie để tạo mã thông báo nhằm xác thực các yêu cầu API của bên thứ nhất. Trong worker dịch vụ, bạn không thể sử dụng các API đồng bộ như document.cookies. Bạn luôn có thể gửi thông báo cho các ứng dụng đang hoạt động (có cửa sổ) từ trình chạy dịch vụ để yêu cầu giá trị cookie, mặc dù trình chạy dịch vụ có thể chạy ở chế độ nền mà không cần bất kỳ ứng dụng cửa sổ nào, chẳng hạn như trong quá trình đồng bộ hoá ở chế độ nền. Để giải quyết vấn đề này, chúng tôi đã tạo một điểm cuối trên máy chủ giao diện người dùng chỉ đơn giản là lặp lại giá trị cookie cho máy khách. Trình chạy dịch vụ đã tạo một yêu cầu mạng đến điểm cuối này và đọc phản hồi để lấy các giá trị cookie.

Với việc phát hành Cookie Store API, giải pháp này sẽ không còn cần thiết cho các trình duyệt hỗ trợ giải pháp này, vì giải pháp này cung cấp quyền truy cập không đồng bộ vào cookie của trình duyệt và trình chạy dịch vụ có thể sử dụng trực tiếp giải pháp này.

Những khó khăn đối với các trình thực thi dịch vụ không được tạo

Đảm bảo tập lệnh của worker dịch vụ thay đổi nếu có tệp tĩnh nào được lưu vào bộ nhớ đệm thay đổi

Mẫu PWA phổ biến là để trình chạy dịch vụ cài đặt tất cả các tệp ứng dụng tĩnh trong giai đoạn install. Giai đoạn này cho phép ứng dụng trực tiếp truy cập vào bộ nhớ đệm của API Bộ nhớ đệm trong tất cả các lượt truy cập tiếp theo. Trình chạy dịch vụ chỉ được cài đặt khi trình duyệt phát hiện thấy tập lệnh trình chạy dịch vụ đã thay đổi theo một cách nào đó. Vì vậy, chúng ta phải đảm bảo rằng chính tệp tập lệnh trình chạy dịch vụ đã thay đổi theo một cách nào đó khi tệp được lưu vào bộ nhớ đệm thay đổi. Chúng tôi đã thực hiện việc này theo cách thủ công bằng cách nhúng hàm băm của tập hợp tệp tài nguyên tĩnh trong tập lệnh worker của dịch vụ. Vì vậy, mỗi bản phát hành sẽ tạo ra một tệp JavaScript worker dịch vụ riêng biệt. Các thư viện trình chạy dịch vụ như Hộp công việc sẽ tự động hoá quy trình này cho bạn.

Kiểm thử đơn vị

API của worker hoạt động bằng cách thêm trình nghe sự kiện vào đối tượng toàn cục. Ví dụ:

self.addEventListener('fetch', (evt) => evt.respondWith(fetch('/foo')));

Việc này có thể gây khó khăn cho việc kiểm thử vì bạn cần mô phỏng trình kích hoạt sự kiện, đối tượng sự kiện, đợi lệnh gọi lại respondWith(), sau đó đợi lời hứa, trước khi xác nhận kết quả. Một cách dễ dàng hơn để định cấu trúc việc này là uỷ quyền tất cả hoạt động triển khai cho một tệp khác, dễ kiểm thử hơn.

import fetchHandler from './fetch_handler.js';
self
.addEventListener('fetch', (evt) => evt.respondWith(fetchHandler(evt)));

Do những khó khăn trong việc kiểm thử đơn vị tập lệnh của worker dịch vụ, chúng tôi đã giữ cho tập lệnh worker dịch vụ cốt lõi ở mức tối giản nhất có thể, chia hầu hết quá trình triển khai thành các mô-đun khác. Vì các tệp đó chỉ là các mô-đun JS tiêu chuẩn, nên bạn có thể dễ dàng kiểm thử đơn vị bằng các thư viện kiểm thử tiêu chuẩn.

Hãy chú ý theo dõi phần 2 và 3

Trong phần 2 và 3 của loạt bài viết này, chúng ta sẽ thảo luận về việc quản lý nội dung nghe nhìn và các vấn đề dành riêng cho iOS. Nếu bạn muốn hỏi chúng tôi thêm về cách xây dựng PWA tại Google, hãy truy cập vào trang doanh nghiệp của tác giả để tìm hiểu cách liên hệ với chúng tôi: