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ề những bài học mà nhóm Google Bản tin rút ra trong khi tạo PWA hướng 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 phải đối mặt, các phương pháp chúng tôi đã thực hiện để vượt qua chúng và lời khuyên chung để tránh các cạm bẫ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 tôi sẽ đề cập đến một ít thông tin cơ bản, sau đó đi sâu vào tất cả nội dung mà chúng tôi đã tìm hiểu về service worker.

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 tại sao việc tạo PWA lại là một giải pháp hấp dẫn dành cho dự án này:

  • Khả năng lặp lại nhanh chóng. Đặc biệt giá trị vì Bản tin sẽ được thí điểm trong 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. Có nghĩa là một ứng dụng web tiến bộ (PWA) chúng tôi có thể xây dựng 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à sức ảnh hưở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 sẽ giảm số lượng ứng dụng lỗi thời. Chúng tôi đã có thể phát hành chương trình phụ trợ có thể gây lỗi thay đổi trong khoảng thời gian di chuyển rất ngắn cho khách hà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. Việc tích hợp như vậy là một yêu cầu cho ứng dụng. Với PWA, thông thường, chỉ cần 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 đã sử dụng Polymer, nhưng bất kỳ bản tin hiện đại nào, được hỗ trợ tốt khung sườn cụ thể 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 khi không có dịch vụ Worker. Trình chạy dịch vụ mang đến cho bạn nhiều quyền truy cập, chẳng hạn như chiến lược lưu vào bộ nhớ đệm nâng cao, các tính năng ngoại tuyến, đồng bộ hoá trong nền, Mặc dù nhân viên 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 họ lớn hơn lợi ích bổ sung độ phức tạp của chúng.

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 sẽ yêu cầu bạn viết theo cách thủ công quản lý tài nguyên được lưu vào bộ nhớ đệm và logic ghi lại thường thấy đố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ư dưới dạng Workbox.

Tuy nhiên, do ngăn xếp công nghệ nội bộ, chúng tôi không thể dùng thư viện để tạo và quản lý trình chạy dịch vụ của chúng tôi. Bài học sau đây của chúng tôi đôi khi sẽ phản ánh điều đó. Truy cập Pitfalls for (Vấn đề cho) 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ụ. Cho giả sử có thể sử dụng window, document hoặc sử dụng một API không dùng được cho dịch vụ worker (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 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ể vì phiên bản này không hỗ trợ trình chạy dịch vụ. Tác giả của thư viện cũng cầ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ợ việc sử dụng trình chạy dịch vụ các trường hợp, 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 các API không tương thích trạng thái.

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

Không đọc IndexedDB khi đang khởi chạy tập lệnh trình chạy dịch vụ của mình, 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 một trình xử lý sự kiện Chu kỳ nâng cấp IDB đi từ N đế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ụ sẽ bị treo khi đang hoạt động các kết nối vẫn đang mở đối với 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 này, 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ài đặt, người dùng không hề 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, thậm chí khi đang ở giữa hoạt động I/O (mạng, IDB, v.v.). Mọi quá trình chạy trong thời gian dài đều phải có thể tiếp tục tại bất kỳ thời điểm 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 đối với quá trình tải lên một phần bị gián đoạn là để tận dụng tính năng tiếp nối của thư viện tải lên nội bộ hệ thống, 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 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 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 từng bản ghi trong quy trình.

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

Bởi vì trình chạy dịch vụ tồn tại trong một ngữ cảnh khác, nhiều biểu tượng mà bạn có thể mong đợi tồn tại lại không hiện tại. Rất nhiều mã của chúng ta đã chạy trong cả ngữ cảnh window, cũng như trong ngữ cảnh trình chạy dịch vụ (chẳng hạn như như ghi nhật ký, gắn cờ, đồng bộ hoá, v.v.). Mã cần có khả năng bảo vệ đối với các dịch vụ mà mã này sử dụng, chẳng hạn 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 mọi ngữ cảnh. Đồng thời sử dụng dữ liệu đã lưu trữ trong các biến toàn cục, vì chúng tôi không thể đảm bảo về thời điểm tập lệnh sẽ bị chấm dứt và trạng thái bị trục xuất.

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, tính năng này hoàn toàn trái ngược với mong muốn của bạn, đặc biệt là khi quá trình 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ủ để bạn có thể gỡ lỗi sự cố 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ể 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 kiểm Bỏ qua cho mạng (bảng điều khiển Ứng dụng > ngăn Trình chạy dịch vụ) trong 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 để cũng tắt bộ nhớ đệm của bộ nhớ. Để bao gồm 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 bao gồ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 nhà phát triển) bản dựng. Đ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ây giờ 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ỳ nội dung nào vào bộ nhớ đệm.

