使用PWA增強你的github pages

xlaoyu發表於2018-04-21

Github PagesGithub 提供的一個網站託管服務,可以用於部署個人部落格或者專案主頁,使用的是 jekyll 框架作為檔案解析轉換為網頁靜態檔案的載體。PWA(Progressive Web Apps) 是近幾年 Google 提出的概念,致力於使用原生 Web 技術快速打造可靠的、媲美原生應用體驗的 Web App。使用 Github Pages 搭建個人部落格是非常快捷,方便以及免費的可靠的方式,再配合以 PWA 技術增強,能使我們的部落格像一個應用那樣被訪問,增強使用者粘度。

什麼是 PWA

PWA 並沒有特指某種技術或者工具,而是指使用一系列最新的 Web 技術,但同時保證應用在不支援新特性的瀏覽器中的使用也不受影響的方法論,可以選擇性只使用其中幾項技術而不必使用全套技術,這就是漸進式(Progressive)的意義所在。大多數 FEer 應該都聽過這個詞語,從大咖 Google 在大力推行開始這個技術迅速在前端圈裡掀起了熱潮,但是如果追尋這個概念最早的提出者,應該要追述到由 Alex Russell 大神的寫的 Progressive Web Apps: Escaping Tabs Without Losing Our Soul,核心就是要在保留 Web 靈魂的基礎上對網頁進行增強。

