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

Những thông tin 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 một ứng dụng web tiến bộ (PWA).

Douglas Parker
Douglas Parker
[Tên người]
Google Workspace Riley
Dikla Cohen
Dikla Cohen

Đây là bài đăng đầu tiên trong chuỗi bài đăng trên blog về các bài học mà nhóm Google Bản tin học được trong khi xây dựng một ứng dụng web tiến bộ (PWA) dành cho 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, phương pháp chúng tôi đã thực hiện để vượt qua và lời khuyên chung để tránh các sai lầm. Điều này chưa có nghĩa là bạn đã nắm được thông tin tổng quan đầy đủ về PWA. Mục tiêu 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.

Đối với bài đăng đầu tiên này, trước tiên, chúng ta sẽ đề cập đến một số thông tin cơ bản, sau đó đi sâu vào tất cả nội dung chúng ta đã tìm hiểu được 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ích cực từ giữa năm 2017 đến giữa năm 2019.

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

Trước khi tìm hiểu về quy trình phát triển, hãy cùng tìm hiểu lý do tại sao việc xây dựng 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. Bản tin đặc biệt hữu ích vì Bản tin sẽ được thí điểm ở nhiều thị trường.
  • Cơ sở mã duy nhất. Người dùng của chúng tôi tương đối đồng đều giữa Android và iOS. PWA nghĩa là chúng tôi có thể xây dựng một ứng dụng web duy nhất hoạt động trên cả hai nền tảng. Điều này giúp tăng tốc độ và tầm ảnh hưởng của đội ngũ.
  • Đượ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ảm số lượng ứng dụng lỗi thời. Chúng tôi đã có thể đưa ra những thay đổi có thể gây lỗi cho phần phụ trợ trong thời gian di chuyển rất ngắn cho khách hàng.
  • 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. Những tiện ích tích hợp như vậy là một yêu cầu bắt buộc đối với ứng dụng. Với PWA, ứng dụng thường chỉ đơn giản là mở một URL.
  • Loại bỏ rào cản 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 và được hỗ trợ tốt đều sẽ hoạt động.

Những gì chúng tôi học được về trình chạy dịch vụ

Bạn không thể có PWA nếu không có trình chạy dịch vụ. Trình chạy dịch vụ cung cấp cho bạn rất nhiều sức mạnh, 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á nền, v.v. Mặc dù trình chạy dịch vụ có thêm một số yếu tố phức tạp, nhưng chúng tôi nhận thấy rằng lợi ích của chúng lớn hơn sự phức tạp bổ sung.

Hãy tạo 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 yêu cầu 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 đố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 nhóm công nghệ nội bộ của mình, 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ụ. Những điều chúng tôi đúc kết được dưới đây đôi khi sẽ phản ánh điều đó. Hãy truy cập phần Các sai lầm đố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 service-worker

Một số thư viện JS đưa ra các giả định sẽ không hoạt động như mong đợi khi chạy bởi một trình chạy dịch vụ. Ví dụ: giả sử có sẵn window hoặc document hoặc sử dụng API không có sẵn cho trình chạy 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 sử dụng gapi.js để xác thực, nhưng không thực hiện được do 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 nếu có thể để hỗ trợ các trường hợp sử dụng 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 toàn cục.

Tránh truy cập vào 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ể gặp phải trường hợp không mong muốn này:

  1. Người dùng có ứng dụng web có phiên bản IndexedDB (IDB) N
  2. Ứng dụng web mới được phát hành bằng IDB phiên bản N+1
  3. Người dùng truy cập PWA, đây là chế độ kích hoạt việc 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 quy trình nâng cấp trình chạy dịch vụ sẽ bị treo vì kết nối đang hoạt động vẫn mở cho phiên bản cơ sở dữ liệu cũ
  6. Service worker 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ụ này chưa từng cài đặt, thì người dùng sẽ không bao giờ nhận được ứng dụng đã cập nhật.

Giúp kênh có khả năng phục hồi

Mặc dù 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 thực hiện hoạt động I/O (mạng, IDB, v.v.). Bạn có thể tiếp tục mọi quá trình diễn ra trong thời gian dài bất cứ lúc nào.

