使用PWA加強你的github pages

Github PagesGithub 提供的一個網站託管服務,能夠用於部署我的博客或者項目主頁,使用的是 jekyll 框架做爲文件解析轉換爲網頁靜態文件的載體。PWA(Progressive Web Apps) 是近幾年 Google 提出的概念,致力於使用原生 Web 技術快速打造可靠的、媲美原生應用體驗的 Web App。使用 Github Pages 搭建我的博客是很是快捷,方便以及免費的可靠的方式,再配合以 PWA 技術加強,能使咱們的博客像一個應用那樣被訪問,加強用戶粘度。css

什麼是 PWA

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

關於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 應用可使用到的技術有:ios

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

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

Github Pages + PWA

下面例子使用的是 Github Pages 默認使用的 jekyll 引擎,詳情能夠參考這裏github

Service Worker

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

sw-lifecycle
(來自MDN)

註冊(register)

Service Worker 和通常的腳本代碼不同,它的全部代碼須要單獨放在一個文件中,而後經過指定的接口註冊到頁面裏。chrome

假設如今有一個 service-worker.js 在項目根目錄下,咱們在根目錄下的 index.html 里加入如下代碼:json

<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 文件路徑相同。api

安裝(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

相關文章
相關標籤/搜索