關於Alex Russell Ps:*Alex Russell* 是框架 [Dojo](https://dojo.io/) 的創始人之一,`Dojo` 是我出來工作後使用的第一款前端**框架**,前幾年風風火火的 `AMD` 模組標準就是從 Dojo 中衍生出來的,裡面的元件化、模組化和展現與邏輯分離等思想,深深影響了我對前端的認識和理解,使我受益匪淺,可惜由於是由 *IBM* 維護所以發展較慢,恰恰近幾年前端發展速度堪比光速百花齊放,各種框架爭妍鬥豔,以致於這個框架越來越少人使用。不過最近 Dojo 2.0 馬上要釋出,結合最新的 TypeScript、Webpack 等技術重寫了一遍,幾乎是全新的一個框架,非常期待未來的表現。

PWA 的帶來的提升主要有:

  1. 可安裝 - 允許使用者把網頁應用新增到裝置主螢幕中,就像安裝一個原生應用但是不用通過 Apple Store 或者其他應用商店。然後直接進入網頁應用。 Ps: 其實 iOS 幾乎是從一出生就支援了這個功能
    ios-add-to-screen
  2. 離線能力 - Web應用一直無法比擬原生應用的重要一點其實就是離線訪問能力,眾所周知傳統網頁離開網路就無法生存。但是 PWA 能突破這個限制。
  3. 喚回能力 - 傳統網頁,在使用者不開啟訪問的時候,是無法主動給使用者推送訊息的,這導致了無法持續和使用者進行互動從而無法提高使用者留存率。PWA 使用最新的 API 能在使用者不訪問應用的時候進行訊息推送,使 Web 應用和原生應用站在了同一起跑線上。
  4. 易於發現 - 歸根到底 Web應用 也是一個網頁,所以它可被搜尋引擎發現,並且擁有原生應用無法比擬的一個特點:可通過 URL 輕鬆分享給別人。上面提到的 Web靈魂 其中一點就是指這個開放性

目前開發 PWA 應用可以使用到的技術有:

  • Service Worker - 實現應用離線訪問的核心技術之一
  • Cache - 實現應用離線訪問的核心技術之二
  • Fetch API - 實現應用離線訪問的核心技術之三
  • App Manifest - 實現應用新增到桌面的技術
  • Push API - 實現伺服器推送的主要技術之一
  • Notifications API - 實現伺服器推送的主要技術之二

本篇幅不一一詳細介紹各項技術的概念與使用,有興趣可自行了解,本文說明如何在 Github Pages 中一步一步引入 PWA 中的各個特性。

Github Pages + PWA

下面例子使用的是 Github Pages 預設使用的 jekyll 引擎,詳情可以參考這裡

Service Worker

說起 PWA 不得不首先提起 Service Worker,其他特性的功效或多或少都依賴於首先啟用了該功能。我們主要在Service Worder的三個生命週期期間(事件)installactivatefetch裡搞事情:

sw-lifecycle
(來自MDN)

註冊(register)

Service Worker 和一般的指令碼程式碼不一樣,它的所有程式碼需要單獨放在一個檔案中,然後通過指定的介面註冊到頁面裡。

假設現在有一個 service-worker.js 在專案根目錄下,我們在根目錄下的 index.html 里加入以下程式碼:

<script>
// 註冊 service worker
if (navigator.serviceWorker) {
  navigator.serviceWorker.register('/service-worker.js', {scope: '/'})
}
</script>
複製程式碼

首先判斷當前環境是否支援 Service Worder,記住我們的核心是 Progressiveregister 方法的第二個引數 scope 用於指定Worker 可控制的範圍(通過 URL 判斷),舉個?:如果 scope 設為 /sub/,那麼網頁中所有到/other//other/foo的請求都無法在 Worker 的 fetch 事件中攔截。預設值範圍和 Service Worker 檔案路徑相同。

安裝(install)

註冊完成後,Service Worker開始執行,首先會接收到一次install事件,我們可以在install事件回撥中進行獲取資源,然後放入快取中的操作:

假設我們的部落格需要使用 main.jsmain.css 兩個檔案,分別放置在 js 目錄和 css目錄下,Service Worker 可以這麼寫:

const CACHE_NAME = 'xlaoyu_blog_1.0.0';

const URLS = [                // Add URL you want to cache in this list.
  // '/',                     // If you have separate JS/CSS files, add path to those files here
  '/index.html',
  '/css/main.css',
  '/js/main.js'
];

// Cache resources
self.addEventListener('install', function (e) {
  e.waitUntil(
    caches.open(CACHE_NAME).then(function (cache) {
      console.log('installing cache : ' + CACHE_NAME)
      return cache.addAll(URLS);
    }).then(_ => {
      return self.skipWaiting();
    });
  );
});
複製程式碼

e.waitUntil - 表示等待傳入的 Promise 完成之後才把安裝狀態標記為完成

caches.open(chche_name) - 開啟一個快取物件。一個域名下可以有多個快取物件

cache.addAll(urls) - 根據傳入的 URL 在後臺自動請求獲取資源,然後以 URL 為 key 資源內容為 value 存入上一條開啟的 cache 物件中。

self.skipWaiting() - 直接跳過 waiting 階段,下面會詳細講解。

攔截請求(fetch)

這個事件使得我們的 Service Worker 有能力對指定範圍內的頁面發出的所有請求進行濾或者替換。

// Respond with cached resources
self.addEventListener('fetch', function (e) {
  e.respondWith(
    caches.match(e.request).then(function (request) {
      if (request) {
        // 如果快取存在,直接返回快取
        console.log('responding with cache : ' + e.request.url);
        return request;
      } else {
        // 快取不存在,發起請求獲取資源返回
        console.log('file is not cached, fetching : ' + e.request.url);
        return fetch(e.request);
      }
    });
  );
});
複製程式碼

啟用(activate)

這個階段我們可以在這裡進行舊或者不再使用的快取的清理工作。

滿足以下兩個條件之一,才會進入此階段:

  • Service Worker 第一次註冊
  • Service Worker 有更新,同時已經沒有頁面使用舊的 Worker 或者 使用了 skipWaiting 跳過 waiting 階段

如果 Service Worker 檔案的內容有改動,當訪問網站頁面時瀏覽器獲取了新的檔案,它會認為有更新,於是會安裝新的檔案並觸發 install 事件。但是此時已經處於啟用狀態的舊的 Service Worker 還在執行,新的 Service Worker 完成安裝後會進入 waiting 狀態。直到所有已開啟的頁面都關閉,舊的 Service Worker 自動停止,新的 Service Worker 才會在接下來開啟的頁面裡生效。

// Delete outdated caches
self.addEventListener('activate', function (e) {
  e.waitUntil(
    caches.keys().then(function (keyList) {
      // `keyList` contains all cache names under your username.github.io
      // filter out ones that has this app prefix to create white list
      // 以 app_prefix 開頭這裡會返回0,會被過濾掉
      // 所以 cacheWhitelist 只包含當前指令碼最新的key或者其他指令碼新增的 cache
      var cacheWhitelist = keyList.filter(function (key) {
        return key.indexOf(APP_PREFIX);
      });
      // add current cache name to white list
      cacheWhitelist.push(CACHE_NAME);

      return Promise.all(keyList.map(function (key, i) {
        if (cacheWhitelist.indexOf(key) === -1) {
          console.log('deleting cache : ' + keyList[i] )
          return caches.delete(keyList[i])
        }
      }));
    }).then(function () {
      // 更新客戶端
      clients.claim();
    })
  );
});
複製程式碼

clients.claim() - 使頁面立刻使用新的 Worker。一般情況下,新的 Service Worker 需要在頁面重新開啟後才生效,通過結合 skipWaiting 和此方法的組合拳,能使新 Worker 立即生效。

至此,一個簡單的 Service Worker 流程已經走完,

工具代替人手

上面我們模擬了最簡單的一個頁面使用 Service Worker 是如何操作的,非常簡單快捷,但是仔細想想,在複雜的場景下事情就沒那麼簡單了,我們需要考慮幾個問題:

  1. 一個大的專案包含的靜態檔案可能成百上千,顯而易見,靠人工維護這份列表是不靠譜的;
  2. 瀏覽器能在位元組級別檢查出 Service-Worker.js 檔案的變化,然後進行對應的操作,但是 Service Worker 如果沒變化,它是無法檢測出被快取的檔案是否有改變而讀取最新的檔案的。其實無論是否使用 Service Worker 都會有這個問題,在傳統場景下最常見的解決方案就是hash化檔名;這個方法也能使用在這裡,不過結合第一點,顯然不可能人手維護;

sw-precache - 通過掃描指定的靜態檔案目錄,計算檔案hash然後生成service worker 檔案的工具,能有效解決上述兩個問題。開啟方式參考官方文件即可,這裡列一下我使用的配置:

const prefix = '_site';

module.exports = {
  staticFileGlobs: [
    '!_site/assets/**/**.*',
    '!_site/service-worker.js',
    prefix + '/**/**.html',     // 所有頁面,文章頁面的html(必須包含)
    prefix + '/js/*.js',        // 所有 js 檔案
    prefix + '/css/*.css',      // 所有 css 檔案
    prefix + '/images/**/**.*', // 個人用於存放部落格相關圖片的資料夾,正常情況是沒有的
    prefix + '/favicon.ico',
    prefix + '/**/*.json',
  ],
  stripPrefix: prefix
}
複製程式碼

有幾點需要說明:

  1. 為什麼掃描 _site 目錄?
    因為 github pages 頁面訪問的就是這個目錄下的檔案,如果曾經使用 jekyll 服務在本地啟動編譯 blog 的話,一定能看到專案根目錄下會多出這個 _site 目錄。
  2. 為什麼排除 _site/assets? 因為本地會生成這個目錄,但是經過測試在我釋出到 github 上後,正式環境下並不會生成這個目錄,所以如果不排除此目錄的話 Service Worker 會嘗試去快取這目錄下的檔案,導致載入報錯然後整個 Worker 都失效,這是我們不願看到的。
  3. 我們要快取什麼才能實現離線訪問? HTML檔案、所有頁面必須使用到的沒有使用 CDN 代理的 JS、CSS、圖片、JSON等。

實際效果:

在聯網狀態下訪問 www.xlaoyu.info,然後把網路斷開,在頁面進行操作(非外鏈轉跳),可以看到在斷網時互動並不受影響。

App Manifest

App Manifest 是一項提升 Web 應用移動端能力的技術。就是讓我們的網頁能被新增到主螢幕中,擁有和原生應用幾乎一致的表現。

首先,我們在頁面 head 區域新增引入 manifest 檔案的資訊:

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

下面是我的 manifest.json 檔案配置

{
  "scope": "/",
  "name": "xlaoyu-blog",
  "short_name": "xlaoyu-blog",
  "start_url": "/?from=homescreen",
  "display": "standalone",
  "description": "路漫漫其修遠兮,吾將上下而求索",
  "dir": "ltr",
  "lang": "cn",
  "orientation": "portrait",
  "theme_color": "#70B7FD",
  "background_color": "#fff",
  "icons": [{
    "src": "images/icon-48x48.png",
    "sizes": "48x48",
    "type": "image/png"
  }, {
    "src": "images/apple-touch-icon-57×57.png",
    "sizes": "57x57",
    "type": "image/png"
  }, {
    "src": "images/apple-touch-icon-72x72.png",
    "sizes": "72x72",
    "type": "image/png"
  }, {
    "src": "images/icon-96x96.png",
    "sizes": "96x96",
    "type": "image/png"
  }]
}
複製程式碼

icons 怎麼配置可以看 這裡

注意,新增到桌面的 Web 頁面,需要先在聯網狀態下開啟一次桌面的版本,才能實現離線訪問,新增後如果一次都沒開啟過,斷網之後這個“APP”還是無法使用的。

其他技術

由於在 Github Pages 中不太可能需要用到推送等功能,這些屬於真正的應用才需要的功能,所以這裡不贅述。

總結

其實在大多數個人blog或者網頁的場景下,是否支援離線訪問,是否能新增到桌面模擬原生應用並沒有那麼的重要,能留住使用者吸引別人來訪問的核心需求是文章的內容和質量,這次嘗試也只是作為練手目的。

以上只是 PWA 的其中一小點應用場景,結合這麼多技術 + 非凡的創意一定會催生出更多令人驚喜的特性和功能。也許現在不是前端最好的時代,但是一定是越來越精彩的時代!

以上內容如有錯漏,或者有其他看法,請留言共同探討。


參考文章:


版權宣告:原創文章,如需轉載,請註明出處“本文首發於xlaoyu.info

相關文章