PWA

Progressive Web Application

2020/03/27

2020/03/28 (補充內容)

2020/03/30 (更新內容)

簡介

Progressive Web Application (PWA)就是要讓網頁的行為,在手機上執行時,能夠跟手機App一致,在介面呈現的部分,已經可以透過RWD來處理,還有一些行為,如:安裝、可離線操作、存取手機上的資源,跟手機是不一樣的,透過PWA,可以進行安裝的動作,也可以進行離線操作,目前,已經可以透過Browser存取一些手機上的資源,未來會慢慢的能夠跟App可取得的權限幾乎一樣。

體驗一下PWA,直接用手機打開網頁:

存取手機上的資源

現在可以利用browser去存取手機上的資源,例如,可以取得目前的經緯度,先在html中加一個按鈕,並留一個顯示位置的區域,最後,引用geoFindMe.js。。

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>PWA Demo</title>
</head>
<body>
  <button id="askButton" onclick="geoFindMe()">顯示經緯度</button>
  <div id="target"></div>
  <script src="./js/geoFindMe.js"></script>
</body>
</html>

先利用navigator.geolocation,確認瀏覽器是否支援。

    if (!navigator.geolocation){
      output.innerHTML = "<p>Geolocation is not supported by your browser</p>";
      return;
    }

利用navigator.geolocation.getCurrentPosition取得位置,當成功時,呼叫success,失敗時呼叫error。(這兩個函數就是所謂的call back function)。

    navigator.geolocation.getCurrentPosition(success, error);

成功的時候,將緯度與經度顯示在網頁上:

    function success(position) {
      var latitude  = position.coords.latitude;
      var longitude = position.coords.longitude;

      output.innerHTML = '<p>緯度:' + latitude + '<br>經度: ' + longitude + '</p>';
    };

失敗的時候,

    function error() {
      output.innerHTML = "未授權,無法取得位置";
    };

完整的geoFindMe.js

function geoFindMe() {
    var output = document.getElementById("target");

    if (!navigator.geolocation){
      output.innerHTML = "<p>Geolocation is not supported by your browser</p>";
      return;
    }

    function success(position) {

      var latitude  = position.coords.latitude;
      var longitude = position.coords.longitude;

      var today = new Date();
      var date = today.getFullYear()+'-'+(today.getMonth()+1)+'-'+today.getDate();
      var time = today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds();
      var dateTime = date+' '+time;
      output.innerHTML = '<p>緯度:' + latitude + '<br>經度:' + longitude + '</p>';

    };

    function error() {
      output.innerHTML = "未授權,無法取得位置";
    };

    output.innerHTML = "<p>定位中...</p>";

    navigator.geolocation.getCurrentPosition(success, error);
  }

現在可以利用browser去存取手機上的資源,例如,可以取得目前的經緯度,先在html中加一個按鈕,並留一個顯示位置的區域,最後,引用location.js。

如果希望能一直更新目前的經緯度,就改用watchPosition,如果都在原地是看不出來有在更新,所以,加個時間,就可以知道其實是有在更新 (可能要等個幾分鐘):

function geoFindMe() {
    var output = document.getElementById("target");
    var watchid;

    if (!navigator.geolocation){
      output.innerHTML = "<p>Geolocation is not supported by your browser</p>";
      return;
    }

    function success(position) {

      var latitude  = position.coords.latitude;
      var longitude = position.coords.longitude;

      var today = new Date();
      var date = today.getFullYear()+'-'+(today.getMonth()+1)+'-'+today.getDate();
      var time = today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds();
      var dateTime = date+' '+time;
      output.innerHTML = '<p>緯度:' + latitude + '<br>經度:' + longitude + '<br>'
       + dateTime + '</p>';

    };

    function error() {
      output.innerHTML = "未授權,無法取得位置";
    };

    output.innerHTML = "<p>定位中...</p>";

    watchid = navigator.geolocation.watchPosition(success, error);
  }


參考資料

試試看,按了按鈕之後,就可以取得手機的經緯度了,並且會持續更新經緯度。利用watchPosition可以一直追蹤位置(如果正在移動中),並呼叫appendLocation。

可以利用手機試試看,當網頁要求授權「存取您的位置資訊」時,要選擇「允許」,否則就會顯示「Unable to retrieve your location」。

**注意** 在chrome下,如果存取的網站不是localhost,也不是採用https,會被直接拒絕,不會要求授權「存取您的位置資訊」,可使用其他的瀏覽器試試看。

**注意** 如果使用手機,不能連localhost,因為那只會連到手機,必須要透過電腦的IP來連線,很多同學的電腦是連家裡的網路,如果利用ipconfig查出來的ip是196.168.X.X,那就表示,這組IP是內網的IP,所以,手機也要連上家裡的網路(而不是利用4G),才連得上 (詳參: 何謂『虛擬 IP 』,與『實體 IP 』或者『固定 IP 』『動態 IP 』有啥不同?[教學]IP 位址的基本介紹)。

以下是另一種寫法,主要差別是利用addEventListener,也省略了錯誤處理的部分:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>PWA Demo</title>
</head>
<body>
    <p> hi! This's PWA Demo. </p>
    <button id="askButton">Ask for location</button>
    <div id="target"></div>
    <script src="./js/location.js"></script>
</body>
</html>

在location.js中,利用getCurrentPosition,並且將取得的位置(location)傳給appendLocation處理。

var target = document.getElementById('target');
var watchId;

function appendLocation(location, verb) {
  verb = verb || 'updated';
  var newLocation = document.createElement('p');
  newLocation.innerHTML = 'Location ' + verb + ': ' + location.coords.latitude + ', ' + location.coords.longitude + '';
  target.appendChild(newLocation);
}

if ('geolocation' in navigator) {
  document.getElementById('askButton').addEventListener('click', function () {
    navigator.geolocation.getCurrentPosition(function (location) {
      appendLocation(location, 'fetched');
    });
    watchId = navigator.geolocation.watchPosition(appendLocation);
  });
} else {
  target.innerText = 'Geolocation API not supported.';
}

參考資料

安裝

首先,你在你的AppServ的www資料夾下,產生一個新的資料夾 (如:test-pwa),並確認你的AppServ可以執行。

第二,在test-pwa資料夾下新增一個manifest.json,並且網路上下載一個(或多個不同大小的icon)。

**如果看到 Manifest does not contain a suitable icon - PNG format of at least 144px is required, the sizes attribute must be set, and the purpose attribute, if set, must include "any" or "maskable",可能的問題是下載的ICON格式有問題,另外,一定要有一個是大於144X144的icon。

{
    "short_name": "Ben",
    "name": "Ben Wu",
    "icons": [
        {
          "src": "icons/bookmark-ribbon.png",
          "sizes": "96x96"
        },
        {
            "src": "icons/fruit.png",
            "sizes": "512x512"
          }
      ],
    "start_url": "http://localhost/test-pwa/",
    "background_color": "#000",
    "theme_color": "#536878",
    "display": "standalone"
}

關於manifest.json的解釋

再開啟網頁,記得在head標籤中引用manifest 。

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <link rel="manifest" href="./manifest.json">
    <meta charset="UTF-8">
    <title>PWA Demo</title>
</head>
<body>
    <p> hi! This's PWA Demo. </p>

</body>
</html>

這時開啟網頁的「開發人員工具」,並切到「Application」頁籤,就會看到「Manifest」選項,就會到剛所設定應用程式的名稱、圖示與URL。這時候會看到因為目前沒有service worker,所以,還不能進行安裝。

可以利用Android手機連上你的網頁,打開網頁之後,可以在設定(...)中在「加到」項下看到「加到首頁」,可以點選「加到主畫面」或在「加到」可以選擇「桌面」,在手機的桌面上就可以看到了,也可以看到剛剛設定的icon。使用iPhone的話,希望在直接把網頁加入主畫面後,要能看到icon,請加入以下的link (詳參: PWA 實戰經驗分享PWA icons are not used in iOS 11.3)。

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <link
    rel='apple-touch-icon'
    href='./icons/fruit.png'
    sizes='512x512'
  />
  <link rel="manifest" href="./manifest.json">
  <meta charset="UTF-8">
  <title>PWA Demo</title>
</head>
<body>
    <p> hi! This's PWA Demo. </p>
</body>
</html>

**注意** 如果使用手機,不能連localhost,因為那只會連到手機,必須要透過電腦的IP來連線,很多同學的電腦是連家裡的網路,如果利用ipconfig查出來的ip是196.168.X.X,那就表示,這組IP是內網的IP,所以,手機也要連上家裡的網路(而不是利用4G),才連得上 (詳參: 何謂『虛擬 IP 』,與『實體 IP 』或者『固定 IP 』『動態 IP 』有啥不同?[教學]IP 位址的基本介紹)。

還有很多內容 (例如自動加到桌面....),未完待續....

離線操作

PWA要能夠可以離線操作,要靠serviceWorker進行快取。接下來在index.html先加入javascript,啟用serviceWorker:


<!DOCTYPE html>
<html lang="en">
<head>
  <link
    rel='apple-touch-icon'
    href='./icons/fruit.png'
    sizes='512x512'
  />
  <link rel="manifest" href="./manifest.json">
  <meta charset="UTF-8">
  <title>PWA Demo</title>
</head>
<body>
    <p> hi! This's PWA Demo. </p>
    <script>
        if ('serviceWorker' in navigator) {
          console.log("Will service worker register?");
          navigator.serviceWorker.register("./service-worker.js")
          .then(function(reg) {
            console.log("Yes it did.");
          }).catch(function(err) {
            console.log("No it didn't", err)
        });
        }
    </script>
</body>
</html>

service-worker.js:

// install
self.addEventListener('install', event => {
    console.log('installing…');
});

// activate
self.addEventListener('activate', event => {
    console.log('now ready to handle fetches!');
});

// fetch
self.addEventListener('fetch', event => {
    console.log('now fetch!');
});

