Progressive Web App (PWA) là dạng web app được xây dựng dựa trên các công nghệ của website, nhưng mang lại trải nghiệm tương tự như Native App. Nhờ vào tính năng của service worker, manifest và https, PWA có thể hoạt động offline ngay cả khi không có mạng.
Mặc dù PWA mang lại rất nhiều cải tiến cho web của bạn, nhưng nó lại không đòi hỏi bạn phải viết lại nhiều thứ trên website; bạn chỉ cần bổ sung một vài thứ là đủ. Và điều tuyệt vời hơn là bất kỳ website nào cũng có thể được cải tiến để có thêm phiên bản PWA dễ dàng.
Trên đây là một số điều cơ bản về Progressive Web App hay PWA mà mình liệt kê nhanh ra, chi tiết bạn tra cứu thông tin thêm trên Google vì bài này sẽ tập trung vào việc làm sao để xây dựng một ứng dụng PWA đúng chuẩn hơn là tập trung vào việc khái niệm và các ưu nhược điểm của nó. Vì vậy, ta sẽ bắt đầu triển khai PWA ngay bây giờ.
Phụ mục
Sử dụng HTTPS
Thành thật mà nói thì điều này nói ra hơi thừa vì hầu hết các website hiện nay đều đã chuyển sang dùng https protocol cả rồi SSL bổ sung cho bạn thêm một lớp bảo mật dựa trên nền tảng mã hóa giúp cho việc trao đổi thông tin giữa client và server trở nên an toàn hơn. Với PWA thì điều này trở nên quan trọng hơn vì nó đảm bảo cho các service worker hoạt động trơn tru và có thể cài đặt làm lên màn hình chính của thiết bị di động. Bạn có thể mua SSL với giả rẻ như SSL Rapid hoặc sử dụng Let’s Encryt miễn phí đều được.
Đăng ký một Service Worker
Để khai thác các tính năng của PWA(push notifications, caching, install prompts) bạn cần một Service Worker lắng nghe và thực hiện các yêu cầu được thiết lập như caching dữ liệu, hiện notification…
Rất dễ dàng thực hiện điều này vì bạn có thể sử dụng của Google, hoặc tự viết javascript, hoặc có thể dễ dàng tìm thấy các đoạn mã tương tự trên internet. Đoạn mã dưới đây có thể là một ví dụ cho bạn: đầu tiên chúng ta sẽ kiểm tra xem trình duyệt có hỗ trợ Javascript hay không; sau đó chúng ta sẽ tiếp tục đăng ký một service worker. Sau đó chúng ta lưu nó xuống dưới dạng một tập tin service‑worker.js. Hoặc ở thời điểm này bạn chỉ đơn giản lưu nó dưới dạng một tập tin mà không cần phải có một đoạn mã thật sự nào bên trong nó.
Để tổng quan, đoạn JS sử dụng cho service worker dưới đây sẽ được đăng ký ba sự kiện chính trong vòng đời của service worker đó là install, activate và fetch khi ứng dụng đưa ra yêu cầu mạng. Đoạn cuối cùng liên quan đến việc triển khai dữ liệu khi không có mạng dựa vào bộ nhớ offline.
Đoạn mã như sau:
if ('serviceWorker' in navigator) { window.addEventListener('load', function() { navigator.serviceWorker.register('service-worker.js').then(function(registration) { // Registration was successful console.log('Registered!'); }, function(err) { // registration failed :( console.log('ServiceWorker registration failed: ', err); }).catch(function(err) { console.log(err); }); }); } else { console.log('service worker is not supported'); } </script> // service-worker.js self.addEventListener('install', function() { console.log('Install!'); }); self.addEventListener("activate", event => { console.log('Activate!'); }); self.addEventListener('fetch', function(event) { console.log('Fetch!', event.request); }); |
Thêm push notifications
Service worker cho phép bạn đẩy thông báo đến người dùng của mình thông qua Push API. Để truy cập và sử dụng, bạn phải thông qua cơ chế self.registration.pushManager từ bên trong file service‑worker.js của mình.
Việc gửi thông báo đòi hỏi phải có thêm một vài thiết lập phụ trợ khác nên chúng ta sẽ không đi sâu vào nó lúc này. Nhưng nếu bạn là người xây dựng từ đầu, bạn có thể thiết lập Google’s Firebase service để gửi thông báo từ dịch vụ Firebase Cloud Messaging bằng Push API.
Dưới đây là đoạn mã cho phép bạn gửi tin nhắn thông qua Push API:
navigator.serviceWorker.ready.then(function(registration) { if (!registration.pushManager) { alert('No push notifications support.'); return false; } //To subscribe `push notification` from push manager registration.pushManager.subscribe({ userVisibleOnly: true //Always show notification when received }) .then(function (subscription) { console.log('Subscribed.'); }) .catch(function (error) { console.log('Subscription error: ', error); }); }) |
Thêm Web app manifest
Để làm cho ứng dụng PWA có thể cài đặt được, bạn cần một file manifest.json được đặt ở thư mục gốc (root) của website. Bạn có thể hiểu đây là một file thông tin về ứng dụng của bạn – tương tự như những gì bạn mô tả cho ứng dụng của mình trên Google Play hay App Store vậy.
Bạn có thể khai báo nhiều thông tin ở đây, nhưng dưới đây là một file manifest.json rút gọn những thông tin cần thiết để bạn bắt đầu làm việc:
{ "short_name": "Chat", "name": "Chat", "icons": [ { "src":"/assets/icon.png", "sizes": "192x192", "type": "image/png" } ], "start_url": "/?utm_source=homescreen", "background_color": "#e05a47", "theme_color": "#e05a47", "display": "standalone" } |
nếu bạn muốn khai báo thêm và đầy đủ hơn, bạn có thể xem thêm file manifest.json được Google hướng dẫn ở đây.
Sâu khi bạn tạo xong file này, hãy thêm đoạn mã này vào header của trang web để thông báo về file mô tả của mình:
Cấu hình trước khi cài đặt ứng dụng
Khi người dùng truy cập mnột trang web, AWP được kích hoạt thông qua service worker và manifest.json đã được khai báo. Chrome sẽ tự động nhắc họ cài đặt PWA vào màn hình chính của họ nếu họ thỏa mãn điều kiện: truy cập vào trang web lần thứ hai và mỗi phiên tối thiểu là 5 phút.
Ý tưởng trên không hẳn là tuyệt vời, chúng ta có thể khai triển một cách tiếp cận và cài đặt khác dựa trên servie worker đó là khi người dùng thực hiện một hành động nào đó (mà nó thể hiện sự quan tâm của họ với trang web của chúng ta) – chúng ta sẽ mời họ cài đặt PWA trên màn hình chính.
Để làm được điều này, bạn cần lưu lại sự kiện beforeinstallprompt để sử dụng sau và gọi lại nó ở thời điểm nào đó mà bạn cảm thấy phù hợp hơn. Ví dụ như người dùng ấn váo nút chia sẻ, hoặc họ lưu sản phẩm của bạn, hoặc thứ gì đó mà bạn cho rằng họ thực hiện điều đó vì họ yêu thích website của bạn.
window.addEventListener('beforeinstallprompt', e => { console.log('beforeinstallprompt Event fired'); e.preventDefault(); // Stash the event so it can be triggered later. this.deferredPrompt = e; return false; }); // When you want to trigger prompt: this.deferredPrompt.prompt(); this.deferredPrompt.userChoice.then(choice => { console.log(choice); }); this.deferredPrompt = null; |
Tối ưuu hóa ứng dụng với Lighroom
Sau khi thực hiện xong ứng dụng PWA, việc tiếp theo bạn cần triển khai là tối ưu hóa lại PWA đó. Nói không quá thì Google lầ ông trùm trong việc thúc đẩy quá trình chuyển đổi Progressive Web Apps bằng cách đưa nó vào các tiêu chí đánh giá xếp hạng trong SEO – chơi thế thì ông nào cũng phải chạy theo cả :D
Công cụ để tối ưu hóa được Google khuyến khích và đưa vào Chrome là Lighthouse chắc nhiều người đã biết rồi nên mình sẽ không hướng dẫn.
Dưới đây là danh sách một số tiêu chí liên quan đến PWA mà bạn phải đạt được nếu bạn muốn website của mình đạt điểm 100.
- Registers a Service Worker
- Responds with a 200 when offline (cái này hình như mới bỏ rồi)
- Contains some content when JavaScript is not available
- Uses HTTPS
- Redirects HTTP traffic to HTTPS
- Page load is fast enough on 3G
- User can be prompted to install the Web App
- Configured for a custom splash screen
- Address bar matches brand colours
- Has a tag with width or initial-scale
- Content is sized correctly for the viewport
Mục nào mà bạn chưa đạt thì bạn có thể ấn vào để đọc và xem hướng dẫn cụ thể làm thế nào để đạt được điểm này. Ở đây mình không hướng dẫn cụ thể nhé.
Còn đây là thành quả của mình:
Cơm thêm
Mình chia sẻ đoạn mã xây dựng service worker của mình để bạn nào cần có thể sử dụng. Ở đây có một số dịch vụ mình tắt nên bạn đừng ý kiến là sao mình không làm như hướng dẫn ở trên nhé. Mình chỉ dùng những cái gì mình cho là cần thiết và xử dụng chúng theo mục đích và ý đồ của mình thôi.
'use strict'; var cacheName = 'app-v3'; var urlsToCache = ['manifest.json']; self.addEventListener('fetch', function (event) {}); self.addEventListener('install', function (ev) { ev.waitUntil(caches.open(cacheName).then(function (cache) { cache.addAll(urlsToCache.map(function (el) { return el; })); })); }); self.addEventListener('activate', function (ev) { ev.waitUntil(caches.keys().then(function (keyList) { keyList.forEach(function (key) { if (key !== cacheName) caches.delete(key); }); })); return self.clients.claim(); }); self.addEventListener('push', function (event) { var data = event.data ? event.data.json().data : {}; var title = data.title; if (!data || !title) return; var options = JSON.parse(data.options); event.waitUntil(self.registration.showNotification(title, options)); }); self.addEventListener('notificationclick', function (event) { event.notification.close(); var url = event.notification.data.url; event.waitUntil(clients.matchAll({ type: 'window' }).then(function (windowClients) { for (var i = 0; i < windowClients.length; i++) { var client = windowClients[i]; if (client.url === url && 'focus' in client) { return client.focus(); } } if (clients.openWindow) { return clients.openWindow(url); } })); }); |
Chúc các bạn thành công !