PWA

Progressive Web Apps (PWAs)

2020/12/29
2021/08/01 (新增內容)
2021/12/14 (更新內容)

簡介

越來越多的公司希望能讓現在的web應用程式能直接用在App上,首先,要採用Responsive Web Design (RWD)的概念開發web應用程式,讓web應用程式可以依據螢幕的大小進行調整。雖然這樣就可以在手機上使用web應用程式,可是還有一些問題:

  • 離線使用: 一般的手機App是安裝在手機上,即使網路斷線,還是可以使用,傳統的網頁是在伺服器上產生網頁內容,只要斷線就沒辦法看到網頁。

  • 使用手機上的資源: 過去因為手機App是安裝在手機上,所以,可以直接使用手機上的資源,網頁因為資訊安全的因素,無法取得手機上的資源,然而,最近web browser可以取得手機的資源越來越豐富 (參考: What Web Can Do Today? ),可以從你的手機打開這個網頁,就可以知道你的手機上目前所使用的瀏覽器可以支援那些功能。

設定

使用react的好處之一是可以透過create-react-app產生一個PWA

First React ,我們建議大家使用pwa的樣板

npx create-react-app . --use-npm --template cra-template-pwa

如果要將既有專案轉成PWA專案:

create-react-app的template比較完整,如果一開始的時候沒有使用pwa的樣板,可以到這裡下載所需要的檔案:

create-react-app會提供PWA的樣板,所以,在index.js裡會有:

serviceWorkerRegistration.unregister();

src/index.js

import React from 'react';

import ReactDOM from 'react-dom';

import './index.css';

import App from './App';

import * as serviceWorkerRegistration from './serviceWorkerRegistration';

import reportWebVitals from './reportWebVitals';


ReactDOM.render(

<React.StrictMode>

<App />

</React.StrictMode>,

document.getElementById('root')

);


// If you want your app to work offline and load faster, you can change

// unregister() to register() below. Note this comes with some pitfalls.

// Learn more about service workers: https://cra.link/PWA

serviceWorkerRegistration.unregister();


// If you want to start measuring performance in your app, pass a function

// to log results (for example: reportWebVitals(console.log))

// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals

reportWebVitals();

要啟動PWA,只要把這一行改為:

serviceWorkerRegistration.register();

如果沒有這一行,就麻煩加上這一行,並且,記得要import。

import * as serviceWorkerRegistration from './serviceWorkerRegistration';

//不是 import serviceWorkerRegistration from './serviceWorkerRegistration';




serviceWorkerRegistration.register();

並到cra-template-pwa下載

  • serviceWorkerRegistration.js

  • service-worker.js

並且放在src下。

這樣就已經可以讓我們的app可以離線了!

不過,為什麼老師不是第一週就請大家改這一行? 因為,改了之後,會增加開發及除錯的困擾,因為,瀏覽器會將內容cache,程式改好之後的結果不會馬上可以看到,還必須先清除瀏覽器上的cache。

來介紹一下PWA的一些基本要件,首先,要有個manifest.json,create-react-app已經幫我們產生了一個manifest.json,manifest.json的用途在於當我們把網頁加到手機的主畫面時,所應該使用的名稱、ICON及其他設定。(詳參: Add a web app manifest )

public/manifest.json

{

"short_name": "React App",

"name": "Create React App Sample",

"icons": [

{

"src": "favicon.ico",

"sizes": "64x64 32x32 24x24 16x16",

"type": "image/x-icon"

},

{

"src": "logo192.png",

"type": "image/png",

"sizes": "192x192"

},

{

"src": "logo512.png",

"type": "image/png",

"sizes": "512x512"

}

],

"start_url": ".",

"display": "standalone",

"theme_color": "#000000",

"background_color": "#ffffff"

}

除此之外,create-react-app也幫我們產生了一些所需要的js檔,如: src/service-worker.js、src/serviceWorkerRegistration.js

啟動PWA

打開chrome,開啟「開發人員工具」,點選「Application」,可以看到在「Manifest」下可以看到manifest.json裡的設定。在「Service Worker」下,會看不到任何service worker正常執行中。

因為service worker必須在production環境下,所以,接下來,必須build我們的react專案

npm run build

build完,先安裝serve

npm install -g serve

就可以執行build的結果了

serve -s build

注意,執行的port從原本的3000改成5000。

打開chrome,可以發現多了一個「安裝Create React App Sample」(如果只是執行 npm run start,是無法安裝的)。

開啟「開發人員工具」,點選「Application」,可以看到在「Manifest」下可以看到manifest.json裡的設定。在Service Worker」下,也看到service worker正常執行中。

如果service worker無法執行,請檢查一下package.json,暫時把homepage改回「.」,因為我們現在是佈署在localhost:

{

"name": "first-class",

"homepage": ".",

部署到github

也可以將pwa部署到github pages,部署到github pages的好處是github提供https,這時候,就要將homepage改為github page的網址:

{

"name": "first-class",

"homepage": "https://jitsungwu.github.io/mobile/",

部署時,可以注意到也會先進行build,只是不是利用localhost:5000,而是在github上執行。

執行

npm run deploy

事實上,執行deploy會自動執行build,所以,不需要先執行build。

現在,你終於可以利用手機,輸入網址,就可以看到我們的App,還可以把App新增到手機的主畫面!! 接下來,試試上上週的功能,上傳一張手機裡的照片。

firestore

firestore提供了離線讀取資料的功能,只要啟動了PWA並且使用onSnapshot,firestore就會自動啟動離線讀取資料。如果想知道現在的資料是從cache讀取或從server讀取,可以加上:

var source = querySnapshot.metadata.fromCache ? "local cache" : "server";

console.log("Data came from " + source);

完整程式區塊:

db.collection("product").orderBy("price", "desc").onSnapshot((querySnapshot)=>{

var source = querySnapshot.metadata.fromCache ? "local cache" : "server";

console.log("Data came from " + source);

querySnapshot.forEach((doc)=>{

const newProduct = {

id:doc.id,

desc:doc.data().desc,

price:doc.data().price

}

newProducts.push(newProduct);

console.log(doc.data().desc);

}

)

setProducts(newProducts);

setIsLoading(false);

});

** 還沒弄清楚為何get()無法使用 **

const querySnapshot = await db.collection("product").orderBy("price", "desc").get();

常見問題

在iOS上需要注意什麼?

參考資料