改造你的網站,變身 PWA

MrDream24發表於2017-03-29

最近有很多關於 Progressive Web Apps(PWAs)的訊息,很多人都在問這是不是(移動)web 的未來。我不想陷入native app 和 PWA 的紛爭,但是有一件事是確定的 --- PWA極大的提升了移動端表現,改善了使用者體驗。

好訊息是開發一個 PWA 並不難。事實上,我們可以將現存的網站進行改進,使之成為PWA。這也是我這篇文章要講的 -- 當你讀完這篇文章,你可以將你的網站改進,讓他看起來就像是一個 native web app。他可以離線工作並且擁有自己的主屏圖示。

Progressive Web Apps 是什麼?

Progressive Web Apps (下文以“PWAs”代指) 是一個令人興奮的前端技術的革新。PWAs綜合了一系列技術使你的 web app表現得就像是 native mobile app。相比於純 web 解決方案和純 native 解決方案,PWAs對於開發者和使用者有以下優點:

  1. 你只需要基於開放的 W3C 標準的 web 開發技術來開發一個app。不需要多客戶端開發。

  2. 使用者可以在安裝前就體驗你的 app。

  3. 不需要通過 AppStore 下載 app。app 會自動升級不需要使用者升級。

  4. 使用者會受到‘安裝’的提示,點選安裝會增加一個圖示到使用者首屏。

  5. 被開啟時,PWA 會展示一個有吸引力的閃屏。

  6. chrome 提供了可選選項,可以使 PWA 得到全屏體驗。

  7. 必要的檔案會被本地快取,因此會比標準的web app 響應更快(也許也會比native app響應快)

  8. 安裝及其輕量 -- 或許會有幾百 kb 的快取資料。

  9. 網站的資料傳輸必須是 https 連線。

  10. PWAs 可以離線工作,並且在網路恢復時可以同步最新資料。

現在還處在 PWA 的早期,但已經有 很多成功案例

PWA 技術目前被 Firefox,Chrome 和其他基於Blink核心的瀏覽器支援。微軟正在努力在Edge瀏覽器上實現。Apple沒有動作 although there are promising comments in the WebKit five-year plan。幸運的是,瀏覽器支援對於 PWA 似乎不太重要...

PWAs 是漸進增強的

你的app仍然可以執行在不支援 PWA 技術的瀏覽器裡。使用者不能離線訪問,不過其他功能都像原來一樣沒有影響。綜合利弊得失,沒有理由不把你的 app 改進為 PWA。

不只是 Apps

Google 引領了 PWA 的一系列動作,所以大多數教程都在說如何從零開始構建一個基於 Chrome,native-looking mobile app。然而並不是只有特殊的單頁應用可以PWA化,也不需要一定遵循 material interface design guidelines。大多數網站都可以在數小時內實現 PWA 化。這包括你的 WordPress站點或者靜態站點。

示例程式碼

示例程式碼可以在github.com/sitepoint-e…找到。

程式碼提供了一個簡單的四個頁面的網站。其中包含一些圖片,一個樣式表和一個main javascript 檔案。這個網站可以執行在所有現代瀏覽器上(IE10+)。如果瀏覽器支援 PWA 技術,當離線時使用者可以瀏覽他們之前看過的頁面。

執行程式碼前,確保 Node.js 已經安裝,然後再命令列裡啟動服務:

node ./server.js [port]複製程式碼

[port]是可配置的,預設為 8888。開啟 Chrome 或者其他基於Blink核心的瀏覽器,比如 Opera 或者 Vivaldi,然後輸入連結 http://localhost:8888/(或者你指定的某個埠)。你也可以開啟開發者工具看一下各個console資訊。

改造你的網站,變身 PWA

瀏覽主頁,或者其他頁面,然後用以下任一方法使頁面離線:

  1. 按下 Cmd/Ctrl + C ,停止 node 伺服器,或者

  2. 在開發者工具的 Network 或者 Application - Service Workers 欄裡點選 offline 選項。

重新瀏覽任意之前瀏覽過的頁面,它們仍然可以瀏覽到。瀏覽一個之前沒有看過的頁面,你會看到一個專門的離線頁面,標識“you’re offline”,還有一個你可以瀏覽的頁面列表:

改造你的網站,變身 PWA

連線手機

你也可以通過 USB 連線你的安卓手機來預覽示例網頁。在開發者工具中開啟 Remote devices 選單。

改造你的網站,變身 PWA

在左邊選擇 Settings ,點選 Add Rule 輸入 8888 埠。你可以在你的手機上開啟Chrome,開啟 http://localhost:8888/

你可以點選瀏覽器選單裡的 “Add to Home screen”。瀏覽幾個頁面,瀏覽器會提醒你去安裝。這兩種方式都可以建立一個新的圖示在你的主屏上。瀏覽幾個頁面後關掉Chrome,斷開裝置連線。你依然可以開啟 PWA Website app -- 你會看到一個啟動頁,並且可以離線訪問之前你訪問過的頁面。

將你的網站改進為一個 Progressive Web App 總共有三個必要步驟:

第一步:開啟 HTTPS

由於一些顯而易見的原因,PWAs 需要 HTTPS 連線。

HTTPS 在示例程式碼中並不是必須的,因為 Chrome 允許使用 localhost 或者任何 127.x.x.x 的地址來測試。你也可以在 HTTP 連線下測試你的 PWA,你需要使用 Chrome ,並且輸入以下命令列引數:

  • --user-data-dir
  • --unsafety-treat-insecure-origin-as-secure

第二步:建立一個 Web App Manifest

manifest 檔案提供了一些我們網站的資訊,例如 name,description 和需要在主屏使用的圖示的圖片,啟動屏的圖片等。

manifest檔案是一個 JSON 格式的檔案,位於你專案的根目錄。它必須用Content-Type: application/manifest+json 或者 Content-Type: application/json這樣的 HTTP 頭來請求。這個檔案可以被命名為任何名字,在示例程式碼中他被命名為 /manifest.json:

{
  "name"              : "PWA Website",
  "short_name"        : "PWA",
  "description"       : "An example PWA website",
  "start_url"         : "/",
  "display"           : "standalone",
  "orientation"       : "any",
  "background_color"  : "#ACE",
  "theme_color"       : "#ACE",
  "icons": [
    {
      "src"           : "/images/logo/logo072.png",
      "sizes"         : "72x72",
      "type"          : "image/png"
    },
    {
      "src"           : "/images/logo/logo152.png",
      "sizes"         : "152x152",
      "type"          : "image/png"
    },
    {
      "src"           : "/images/logo/logo192.png",
      "sizes"         : "192x192",
      "type"          : "image/png"
    },
    {
      "src"           : "/images/logo/logo256.png",
      "sizes"         : "256x256",
      "type"          : "image/png"
    },
    {
      "src"           : "/images/logo/logo512.png",
      "sizes"         : "512x512",
      "type"          : "image/png"
    }
  ]
}複製程式碼

在頁面的<head>中引入:

<link rel="manifest" href="/manifest.json">複製程式碼

manifest 中主要屬性有:

  • name —— 網頁顯示給使用者的完整名稱
  • short_name —— 當空間不足以顯示全名時的網站縮寫名稱
  • description —— 關於網站的詳細描述
  • start_url —— 網頁的初始 相對 URL(比如 /
  • scope —— 導航範圍。比如,/app/的scope就限制 app 在這個資料夾裡。
  • background-color —— 啟動屏和瀏覽器的背景顏色
  • theme_color —— 網站的主題顏色,一般都與背景顏色相同,它可以影響網站的顯示
  • orientation —— 首選的顯示方向:any, natural, landscape, landscape-primary, landscape-secondary, portrait, portrait-primary, 和 portrait-secondary
  • display —— 首選的顯示方式:fullscreen, standalone(看起來像是native app),minimal-ui(有簡化的瀏覽器控制選項) 和 browser(常規的瀏覽器 tab)
  • icons —— 定義了 src URL, sizestype的圖片物件陣列。

MDN提供了完整的manifest屬性列表:Web App Manifest properties

在開發者工具中的 Application tab 左邊有 Manifest 選項,你可以驗證你的 manifest JSON 檔案,並提供了 “Add to homescreen”。

改造你的網站,變身 PWA

第三步:建立一個 Service Worker

Service Worker 是攔截和響應你的網路請求的程式設計介面。這是一個位於你根目錄的一個單獨的 javascript 檔案。

你的 js 檔案(在示例程式碼中是 /js/main.js)可以檢查是否支援 Service Worker,並且註冊:

if ('serviceWorker' in navigator) {

  // register service worker
  navigator.serviceWorker.register('/service-worker.js');

}複製程式碼

如果你不需要離線功能,可以簡單的建立一個空的 /service-worker.js檔案 —— 使用者會被提示安裝你的 app。

Service Worker 很複雜,你可以修改示例程式碼來達到自己的目的。這是一個標準的 web worker,瀏覽器用一個單獨的執行緒來下載和執行它。它沒有呼叫 DOM 和其他頁面 api 的能力,但他可以攔截網路請求,包括頁面切換,靜態資源下載,ajax請求所引起的網路請求。

這就是需要 HTTPS 的最主要的原因。想象一下第三方程式碼可以攔截來自其他網站的 service worker, 將是一個災難。

service worker 主要有三個事件: installactivatefetch

Install 事件

這個事件在app被安裝時觸發。它經常用來快取必要的檔案。快取通過 Cache API來實現。

首先,我們來構造幾個變數:

  1. 快取名稱(CACHE)和版本號(version)。你的應用可以有多個快取但是隻能引用一個。我們設定了版本號,這樣當我們有重大更新時,我們可以更新快取,而忽略舊的快取。

  2. 一個離線頁面的URL(offlineURL)。當離線時使用者試圖訪問之前未快取的頁面時,這個頁面會呈現給使用者。

  3. 一個擁有離線功能的頁面必要檔案的陣列(installFilesEssential)。這個陣列應該包含靜態資源,比如 CSS 和 JavaScript 檔案,但我也把主頁面(/)和圖示檔案寫進去了。如果主頁面可以多個URL訪問,你應該把他們都寫進去,比如//index.html。注意,offlineURL也要被寫入這個陣列。

  4. 可選的,描述檔案陣列(installFilesDesirable)。這些檔案都很會被下載,但如果下載失敗不會中止安裝。

// configuration
const
  version = '1.0.0',
  CACHE = version + '::PWAsite',
  offlineURL = '/offline/',
  installFilesEssential = [
    '/',
    '/manifest.json',
    '/css/styles.css',
    '/js/main.js',
    '/js/offlinepage.js',
    '/images/logo/logo152.png'
  ].concat(offlineURL),
  installFilesDesirable = [
    '/favicon.ico',
    '/images/logo/logo016.png',
    '/images/hero/power-pv.jpg',
    '/images/hero/power-lo.jpg',
    '/images/hero/power-hi.jpg'
  ];複製程式碼

installStaticFiles()方法新增檔案到快取,這個方法用到了基於 promise的 Cache API。當必要的檔案都被快取後才會生成返回值。

// install static assets
function installStaticFiles() {

  return caches.open(CACHE)
    .then(cache => {

      // cache desirable files
      cache.addAll(installFilesDesirable);

      // cache essential files
      return cache.addAll(installFilesEssential);

    });

}複製程式碼

最後,我們新增install的事件監聽函式。 waitUntil方法確保所有程式碼執行完畢後,service worker 才會執行 install。執行 installStaticFiles()方法,然後執行 self.skipWaiting()方法使service worker進入 active狀態。

// application installation
self.addEventListener('install', event => {

  console.log('service worker: install');

  // cache core files
  event.waitUntil(
    installStaticFiles()
    .then(() => self.skipWaiting())
  );

});複製程式碼

Activate 事件

當 install完成後, service worker 進入active狀態,這個事件立刻執行。你可能不需要實現這個事件監聽,但是示例程式碼在這裡刪除老舊的無用快取檔案:

// clear old caches
function clearOldCaches() {

  return caches.keys()
    .then(keylist => {

      return Promise.all(
        keylist
          .filter(key => key !== CACHE)
          .map(key => caches.delete(key))
      );

    });

}

// application activated
self.addEventListener('activate', event => {

  console.log('service worker: activate');

    // delete old caches
  event.waitUntil(
    clearOldCaches()
    .then(() => self.clients.claim())
    );

});複製程式碼

注意,最後的self.clients.claim()方法設定本身為active的service worker。

Fetch 事件

當有網路請求時這個事件被觸發。它呼叫respondWith()方法來劫持 GET 請求並返回:

  1. 快取中的一個靜態資源。

  2. 如果 #1 失敗了,就用 Fetch API(這與 service worker 的fetch 事件沒關係)去網路請求這個資源。然後將這個資源加入快取。

  3. 如果 #1 和 #2 都失敗了,那就返回一個適當的值。
// application fetch network data
self.addEventListener('fetch', event => {

  // abandon non-GET requests
  if (event.request.method !== 'GET') return;

  let url = event.request.url;

  event.respondWith(

    caches.open(CACHE)
      .then(cache => {

        return cache.match(event.request)
          .then(response => {

            if (response) {
              // return cached file
              console.log('cache fetch: ' + url);
              return response;
            }

            // make network request
            return fetch(event.request)
              .then(newreq => {

                console.log('network fetch: ' + url);
                if (newreq.ok) cache.put(event.request, newreq.clone());
                return newreq;

              })
              // app is offline
              .catch(() => offlineAsset(url));

          });

      })

  );

});複製程式碼

最後這個offlineAsset(url)方法通過幾個輔助函式返回一個適當的值:

// is image URL?
let iExt = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'].map(f => '.' + f);
function isImage(url) {

  return iExt.reduce((ret, ext) => ret || url.endsWith(ext), false);

}


// return offline asset
function offlineAsset(url) {

  if (isImage(url)) {

    // return image
    return new Response(
      '<svg role="img" viewBox="0 0 400 300" xmlns="http://www.w3.org/2000/svg"><title>offline</title><path d="M0 0h400v300H0z" fill="#eee" /><text x="200" y="150" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif" font-size="50" fill="#ccc">offline</text></svg>',
      { headers: {
        'Content-Type': 'image/svg+xml',
        'Cache-Control': 'no-store'
      }}
    );

  }
  else {

    // return page
    return caches.match(offlineURL);

  }

}複製程式碼

offlineAsset()方法檢查是否是一個圖片請求,如果是,那麼返回一個帶有 “offline” 字樣的 SVG。如果不是,返回 offlineURL 頁面。

開發者工具提供了檢視 Service Worker 相關資訊的選項:

改造你的網站,變身 PWA

在開發者工具的 Cache Storage 選項列出了所有當前域內的快取和所包含的靜態檔案。當快取更新的時候,你可以點選左下角的重新整理按鈕來更新快取:

改造你的網站,變身 PWA

不出意料, Clear storage 選項可以刪除你的 service worker 和快取:

改造你的網站,變身 PWA

再來一步 - 第四步:建立一個可用的離線頁面

離線頁面可以是一個靜態頁面,來說明當前使用者請求不可用。然而,我們也可以在這個頁面上列出可以訪問的頁面連結。

main.js中我們可以使用 Cache API 。然而API 使用promises,在不支援的瀏覽器中會引起所有javascript執行阻塞。為了避免這種情況,我們在載入另一個 /js/offlinepage.js 檔案之前必須檢查離線檔案列表和是否支援 Cache API 。

// load script to populate offline page list
if (document.getElementById('cachedpagelist') && 'caches' in window) {
  var scr = document.createElement('script');
  scr.src = '/js/offlinepage.js';
  scr.async = 1;
  document.head.appendChild(scr);
}複製程式碼

/js/offlinepage.js locates the most recent cache by version name, 取到所有 URL的key的列表,移除所有無用 URL,排序所有的列表並且把他們加到 ID 為cachedpagelist的 DOM 節點中:

// cache name
const
  CACHE = '::PWAsite',
  offlineURL = '/offline/',
  list = document.getElementById('cachedpagelist');

// fetch all caches
window.caches.keys()
  .then(cacheList => {

    // find caches by and order by most recent
    cacheList = cacheList
      .filter(cName => cName.includes(CACHE))
      .sort((a, b) => a - b);

    // open first cache
    caches.open(cacheList[0])
      .then(cache => {

        // fetch cached pages
        cache.keys()
          .then(reqList => {

            let frag = document.createDocumentFragment();

            reqList
              .map(req => req.url)
              .filter(req => (req.endsWith('/') || req.endsWith('.html')) && !req.endsWith(offlineURL))
              .sort()
              .forEach(req => {
                let
                  li = document.createElement('li'),
                  a = li.appendChild(document.createElement('a'));
                  a.setAttribute('href', req);
                  a.textContent = a.pathname;
                  frag.appendChild(li);
              });

            if (list) list.appendChild(frag);

          });

      })

  });複製程式碼

開發工具

如果你覺得 javascript 除錯困難,那麼 service worker 也不會很好。Chrome的開發者工具的 Application 提供了一系列除錯工具。

你應該開啟 隱身視窗 來測試你的 app,這樣在你關閉這個視窗之後快取檔案就不會儲存下來。

最後,Lighthouse extension for Chrome 提供了很多改進 PWA 的有用資訊。

PWA 陷阱

有幾點需要注意:

URL 隱藏

我們的示例程式碼隱藏了 URL 欄,我不推薦這種做法,除非你有一個單 url 應用,比如一個遊戲。對於多數網站,manifest 選項 display: minimal-ui 或者 display: browser是最好的選擇。

快取太多

你可以快取你網站的所有頁面和所有靜態檔案。這對於一個小網站是可行的,但這對於上千個頁面的大型網站實際嗎?沒有人會對你網站的所有內容都感興趣,而裝置的記憶體容量將是一個限制。即使你像示例程式碼一樣只快取訪問過的頁面和檔案,快取大小也會增長的很快。

也許你需要注意:

  • 只快取重要的頁面,類似主頁,和最近的文章。
  • 不要快取圖片,視訊和其他大型檔案
  • 經常刪除舊的快取檔案
  • 提供一個快取按鈕給使用者,讓使用者決定是否快取

快取重新整理

在示例程式碼中,使用者在請求網路前先檢查該檔案是否快取。如果快取,就使用快取檔案。這在離線情況下很棒,但也意味著在聯網情況下,使用者得到的可能不是最新資料。

靜態檔案,類似於圖片和視訊等,不會經常改變的資源,做長時間快取沒有很大的問題。你可以在HTTP 頭裡設定 Cache-Control 來快取檔案使其快取時間為一年(31,536,000 seconds):

Cache-Control: max-age=31536000複製程式碼

頁面,CSS和 script 檔案會經常變化,所以你應該改設定一個很短的快取時間比如 24 小時,並在聯網時與服務端檔案進行驗證:

Cache-Control: must-revalidate, max-age=86400複製程式碼

譯自 Retrofit Your Website as a Progressive Web App

相關文章