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.
Đâ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, chúng ta sẽ tìm hiểu một chút thông tin cơ bản trước, sau đó tìm hiểu tất cả những điều chúng ta đã học được về worker 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:
- Có 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 các ứ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 nhiều quyền, chẳng hạn như các chiến lược lưu vào bộ nhớ đệm nâng cao, chức năng ngoại tuyến, đồng bộ hoá ở chế độ nền, v.v. Mặc dù trình chạy dịch vụ làm tăng thêm một số độ phức tạp, nhưng chúng tôi nhận thấy rằng lợi ích của trình chạy dịch vụ lớn hơn độ phức tạp tăng thêm.
Tạo mã này 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ý tài nguyên được lưu vào bộ nhớ đệm và viết lại logic theo cách thủ công. Điều này thường gặp ở 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:
- Người dùng có ứng dụng web với IndexedDB (IDB) phiên bản N
- Ứng dụng web mới được đẩy bằng IDB phiên bản N+1
- 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
- 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 - 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
- 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 tôi, 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ù tập lệnh của worker 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 thực hiện các thao tác I/O (mạng, IDB, v.v.). Mọi quy trình chạy trong thời gian dài đều có thể 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 bảo vệ các dịch vụ mà nó 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 các 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 các vấn đề với worker đó hoặc làm việc với các API khác như đồng bộ hoá ở chế độ nền hoặc thông báo. Trên Chrome, bạn có thể thực hiện 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 Bypass for network (Bỏ qua cho mạng) (bảng điều khiển Application (Ứng dụng) > ngăn Service workers (Trình chạy dịch vụ)) ngoài việc bật hộp đánh dấu Disable cache (Tắt bộ nhớ đệm) trong bảng điều khiển Network (Mạng) để tắt cả 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 giúp đảm bảo rằng các nhà phát triển luôn nhận được các thay đổi mới nhất mà không gặp vấn đề nào về 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 ở chế độ 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 bản 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 đến các ứng dụng đang hoạt động (có cửa sổ) từ worker dịch vụ để yêu cầu các giá trị cookie, mặc dù worker dịch vụ có thể chạy ở chế độ nền mà không có ứng dụng nào có cửa sổ, 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ỉ cần phản hồi giá trị cookie trở lại cho ứng dụng. 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 bản phát hành Cookie Store API, bạn không cần phải sử dụng giải pháp này nữa đối với các trình duyệt hỗ trợ API này, vì API này cung cấp quyền truy cập không đồng bộ vào cookie của trình duyệt và có thể được worker dịch vụ sử dụng trực tiếp.
Các cạm bẫy đối với trình chạy 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ột mẫu PWA phổ biến là để trình chạy dịch vụ cài đặt tất cả tệp ứng dụng tĩnh trong giai đoạn install
, cho phép ứng dụng truy cập trực tiếp vào bộ nhớ đệm API Bộ nhớ đệm cho 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 tôi 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ư Workbox 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: