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

Những nội dung mà nhóm Bản tin tìm hiểu được về trình chạy dịch vụ trong quá trình phát triển 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ề các bài học mà nhóm Google Bản tin rút ra được trong khi xây dựng một ứng dụng web tiến bộ (PWA) phía bên ngoài. Trong những 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, những phương pháp đã thực hiện để vượt qua những thách thức đó và lời khuyên chung để tránh các cạm bẫy. Điều này không có nghĩa là thông tin tổng quan đầy đủ về PWA. Mục đích của chúng tôi là chia sẻ những bài học rút ra được 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

Bản tin đ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 tạo PWA

Trước khi tìm hiểu sâu hơn về quá trình phát triển, hãy tìm hiểu lý do vì sao việc tạo PWA là một lựa chọn hấp dẫn đối với dự án này:

  • Khả năng lặp lại nhanh chóng. Bản tin sẽ đặc biệt có giá trị vì được thí điểm ở nhiều thị trường.
  • Cơ sở mã đơn. Người dùng của chúng tôi được phân chia gần như đồng đều giữa Android và iOS. PWA có nghĩa là chúng tôi có thể tạo một ứng dụng web duy nhất hoạt động được trên cả hai nền tảng. Điều này giúp tăng tốc độ và mức độ 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 để làm giảm số lượng ứng dụng lỗi thời trong môi trường tự nhiên. Chúng tôi đã có thể đưa ra các thay đổi có thể gây lỗi trong phần phụ trợ với khoảng thời gian di chuyển rất ngắn cho ứng dụng.
  • Dễ dàng tích hợp với ứng dụng của bên thứ nhất và bên thứ ba. Những tích hợp như vậy là một yêu cầu đối với ứng dụng. Với PWA, thường chỉ có nghĩa là mở một URL.
  • Loại bỏ 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 đã 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ó trình thực thi 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 tệp nếu 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 đòi hỏi phải quản lý tài nguyên đã lưu vào bộ nhớ đệm và ghi lại logic theo cách thủ công, đây là điều phổ biến đối với hầu hết các thư viện của trình chạy dịch vụ, chẳng hạn như Hộp công việc.

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ý trình chạy dịch vụ. Bài học sau đây của chúng tôi đôi khi sẽ phản ánh điều đó. Truy cập phần Các lỗi dành cho các 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 trình chạy 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 chạy bởi một trình chạy dịch vụ. 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 các trình thực thi dịch vụ (XMLHttpRequest, bộ nhớ cục bộ, v.v.). Hãy đả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 trình chạy dịch vụ. Đối với PWA cụ thể này, chúng tôi muốn dùng gapi.js để xác thực, nhưng không thể thực hiện được vì công cụ 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 bớt hoặc xoá các giả định không cần thiết về ngữ cảnh JavaScript (nếu có thể) để hỗ trợ các trường hợp sử dụng của trình chạy dịch vụ, chẳng hạn như bằng cách tránh các API không tương thích với trình chạy dịch vụ và tránh trạng thái chung.

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 trình chạy dịch vụ, nếu không, bạn có thể rơi vào tình huống không mong muốn này:

  1. Người dùng có ứng dụng web sử dụng 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 PWA, thao tác 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 sẽ đọ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 để đi từ N lên N+1
  5. Vì người dùng sử dụng ứng dụng cũ có phiên bản N, nên quá trình nâng cấp trình chạy dịch vụ sẽ bị treo vì các kết nối đang hoạt động vẫn đang mở đến phiên bản cơ sở dữ liệu cũ
  6. Service worker bị treo và không bao giờ cài đặt

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