Ngọn hải đăng

Lighthouse cung cấp một số tiện ích gỡ lỗi các công cụ hữu ích cho PWA. Công cụ này quét một trang web và tạo các 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 trên chế độ liên tục tích hợp để cảnh báo bạn nếu bạn phá vỡ một trong 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 service worker không cài đặt và chúng tôi đã không nhận ra điều đó trước khi bắt đầu sản xuất. Việc sử dụng Lighthouse trong CI sẽ có tác dụng đã ngăn cả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. Chiến dịch này sẽ 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 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 của ứng dụng mới xuống. Khi ứng dụng khách mới được tải xuống, trang này sẽ nhắc người dùng làm mới trang để truy cập các tính năng mới. Ngay cả khi người dùng đã bỏ qua yêu cầu này, thì lần tiếp theo khi làm mới trang, họ sẽ nhận được phiên bản của ứng dụng khách. Do đó, người dùng gặp khó khăn khi từ chối các bản cập nhật trong cùng một cho các ứng dụng iOS/Android.

Chúng tôi đã có thể đưa ra những 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 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 cho khách hàng mới hơn 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 là có thể xảy ra với các ứng dụng cũ tồn tại trong môi trường 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 sẽ 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 trong khi 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 sự cố từ ứng dụng cũ. Mức độ nghiêm ngặt muốn áp dụng ở đây tuỳ thuộc vào mục đích sử dụng cụ thể của họ nhưng PWA linh hoạt 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 tôi cần để truy cập vào các giá trị cookie nhằm 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 service worker, các API đồng bộ như document.cookies sẽ không sử dụng được. Bạn luôn có thể gửi thông báo cho các ứng dụng khách đang hoạt động (dạng cửa sổ) từ trình chạy dịch vụ để yêu cầu giá trị cookie, mặc dù có thể trình chạy dịch vụ chạy ở chế độ 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 chỉ 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 để nhận 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ợ nó, vì nó cung cấp quyền truy cập không đồng bộ vào các 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.

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 quá trình Giai đoạn install cho phép khách hàng truy cập trực tiếp vào bộ nhớ đệm của API Lưu trữ 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 dịch vụ tập lệnh trình chạy dịch vụ đã thay đổi theo cách nào đó, vì vậy chúng tôi phải đảm bảo chính tệp tập lệnh trình chạy dịch vụ thay đổi theo 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 tệp tài nguyên tĩnh trong tập lệnh trình chạy dịch vụ của chúng tôi, 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ụ. Các thư viện trình chạy dịch vụ như Hộp công việc 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à vấn đề bạn cần thử nghiệm vì bạn cần mô phỏng trình kích hoạt sự kiện, đối tượng sự kiện, hãy đợi 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 để 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, cách này dễ dàng hơn đã kiểm tra.

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ụ, nên chúng tôi đã giữ trình chạy dịch vụ chính này tập lệnh càng đơn giản càng tốt, chia hầu hết hoạt động triển khai thành các mô-đun khác. Từ những tệp đó chỉ là mô-đun JS tiêu chuẩn, chúng có thể dễ dàng kiểm thử đơn vị hơn bằng phương thức kiểm thử tiêu chuẩn thư việ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 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: