Chỉ cần vài config setup đơn giản là anh em có thể biến React web app của mình có thể hỗ trợ PWA cho điện thoại thông minh rồi :)

Vào một ngày đẹp trời bỗng nhiên sếp giao task cho anh em bảo nghiên cứu convert dự án React hiện tại để nó support PWA. Thì kiểu gì việc đầu tiên anh em sẽ phải đi search ngay PWA là gì? tại sao sếp lại muốn support PWA nhỉ?


PWA (Progressive Web App) là một ứng dụng web tiên tiến được thiết kế để mang lại trải nghiệm tương tự ứng dụng di động trên các thiết bị di động và máy tính. Cụ thể thì PWA giúp người dùng lướt web trên trình duyệt mobile có thể cài đặt được web của bạn về máy như kiểu download trên Appstore (ios) hay Google Play Store (android), trong máy sẽ có shortcut của app giúp người dùng cảm thấy lướt web của bạn giống như là một native app.

Tại sao lại cần PWA cho web? Đơn giản có thể là vì dự án bạn chưa có kinh phí để thuê thêm 1 team dev mobile hoặc là chưa đến giai đoạn cần làm app mobile nên làm trước PWA cho web để release sản phẩm trước.


Để hiểu rõ hơn về PWA là gì, nó có những tính năng gì hay ho anh em có thể đọc và học thêm ở đây: Learn PWA (web.dev)


Trường hợp 1: Khởi tạo dự án từ đầu:

Nếu ngay từ đầu anh em đã biết yêu cầu là cần có PWA thì lúc build source code anh em chỉ cần dùng create-react-app với templates như ở đây: Making a Progressive Web App | Create React App (create-react-app.dev).


Trường hợp 2: Dự án được create bằng Vite:

Dự án create từ Vite kể cả setup từ đầu hay là dự án đã có hiện tại thì ngoài create-react-app có sẵn template hỗ trợ setup luôn có PWA ra thì Vite ở thời điểm hiện tại đang không có template sẵn hỗ trợ PWA mà phải tự config thêm bằng tay.


Anh em sẽ cài thêm plugin này trước:

npm i vite-plugin-pwa -D

Sau đó sửa file vite.config.js / vite.config.ts và thêm vite-plugin-pwa vừa cài ở trên vào:

import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
  plugins: [
    VitePWA({ registerType: 'autoUpdate' })
  ]
})

Giờ anh em cần run lệnh để build app sau đó plugin trên sẽ tự generate cho anh em các file cần thiết như service worker, web app manifest. Sau đó anh em chỉ cần sửa lại các thông tin trong file manifest.json như icon, tên app,... là xong rồi.

{
  "name": "Name of your app",
  "short_name": "Name of your app",
  "icons": [
    {
      "src": "/maskable_icon.png",
      "sizes": "200x200",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/android-chrome-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-256x256.png",
      "sizes": "256x256",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-384x384.png",
      "sizes": "384x384",
      "type": "image/png"
    }
  ],
  "theme_color": "#eab308",
  "background_color": "#eab308",
  "display": "standalone",
  "scope": "/",
  "start_url": "sharetolearn.vercel.app"
}


Trường hợp 3: Dự án được create bằng Create-React-App:

Đa phần anh em sẽ đang ở trường hợp này, với dự án cũ trước đây dùng create-react-app nhưng chưa có PWA thì chúng ta phải config app thế nào?

Không giống như Vite ở trên, creat-react-app không có plugin ngon như vite ở trên để có thể generate sẵn ra các file cần thiết cho PWA như services worker, web app manifest,...


Để dễ dàng nhất thì anh em có thể follow các bước sau:

  1. Anh em tự tạo lại 1 project mới hoàn toàn bên ngoài bằng create-react-app nhưng với template có chức năng PWA rồi theo doc ở đây: Making a Progressive Web App | Create React App (create-react-app.dev) nếu dự án hiện tại dùng typescipt thì anh em thêm template typescript vào nhé.
  2. Copy file manifest (public/manifest.json) và các file liên quan (ví dụ như các icons trong file nhưng mình sẽ thay các icons đó bằng của mình) vào dự án hiện tại của anh em.
  3. Copy các files: service-worker.ts and serviceWorkerRegistration.ts vào dự án hiện tại.
  4. Register the service worker in your index.ts file: serviceWorkerRegistration.register();
  5. Copy tất cả các library liên quan đến workbox trong file package.json sang dự án hiện tại


Sau đó anh em install các package rồi run dự án lên là xong rồi.


Lỗi có thể gặp:

Có thể anh em sẽ gặp phải 1 vấn đề khi dự án được deploy lên môi trường production sau mỗi lần build mới là “Uncaught SyntaxError: Unexpected token <”:

Do PWA thì nó có feature offline mode nó sẽ cache các file bao gồm cả index.html để khi không có mạng thì vẫn vào được với trường hợp các app data là tĩnh thường như các trang landing page. Mỗi lần mình build mới các file js đặc biệt file main.abcxyz.js thì abcxyz nó sẽ thay đổi, mà web nó cache file root html cũ rồi nên nó không trỏ được vào file js mới.

Giải pháp là dùng tính năng NetworkFirst của workbox tức là app sẽ cố download file index.html mới nhất về trước bằng network, nếu trường hợp offline thì nó mới dùng các file cache.

Anh em có thể sửa file service-worker.ts thêm NetworkFirst như bên dưới:

import { NetworkFirst } from 'workbox-strategies'

// precacheAndRoute(self.__WB_MANIFEST)

const toPrecache = self.__WB_MANIFEST.filter(
  (file) => !file.url.includes('index.html'),
)

precacheAndRoute(toPrecache)

registerRoute(
  ({ url }) => url.pathname.includes('index.html'),
  new NetworkFirst(),
)

Sau đó hãy xóa các đoạn code nơi mà các routes navigation đến index.html

// Set up App Shell-style routing, so that all navigation requests
// are fulfilled with your index.html shell. Learn more at
// https://developers.google.com/web/fundamentals/architecture/app-shell
// const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$')
// registerRoute(
//   // Return false to exempt requests from being fulfilled by index.html.
//   ({ request, url }) => {
//     // If this isn't a navigation, skip.
//     if (request.mode !== 'navigate') {
//       return false
//     } // If this is a URL that starts with /_, skip.

//     if (url.pathname.startsWith('/_')) {
//       return false
//     } // If this looks like a URL for a resource, because it contains // a file extension, skip.

//     if (url.pathname.match(fileExtensionRegexp)) {
//       return false
//     } // Return true to signal that we want to use the handler.

//     return true
//   },
//   createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html', true),
// )


Tổng kết

Như vậy là việc convert dự án hiện tại để support tính năng PWA cho mobile cũng không hề khó khăn lắm, hy vọng bài viết có thể giúp ích được cho anh em!

Nếu có bất cứ ý kiến gì mời anh em comment bên dưới nhé <3