Tạo ra khả năng chống chịu

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 quá trình 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 quá trình tải lên một phần bị gián đoạ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 chưa hoàn tất trong lần đầu tiên. Ngoài ra, trước khi có bất kỳ thao tác I/O nào chạy trong thời gian dài, trạng thái đã được lưu vào IDB để cho biết vị trí của từng bản ghi trong quy trình.

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

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 muốn sẽ không xuất hiện. Rất nhiều mã của chúng ta chạy trong cả ngữ cảnh window cũng như ngữ cảnh trình chạy 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 chung theo cách sẽ hoạt động trong mọi ngữ cảnh. Ngoài ra, hãy hạn chế sử dụng dữ liệu được lưu trữ trong các biến toàn cục, 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, đây là điều trái ngược với mong muốn của bạn, đặc biệt là khi bản cập nhật được thực hiện từng phần. 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ể 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 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. Để đáp ứng 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 đính kèm cờ để tắt tính năng lưu vào bộ nhớ đệm trong trình chạy dịch vụ (được bật theo mặc định trên các bản dựng của 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 phải thêm tiêu đề Cache-Control: no-cache để ngăn trình duyệt lưu bất kỳ tài sả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 hỗ trợ 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, đó là khi worker không cài đặt dịch vụ và chúng tôi không nhận ra điều này trước khi đẩy sản phẩm. Việc có Lighthouse trong CI của chúng tôi sẽ ngăn chặn điều đó.

Ưu tiên khả năng phân phối liên tục

Vì trình chạy 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 làm giảm đáng kể số lượng ứng dụng lỗi thời trong môi trường tự nhiên. Khi người dùng mở ứng dụng của chúng ta, trình chạy dịch vụ sẽ phân phát ứng dụng cũ trong khi tải từng phần ứng dụng mới xuống. Sau khi ứng dụng mới được tải xuống, ứng dụng sẽ nhắc người dùng làm mới trang để sử dụng các tính năng mới. Ngay cả khi người dùng bỏ qua yêu cầu này, vào lần làm mới trang tiếp theo, họ vẫn sẽ nhận được phiên bản ứng dụng mới. Do đó, người dùng rất khó từ chối bản cập nhật giống như cách họ có thể làm đối với các ứng dụng iOS/Android.

Chúng tôi đã có thể đưa ra các thay đổi có thể gây lỗi trong phần phụ trợ với thời gian di chuyển rất ngắn cho ứ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 các ứng dụng mới trước khi thực hiện những thay đổi có thể gây lỗi. Vì ứng dụng sẽ phân phát khi lỗi thời, nên thực ra các ứng dụng cũ vẫn 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 một vài tuần để trường hợp này không xảy ra. Đối với Android, vấn đề này có thể được giảm thiểu bằng cách không phân phát khi 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. Trên thực tế, chúng tôi chưa bao giờ gặp sự cố từ các ứng dụng cũ. Mức độ nghiêm ngặt mà một nhóm cụ thể 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 có khả năng linh hoạt cao hơn đáng kể so với ứng dụng iOS/Android.

Nhận giá trị cookie trong trình chạy dịch vụ

Đôi khi, bạn cần phải truy cập vào các giá trị cookie trong ngữ cảnh của trình chạy dịch vụ. Trong trường hợp này, chúng ta 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 một trình chạy dịch vụ, các API đồng bộ như document.cookies sẽ không có sẵn. 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ụ đã thực hiện yêu cầu mạng đến điểm cuối này và đọc phản hồi để nhận 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 trình chạy dịch vụ thay đổi nếu có bất kỳ tệp tĩnh được lưu vào bộ nhớ đệm nào 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 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 tệp tập lệnh trình chạy dịch vụ tự 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 làm việc này theo cách thủ công bằng cách nhúng một hàm băm của nhóm tệp tài nguyên tĩnh trong tập lệnh trình chạy dịch vụ, vì vậy, mỗi bản phát hành đều tạo ra một tệp JavaScript trình chạy 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ị

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

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

Đây có thể là việc không dễ dàng khi 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, chờ lệnh gọi lại respondWith(), sau đó chờ lời hứa, trước khi cuối cùng xác nhận kết quả. Một cách dễ dàng hơn để định cấu trúc đối tượng này là uỷ quyền tất cả phương thức triển khai cho một tệp khác. Tệp này sẽ được kiểm thử dễ dàng 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ị một tập lệnh trình chạy dịch vụ, chúng tôi đã giữ tập lệnh trình chạy dịch vụ cốt lõi ở dạng cơ bản nhất có thể, chia phần lớn hoạt động 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 chúng có thể dễ dàng kiểm thử đơn vị hơn bằng các thư viện kiểm thử tiêu chuẩn.

Đón xem phần 2 và 3

Trong phần 2 và 3 của loạt bài này, chúng ta sẽ nói về quản lý phương tiện và các vấn đề cụ thể trên iOS. Nếu bạn muốn hỏi thêm về cách xây dựng PWA tại Google, hãy truy cập vào hồ sơ tác giả để tìm hiểu cách liên hệ với chúng tôi: