Progressive Web Apps là gì ?

Progressive Web Apps (PWA) là một loại ứng dụng web có khả năng cung cấp trải nghiệm người dùng giống như ứng dụng di động native, nhưng chúng có thể chạy trên mọi nền tảng và trình duyệt web.

Điều quan trọng là PWA được thiết kế để hoạt động tốt trên mọi thiết bị, có hoặc không có kết nối internet.

Các đặc điểm chính bao gồm:

  • Responsive: Tương thích trên nhiều loại thiết bị, từ điện thoại di động đến máy tính bảng và máy tính.
  • Connectivity-Independent: Trải nghiệm ổn định và có thể hoạt động offline.
  • App-Like: Tạo cảm giác giống như ứng dụng native với các chức năng như icon trên màn hình chính, push notifications, và trải nghiệm người dùng mượt mà.

Đặc điểm quan trọng của PWA

Service Workers

Service Workers là một loại mã JavaScript chạy ở nền tảng của trình duyệt và có khả năng thực hiện các tác vụ không liên quan trực tiếp đến trang web mà chúng phục vụ.

Nó có 3 vai trò chính:

  • Cache và Lưu Trữ Tài Nguyên: Service Workers có khả năng lưu trữ các tài nguyên như HTML, CSS, JavaScript, hình ảnh và dữ liệu khác trong bộ nhớ cache.
  • Fetch và Intercepting Requests:  Service Workers có thể chặn và xử lý các yêu cầu bằng cách sử dụng sự kiện fetch. Khi trang web gửi request server, Service Workers có thể kiểm tra cache để trả về data từ cache khi có sẵn.
  • Cập Nhật Tài Nguyên Một Cách Linh Hoạt: Service Workers cho phép trang web tự động cập nhật tài nguyên mà không cần sự tương tác từ người dùng. Nó có thể cập nhật bộ nhớ cache để đảm bảo rằng người dùng nhận được phiên bản mới nhất khi truy cập offline.

Web App Manifest

Web App Manifest là một tệp JSON đặc biệt chứa các thông tin về ứng dụng web, bao gồm cả cấu hình và metadata.

  • Web App Manifest là một yếu tố quan trọng của PWA vì nó định nghĩa cách ứng dụng web sẽ hoạt động trên thiết bị di động và cung cấp trải nghiệm như ứng dụng native.
  • Nó làm cho ứng dụng web trở nên "installable" và có thể được truy cập một cách thuận tiện từ màn hình chính của thiết bị.

Lighthouse và Tiêu chí đánh giá PWA

Lighthouse là một công cụ từ Google cung cấp đánh giá về hiệu suất, tính khả dụng và các tiêu chí PWA khác. Có thể chạy Lighthouse trực tiếp trong trình duyệt Chrome hoặc sử dụng CLI để kiểm tra tự động.

Các tiêu chí bao gồm:

  • Performance chính là hiệu suất của website, cần dựa trên nhiều yếu tố như back-end và front-end.
  • Accessibility cho biết được liệu web của mình đã được tối ưu khả năng truy cập hay chưa?
  • Best Practices: Lighthouse phân tích xem HTTPS và HTTP / 2 có được sử dụng hay không, kiểm tra xem tài nguyên có đến từ các nguồn an toàn hay không và đánh giá lỗ hổng của các thư viện JavaScript.
  • SEO việc tối ưu hóa công cụ tìm kiếm mang lại tiềm năng rất lớn cho những cải tiến khác, điều này thực sự quan trọng.
  • Progressive Web App là một nhóm các kỹ thuật tạo ra trải nghiệm tốt hơn cho người dùng dựa trên nền tảng web

So sánh PWA và Native App

Tính năng

PWA

Native App

Installation

Download từ trang web

Cài đặt trên các App Store

Cross-Platform

Hỗ trợ đa nền tảng

Nền tảng cụ thể

Discovery

Tối ưu được với SEO

Tìm kiếm qua các App Store (ASO)

Offline Usage

Hỗ trợ

Hỗ trợ mạnh mẽ hơn

Push Notifications

Hỗ trợ

Hỗ trợ

Home Screen Access

Hỗ trợ

Hỗ trợ

Device Features

Bị hạn chế quyền khi truy cập các tính năng trên thiết bị

Có nhiều quyền truy cập tới các tính năng trên thiết bị

Cost

Tiết kiệm chi phí

Tốn nhiều nguồn lực hơn

Security

Sử dụng HTTPS, HTTP/2

Ủy quyền qua GG và Apple nên là nguồn đáng tin cậy

Updates

Tự động update khi có mạng

Update thủ công

Tại sao nên chọn PWA

  1. Tính Linh Hoạt và Đa Nền Tảng
    PWA có khả năng chạy trên mọi nền tảng và trình duyệt, giảm công sức phát triển và duy trì cho nhiều phiên bản ứng dụng.
    Tính linh hoạt giúp PWA tự động điều chỉnh giao diện cho mọi loại thiết bị, từ điện thoại đến máy tính bảng.
  2. Hiệu Suất so với Ứng Dụng Native
    PWA thường có hiệu suất tốt, đặc biệt là trong các điều kiện mạng không đáng tin cậy, nhờ vào việc sử dụng Service Workers để quản lý bộ nhớ cache và kiểm soát việc tải lại dữ liệu.
  3. Chi Phí Phát Triển và Duy Trì
    PWA giảm chi phí phát triển vì có thể chia sẻ mã nguồn và không yêu cầu quy trình xét duyệt từ các cửa hàng ứng dụng.

Tích hợp PWA vào trang web của mình thôi nào!

Prerequisites

Chuẩn bị 2 files

  • manifest.json (file mô tả thông tin chính về ứng dụng của bạn)
{
	"name": "Progressive Web App",
	"short_name": "Demo PWA",
	"description": "Demo for progressive web app with push notifications, background sync etc.",
	"start_url": "./index.html?utm=homescreen",
	"display": "standalone",
	"orientation": "portrait",
	"background_color": "#f5f5f5",
	"theme_color": "#29434d",
	"icons": [
                {
                    "src": "./images/icons/android-chrome-192x192.png",
                    "type": "image/png",
                    "sizes": "192x192"
                },
                    {
                    "src": "./images/icons/android-chrome-512x512.png",
                    "type": "image/png",
                    "sizes": "512x512"
                },
                {
                    "src": "./images/icons/android-maskable-192x192.png",
                    "type": "image/png",
                    "sizes": "192x192",
                    "purpose": "maskable"
                },
                {	
                    "src": "./images/icons/android-maskable-512x512.png",
                    "type": "image/png",
                    "sizes": "512x512",
                    "purpose": "maskable"
                }
	],
	"author": {
		"name": "Gokulakrishnan Kalaikovan",
		"website": "https://gokulkrishh.github.io",
		"github": "https://github.com/gokulkrishh",
		"source-repo": "https://github.com/gokulkrishh/demo-progressive-web-app"
	},
	"gcm_sender_id": "847244712742",
	"gcm_user_visible_only": true
}
  • serviceWorker.js
//Cache polyfil to support cacheAPI in all browsers
importScripts('./cache-polyfill.js');

var cacheName = 'cache-v4';

//Files to save in cache
var files = [
  './',
  './index.html?utm=homescreen', //SW treats query string as new request
  'https://fonts.googleapis.com/css?family=Roboto:200,300,400,500,700', //caching 3rd party content
  './css/styles.css',
  './images/icons/android-chrome-192x192.png',
  './images/push-on.png',
  './images/push-off.png',
  './images/icons/favicon-16x16.png',
  './images/icons/favicon-32x32.png',
  './js/main.js',
  './js/app.js',
  './js/offline.js',
  './js/push.js',
  './js/sync.js',
  './js/toast.js',
  './js/share.js',
  './js/menu.js',
  './manifest.json'
];

//Adding `install` event listener
self.addEventListener('install', (event) => {
  console.info('Event: Install');

  event.waitUntil(
    caches.open(cacheName)
    .then((cache) => {
      //[] of files to cache & if any of the file not present `addAll` will fail
      return cache.addAll(files)
      .then(() => {
        console.info('All files are cached');
        return self.skipWaiting(); //To forces the waiting service worker to become the active service worker
      })
      .catch((error) =>  {
        console.error('Failed to cache', error);
      })
    })
  );
});
// Define other events...

Convert web sang PWA

Bạn cần nhúng manifest.jsonserviceWorker.js vào index.html  như sau

<!-- index.html -->

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="utf-8" />
		<title>Demo Progressive Web Application</title>
		<!-- Headers  -->
		<link rel="manifest" href="./manifest.json" />
	</head>

	<body>
		<!-- Body content  -->
			
		<!-- JS Files  -->
		<script src="./js/main.js"></script>
	</body>
</html>
// main.js

(function () {
  //If serviceWorker supports, then register it.
  if ("serviceWorker" in navigator) {
    navigator.serviceWorker.register('./serviceWorker.js', { scope: "./" }) //setting scope of sw
    .then(function(registration) {
      console.info('Service worker is registered!');
      checkForPageUpdate(registration); // To check if new content is updated or not

      // request permission for push notifications
      Notification.requestPermission(function(result) {
        if (result !== 'granted') {
          console.warn('No notification permission granted!');
        }
      });

    })
    .catch(function(error) {
      console.error('Service worker failed ', error);
    });
  }

  // To content update on service worker state change
  function checkForPageUpdate(registration) {
    // onupdatefound will fire on first time install and when serviceWorker.js file changes      
    registration.addEventListener("updatefound", function() {
      // To check if service worker is already installed and controlling the page or not
      if (navigator.serviceWorker.controller) {
        var installingSW = registration.installing;
        installingSW.onstatechange = function() {
          console.info("Service Worker State :", installingSW.state);
          switch(installingSW.state) {
            case 'installed':
              // Now new contents will be added to cache and old contents will be remove so
              // this is perfect time to show user that page content is updated.
              toast('Site is updated. Refresh the page.', 5000);
              break;
            case 'redundant':
              throw new Error('The installing service worker became redundant.');
          }
        }
      }
    });
  }
})();

Tiếp theo chúng ta sẽ tạo 1 server bằng ExpressJS để gửi index.html và push notifications cho client

// server.js

'use strict';
require('dotenv').config();
var express = require('express');
var bodyParser = require('body-parser');
var app = express();
var webpush = require('web-push');

webpush.setVapidDetails(
  process.env.MAIL_TO,
  process.env.PUBLIC_VAPID_KEY,
  process.env.PRIVATE_VAPID_KEY,
);

var payloads = require('./payloads');

//Here we are configuring express to use body-parser as middle-ware.
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

//To server static assests in root dir
app.use(express.static(__dirname));

//To allow cross origin request
app.use(function(req, res, next) {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
  next();
});

//To server index.html page
app.get('/', function (req, res) {
  res.sendFile(__dirname + '/index.html');
});

//To send public key to client
app.get('/vapid_public_key', function (req, res) {
  res.send(process.env.PUBLIC_VAPID_KEY);
});

//To receive push request from client
app.post('/send_notification', function (req, res) {
  if (!req.body || !req.body.endpoint) {
    res.status(400).send('Invalid request');
    return;
  }
  
  const pushConfig = {
    endpoint: req.body.endpoint,
    keys: {
      auth: req.body.keys.auth,
      p256dh: req.body.keys.p256dh
    }
  };

  // get random payload
  let random = Math.floor(Math.random() * payloads.length);
  let payload = payloads[random];
  
  webpush.sendNotification(pushConfig, JSON.stringify(payload))
    .then(function() {
      res.status(200);

    })
    .catch(function(error) {
      console.error(error);
      res.status(500);
    });
});

app.listen(process.env.PORT || 3000, function() {
  console.log('Local Server : http://localhost:3000');
});

Lưu ý để nhận được Notification chúng ta cần sử dụng PUBLIC_VAPID_KEY đã được convert sang Uint8Array.

// push.js

...
//To subscribe `push notification`
      function subscribePush() {
        navigator.serviceWorker.ready.then(function (registration) {
          if (!registration.pushManager) {
            alert("Your browser doesn't support push notification.");
            return false;
          }

          //To subscribe `push notification` from push manager
          registration.pushManager
            .subscribe({
              userVisibleOnly: true, //Always show notification when received
              applicationServerKey: convertedVapidPublicKey, // Here is the converted key
            })
            .then(function (subscription) {
              toast("Subscribed successfully.");
              console.info("Push notification subscribed.");
              changePushStatus(true);
              sendPushNotification();
            })
            .catch(function (error) {
              changePushStatus(false);
              console.error("Push notification subscription error: ", error);
            });
        });
      }

Các tạo PUBLIC_VAPID_KEY   PRIVATE_VAPID_KEY

# if not install web-push
npm i -g web push

web-push generate-vapid-keys

=======================================

Public Key:
BIDyvhW5PBPfADl_Su7bw2NZxn4yYiU-vF7FdTAEBWKN62cXtAPJqJ-m2Al41Cfp5YCuL6qIJNipfOJGFEaw7Hk

Private Key:
hWiadmwjz082uvLDeJ7WnHJP4Bs1nyeewhUwdBqAnQA

=======================================

Cấu trúc dự án

Khởi động dự án

$ npm start

> demo-pwa@1.0.2 start
> npm run css && node server.js


> demo-pwa@1.0.2 css
> postcss -u autoprefixer -r css/*

Local Server : http://localhost:3000

Bấm nút như trên màn hình

Nếu thông báo này được hiển thị lên, thì việc convert web sang PWA đã thành công.

Bạn có thể trải nghiệm PWA tại đây: https://pwa-slxh.onrender.com/

Kết luận

PWA mang lại khả năng tương tác tốt hơn và liên kết với người dùng, đặc biệt là trong các ngữ cảnh đa thiết bị và đóng góp vào xu hướng phát triển phần mềm bằng cách cung cấp các tính năng và trải nghiệm đặc biệt..

Hy vọng rằng qua bài viết này về Progressive Web Apps (PWA), các bạn đã hiểu hơn về công nghệ này và có thể bắt đầu tìm hiểu những vấn đề sâu hơn về PWA để có thể nâng cấp các sản phẩm của mình lên một tầm cao mới.

📚Tài liệu tham khảo

🚀Github