改造你的網站,變身 PWA

最近有不少關於 Progressive Web Apps(PWAs)的消息,不少人都在問這是否是(移動)web 的將來。我不想陷入native app 和 PWA 的紛爭,可是有一件事是肯定的 --- PWA極大的提高了移動端表現,改善了用戶體驗。javascript

好消息是開發一個 PWA 並不難。事實上,咱們能夠將現存的網站進行改進,使之成爲PWA。這也是我這篇文章要講的 -- 當你讀完這篇文章,你能夠將你的網站改進,讓他看起來就像是一個 native web app。他能夠離線工做而且擁有本身的主屏圖標。css

Progressive Web Apps 是什麼?

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

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

  2. 用戶能夠在安裝前就體驗你的 app。java

  3. 不須要經過 AppStore 下載 app。app 會自動升級不須要用戶升級。node

  4. 用戶會受到‘安裝’的提示,點擊安裝會增長一個圖標到用戶首屏。git

  5. 被打開時,PWA 會展現一個有吸引力的閃屏。github

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

  7. 必要的文件會被本地緩存,所以會比標準的web app 響應更快(也許也會比native app響應快)ajax

  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信息。

瀏覽主頁,或者其餘頁面,而後用如下任一方法使頁面離線:

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

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

從新瀏覽任意以前瀏覽過的頁面,它們仍然能夠瀏覽到。瀏覽一個以前沒有看過的頁面,你會看到一個專門的離線頁面,標識「you’re offline」,還有一個你能夠瀏覽的頁面列表:

鏈接手機

你也能夠經過 USB 鏈接你的安卓手機來預覽示例網頁。在開發者工具中打開 Remote devices 菜單。

在左邊選擇 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」。

第三步:建立一個 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 相關信息的選項:

在開發者工具的 Cache Storage 選項列出了全部當前域內的緩存和所包含的靜態文件。當緩存更新的時候,你能夠點擊左下角的刷新按鈕來更新緩存:

不出意料, Clear storage 選項能夠刪除你的 service worker 和緩存:

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

離線頁面能夠是一個靜態頁面,來講明當前用戶請求不可用。然而,咱們也能夠在這個頁面上列出能夠訪問的頁面連接。

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

相關文章
相關標籤/搜索