都完成後,再點Chrome右上的選單圖示,在「更多工具...」上面就可看到「安裝Ben Wu」。

點一下,就會出現安裝畫面,再點「安裝」。當安裝完畢後,就可以在桌面看到安裝的應用程式了。也可以利用Android手機連上你的網頁,打開網頁之後,可以在設定(...)中在「加到」項下看到「首頁」,可以點選「加到首頁」,在桌面上就可以看到了,也可以看到剛剛設定的icon。使用iPhone的話,希望在直接把網頁加入主畫面後,要能看到icon,請加入以下的link (詳參: PWA 實戰經驗分享PWA icons are not used in iOS 11.3)。

這時開啟網頁的「開發人員工具」,並切到「Application」頁籤,在「Application」下會看到「Service Workers」選項,就會到剛註冊的service-worker.js。如果在Status下標註「#xxxx is activated and running」,那就表示service-worker已經成功的執行了。在下面的Console,可以看到在index.html及service-worker.js所有console.log的內容了。

install及activate下的訊息只會在第一次註冊的時候出現,之後,當我們reload時,因為已經安裝也啟動了,所以,只會觸發fetch。

接下來,我們會使用到google提供的Workbox,在service-worker.js裡利用importScripts匯入workbox-sw.js。如果成功的載入,會出現「Yay! Workbox is loaded 🎉」,記得要把前一次的service worker停掉,否則新的service worker會被卡住(waiting to activate),這時候,要把先將把前一次的service worker停掉 ,按「unregister」,再重整一次瀏覽器,就可以了。每次只要改了service-worker.js就要重複這個動作。

service-worker.js:

importScripts('https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js');

if (workbox) {
  console.log(`Yay! Workbox is loaded 🎉`);
} else {
  console.log(`Boo! Workbox didn't load 😬`);
}
// install
self.addEventListener('install', event => {
    console.log('installing…');
});

// activate
self.addEventListener('activate', event => {
    console.log('now ready to handle fetches!');
});

// fetch
self.addEventListener('fetch', event => {
    console.log('now fetch!');
});

我們來試試看,將index.html放到cache。

service-worker.js:

importScripts('https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js');

// 使用precache功能,在offline下也可以執行
// 要存進cache storage裡的檔案清單
var cacheFiles = [
    {
      url: './index.html',
      revision: '00000001' // 加revision,版本改了以後,sw.js 在 application 上會更新
    }
  ];
  workbox.precaching.precacheAndRoute(cacheFiles);

這時開啟網頁的「開發人員工具」,並切到「Application」頁籤,在「Cache」下就會看到「Cache Storage」選項,就會到剛剛存進cache storage的index.html。

我們試試看是不是真的有用,我們可以把AppServ停掉,reload這個頁面,會發現這個頁面不會出現「localhost 拒絕連線」,但是,如果是連到http://localhost,是會出現「localhost 拒絕連線」。因為我們成功的把頁面放到cache storage裡了!

但是,因為index.html用到manifest.json以及圖片,所以,會出現「Precaching did not find a match for /test-pwa/icons/fruit.png」、「Precaching did not find a match for /test-pwa/icons/fruit.png」,解決的方法就是把這兩個檔案也存進cache storage裡。

importScripts('https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js');

// 使用precache功能,在offline下也可以執行
// 要存進cache storage裡的檔案清單
var cacheFiles = [
    'icons/fruit.png',
    'manifest.json',
    {
      url: './index.html',
      revision: '00000001' // 加revision,版本改了以後,sw.js 在 application 上會更新
    }
  ];
  workbox.precaching.precacheAndRoute(cacheFiles);

好,啟動App Serv,這時候,這兩個檔案就會進入cache storage了。這時候會看到「Workbox is precaching URLs without revision info: icons/fruit.png, manifest.json This is generally NOT safe.」,這就是為什麼我們需要revision了。

service-worker.js:

importScripts('https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js');

// 使用precache功能,在offline下也可以執行
// 要存進cache storage裡的檔案清單
var cacheFiles = [
    { url: 'icons/fruit.png', revision: null},
    { url: 'manifest.json', revision: null},
    {
      url: './index.html',
      revision: '00000001' // 加revision,版本改了以後,sw.js 在 application 上會更新
    }
  ];
  workbox.precaching.precacheAndRoute(cacheFiles);

記得,當內容修改之後,要改這邊的版本,例如,我們改了manifest.json,否則,會以為內容沒有更動。

importScripts('https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js');

// 使用precache功能,在offline下也可以執行
// 要存進cache storage裡的檔案清單
var cacheFiles = [
    { url: 'icons/fruit.png', revision: null},
    { url: 'manifest.json', revision: '000000001'},
    {
      url: './index.html',
      revision: '00000001' // 加revision,版本改了以後,sw.js 在 application 上會更新
    }
  ];
  workbox.precaching.precacheAndRoute(cacheFiles);

可是,這樣不是很累,而且,很有可能會弄錯嗎? WorkBox CLI就是來幫忙解決這個問題。

剛開始喔,未完待續....


參考資料

存取手機上的資源

安裝

manifest.json

離線操作