Trong trường hợp quy 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 để quá trình tải lên một phần bị gián đoạn là tận dụng hệ thống tiếp tục của thư viện tải lên nội bộ, lưu URL tải lên tiếp nối 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 không hoàn tất lần tải lên đầu tiên. Ngoài ra, trước bất kỳ hoạt động I/O nào diễn ra trong thời gian dài, trạng thái được lưu vào IDB để cho biết vị trí của mỗi 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 đợ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 cũng như ngữ cảnh trình chạy dịch vụ (chẳng hạn như ghi nhật ký, gắn cờ, đồng bộ hoá, v.v.). Mã cần phải bảo vệ 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ộ

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 với những gì bạn muốn, đặc biệt là khi cập nhật từng phần. Bạn vẫn muốn cài đặt trình chạy máy chủ để có thể gỡ lỗi các vấn đề với trình chạy này hoặc làm việc với các API khác như đồng bộ hoá trong nền hoặc thông báo. Trên Chrome, bạn có thể làm được điều 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 mạng) (bảng Application (Ứng dụng) > ngăn Service worker (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 Network (Mạng) để tắt bộ nhớ đệm của bộ nhớ. Để áp dụng cho 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 cờ vào để vô hiệu hoá chức năng lưu vào bộ nhớ đệm trong trình chạy dịch vụ. Tính năng này được bật theo mặc định trên 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 mới nhất mà không gặp phải vấn đề khi 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 khi tích hợp liên tục để cảnh báo nếu bạn phá vỡ một trong các tiêu chí là 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ụ chưa cài đặt và chúng tôi không nhận ra điều đó trước khi triển khai quá trình sản xuất. Việc sử dụng Lighthouse trong CI sẽ giúp ngăn chặn điều đó.

Ưu tiên việc phân phối liên tục

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

Chúng tôi đã có thể triển khai các thay đổi có thể gây lỗi cho phần phụ trợ trong khoảng thời gian di chuyển rất ngắn cho khách hà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 các ứng dụng cũ có thể tồn tại ở chế độ 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ụ bị loại sau vài tuần nên trường hợp này 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 trong khi đã lỗi thời hoặc nội dung sẽ tự hết hạn sau vài tuần. Trong thực tế, chúng tôi chưa bao giờ gặp phải vấn đề từ các ứng dụng cũ. Mức độ nghiêm ngặt mà một nhóm cụ thể muốn tham gia tuỳ thuộc vào trường hợp sử dụng cụ thể của họ, nhưng PWA cung cấp tính linh hoạt cao hơn đáng kể so với các ứng dụng iOS/Android.

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

Đôi khi cần phải truy cập vào các giá trị cookie trong ngữ cảnh trình chạy dịch vụ. Trong trường hợp này, 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 một trình chạy dịch vụ, bạn không thể sử dụng các API đồng bộ, chẳng hạn 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 (ở chế độ cửa sổ) từ trình chạy dịch vụ để yêu cầu các giá trị cookie, mặc dù trình chạy dịch vụ này có thể chạy trong nền mà không có 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 của mình. Điểm cuối này chỉ lặp lại giá trị cookie trở lại cho ứng dụng khách. Trình chạy dịch vụ đã gửi một 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 bản 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ợ API này nữa 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à có thể được trình chạy dịch vụ sử dụng trực tiếp.

Các lỗi đối với trình chạy 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 nào được lưu vào bộ nhớ đệm thay đổi

Một mẫu PWA phổ biến là để một 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 truy cập trực tiếp vào bộ nhớ đệm của cache Storage API cho tất cả các lượt truy cập sau đó. 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 của 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 tệp tập lệnh trình chạy dịch vụ sẽ tự thay đổi theo một cách nào đó khi tệp đã 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 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 của trình chạy 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 trình chạy dịch vụ 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')));

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 điều kiện 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 xác nhận kết quả. Một cách dễ dàng hơn để sắp xếp cấu trúc là uỷ quyền tất cả quá trình triển khai cho một tệp khác, cách này có thể 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 khi kiểm thử đơn vị cho một tập lệnh trình chạy dịch vụ, nên chúng tôi đã giữ nguyên tập lệnh của trình chạy dịch vụ cốt lõi ở mức tối thiểu, chia hầu hết hoạt động triển khai thành các mô-đun khác. Vì các tệp đó chỉ là mô-đun JS tiêu chuẩn, nên bạn có thể kiểm thử đơn vị các tệp này dễ dàng hơn bằng thư viện kiểm thử tiêu chuẩn.

Nhớ đón xem phần 2 và 3 nhé

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