web漸進式應用PWA

什麼是漸進式 Web 應用

漸進式 Web 應用首先是一種應用,它根據設備的支持狀況來提供更多功能,提供離線能力,推送通知,甚至原生應用的外觀和速度,以及對資源進行本地緩存。css

漸進式 Web 應用是一個網站,它使用了某些開發技術,使其體驗比普通針對移動優化的網站體驗更好。它使用起來就像是在使用一個原生應用同樣html

漸進式 Web 應用多是一個不清晰的術語,而更好的定義是:它們是一種 Web 應用,利用現代瀏覽器特性(好比 Web Worker 和 Web 應用清單),讓移動設備對其「升級」,使之成爲一等公民角色的應用程序。webpack

解決方案 漸進式Web App(PWA)

PWA結合了最好的Web應用和最好的原生應用的用戶體驗。包含如下:git

  • 漸進式 - 每一個用戶均可用而無論選擇什麼樣的瀏覽器,由於它們是以漸進式加強爲核心原則構建的。
  • 自適應 - 適應任何形態:桌面,移動設備,平板電腦或還沒有出現的形式。
  • 不依賴網絡鏈接 - Service Workers容許離線工做,或在低質量網絡上工做。
  • 相似於應用程序 - 使用應用程序風格的交互和導航,感受像一個應用程序。
  • 保持最新 - 得益於service Woker的更新進程,應用能始終保持最新狀態。
  • 安全 - 藉助於HTTPS,防止窺探,並確保內容沒有被篡改
  • 可發現 - 受益於W3C清單和service Worker註冊做用域,搜索引擎可找到它們,能夠識別爲「應用程序」。
  • 用戶粘性 - 經過推送通知等功能讓用戶重返應用。
  • 可安裝 - 容許用戶在主屏幕上「保留」他們認爲最有用的應用程序,而無需通過應用程序商店。
  • 可連接 - 經過URL輕鬆共享,不須要複雜的安裝。

離線解決方案 Service Workers

漸進式 Web 應用的定義中有部分是這樣說的:它必須支持離線工做。github

因爲容許 Web 應用程序脫機工做的是 Service Worker,這意味着 Service Worker 是漸進式 Web 應用強制要求的部分。web

1.使用HTTPS

漸進式Web應用程序須要使用HTTPS鏈接。雖然使用HTTPS會讓您服務器的開銷變多,但使用HTTPS可讓您的網站變得更安全 ,如何給網站開啓https編程

2.建立一個應用程序清單(Manifest)

應用程序清單提供了和當前漸進式Web應用的相關信息,如:json

  • 應用程序名
  • 描述
  • 全部圖片(包括主屏幕圖標,啓動屏幕頁面和用的圖片或者網頁上用的圖片)

本質上講,程序清單是頁面上用到的圖標和主題等資源的元數據。api

程序清單是一個位於您應用根目錄的JSON文件。該JSON文件返回時必須添加Content-Type: application/manifest+json 或者 Content-Type: application/jsonHTTP頭信息。程序清單的文件名不限,在本文的示例代碼中爲manifest.json:跨域

// manifest.json
{
  "dir": "ltr",
  "lang": "en",
  "name": "D.D Blog",
  "scope": "/",
  "display": "standalone",
  "start_url": "/",
  "short_name": "D.D Blog",
  "theme_color": "transparent",
  "description": "Share More, Gain More. - D.D Blog",
  "orientation": "any",
  "background_color": "transparent",
  "related_applications": [],
  "prefer_related_applications": false,
  "icons": [{
    "src": "assets/img/logo/size-32.png",
    "sizes": "32x32",
    "type": "image/png"
  }, {
    "src": "assets/img/logo/size-48.png",
    "sizes": "48x48",
    "type": "image/png"
  } //...
  ],
  "gcm_sender_id": "...",
  "applicationServerKey": "..."
}

程序清單文件創建完以後,你須要在每一個頁面上引用該文件:

<link rel="manifest"href="/manifest.json">

如下屬性在程序清單中常用,介紹說明以下:

  • short_name: 應用展現的名字
  • icons: 定義不一樣尺寸的應用圖標
  • start_url: 定義桌面啓動的 URL
  • description: 應用描述,能夠參考 meta 中的 description
  • display: 定義應用的顯示方式,有 4 種顯示方式,分別爲:
    • fullscreen: 全屏
    • standalone: 應用
    • minimal-ui: 相似於應用模式,但比應用模式多一些系統導航控制元素,但又不一樣於瀏覽器模式
    • browser: 瀏覽器模式,默認值
  • name: 應用名稱
  • orientation: 定義默認應用顯示方向,豎屏、橫屏
  • prefer_related_applications: 是否設置對應移動應用,默認爲 false
  • related_applications: 獲取移動應用的方式
  • background_color: 應用加載以前的背景色,用於應用啓動時的過渡
  • theme_color: 定義應用默認的主題色
  • dir: 文字方向,3 個值可選 ltr(left-to-right), rtl(right-to-left) 和 auto(瀏覽器判斷),默認爲 auto
  • lang: 語言
  • scope: 定義應用模式下的路徑範圍,超出範圍會已瀏覽器方式顯示

manifest注意事項

  • 站點離線存儲的容量限制是5M
  • 若是manifest文件,或者內部列舉的某一個文件不能正常下載,整個更新過程將視爲失敗,瀏覽器繼續所有使用老的緩存
  • 引用manifest的html必須與manifest文件同源,在同一個域下
  • 在manifest中使用的相對路徑,相對參照物爲manifest文件
  • CACHE MANIFEST字符串應在第一行,且必不可少
  • 系統會自動緩存引用清單文件的 HTML 文件
  • manifest文件中CACHE則與NETWORK,FALLBACK的位置順序沒有關係,若是是隱式聲明須要在最前面
  • FALLBACK中的資源必須和manifest文件同源
  • 當一個資源被緩存後,該瀏覽器直接請求這個絕對路徑也會訪問緩存中的資源。
  • 站點中的其餘頁面即便沒有設置manifest屬性,請求的資源若是在緩存中也從緩存中訪問
  • 當manifest文件發生改變時,資源請求自己也會觸發更新
3.建立一個 Service Worker

Service Worker 是一個可編程的服務器代理,它能夠攔截或者響應網絡請求。ServiceWorker 是位於應用程序根目錄的一個個的JavaScript文件。
您須要在頁面對應的JavaScript文件中註冊該ServiceWorker:

//main.js
if ('serviceWorker' in navigator) {
  // 註冊 service worker
  navigator.serviceWorker.register('/service-worker.js');
}

若是您不須要離線的相關功能,您能夠只建立一個 /service-worker.js文件,這樣用戶就能夠直接安裝您的Web應用了!

Service Worker這個概念可能比較難懂,它實際上是一個工做在其餘線程中的標準的Worker,它不能夠訪問頁面上的DOM元素,沒有頁面上的API,可是能夠攔截全部頁面上的網絡請求,包括頁面導航,請求資源,Ajax請求。

上面就是使用全站HTTPS的主要緣由了。假設您沒有在您的網站中使用HTTPS,一個第三方的腳本就能夠從其餘的域名注入他本身的ServiceWorker,而後篡改全部的請求——這無疑是很是危險的。

Service Worker 會響應三個事件:install,activate和fetch。

Service Worker 和 Web Worker 不是同一個東西 ,不要搞混淆了

Install事件

該事件將在應用安裝完成後觸發。咱們通常在這裏使用CacheAPI緩存一些必要的文件。

首先,咱們須要提供以下配置

  • 緩存名稱(CACHE)以及版本(version)。應用能夠有多個緩存存儲,可是在使用時只會使用其中一個緩存存儲。每當緩存存儲有變化時,新的版本號將會指定到緩存存儲中。新的緩存存儲將會做爲當前的緩存存儲,以前的緩存存儲將會被做廢。
  • 一個離線的頁面地址(offlineURL):當用戶訪問了以前沒有訪問過的地址時,該頁面將會顯示。
  • 一個包含了全部必須文件的數組,包括保障頁面正常功能的CSS和JavaScript。在本示例中,我還添加了主頁和logo。當有不一樣的URL指向同一個資源時,你也能夠將這些URL分別寫到這個數組中。offlineURL將會加入到這個數組中。
  • 咱們也能夠將一些非必要的緩存文件(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的方式使用CacheAPI將文件存儲到緩存中。

// 安裝 靜態資源
function installStaticFiles() {
  return caches.open(CACHE)
    .then(cache => {
      // 緩存靜態文件
      cache.addAll(installFilesDesirable);
      // 緩存主要的文件
      return cache.addAll(installFilesEssential);
    });
}

最後,咱們添加一個install的事件監聽器。waitUntil方法保證了service worker不會安裝直到其相關的代碼被執行。這裏它會執行installStaticFiles()方法,而後self.skipWaiting()方法來激活service worker:

// 程序安裝
self.addEventListener('install', event => {
  console.log('service worker: install');
  // 緩存核心文件
  event.waitUntil(
    installStaticFiles()
    .then(() => self.skipWaiting())
  );
});

Activate 事件

這個事件會在service worker被激活時發生。你可能不須要這個事件,可是在示例代碼中,咱們在該事件發生時將老的緩存所有清理掉了:

// 清理舊的緩存
function clearOldCaches() {
  return caches.keys()
    .then(keylist => {
      return Promise.all(
        keylist
          .filter(key => key !== CACHE)
          .map(key => caches.delete(key))
      );
    });
}

// 程序激活
self.addEventListener('activate', event => {
  console.log('service worker: activate');
    // 刪除舊的緩存
  event.waitUntil(
    clearOldCaches()
    .then(() => self.clients.claim())
    );

});

注意self.clients.claim()執行時將會把當前service worker做爲被激活的worker。

Fetch 事件該事件將會在網絡開始請求時發起。該事件處理函數中,咱們可使用respondWith()方法來劫持HTTP的GET請求而後返回:

  1. 從緩存中取到的資源文件
  2. 若是第一步失敗,資源文件將會從網絡中使用Fetch API來獲取(和service worker中的fetch事件無關)。獲取到的資源將會加入到緩存中。
  3. 若是第一步和第二步均失敗,將會從緩存中返回正確的資源文件。
// 程序獲取網絡數據
self.addEventListener('fetch', event => {
  // 放棄非get請求
  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) {
              // 還回緩存文件
              console.log('cache fetch: ' + url);
              return response;
            }
            // 發起網絡請求
            return fetch(event.request)
              .then(newreq => {
                console.log('network fetch: ' + url);
                if (newreq.ok) cache.put(event.request, newreq.clone());
                return newreq;
              })
              // 程序離線
              .catch(() => offlineAsset(url));
          });
      })
  );
});

offlineAsset(url)方法中使用了一些helper方法來返回正確的數據:

// 判斷是否是圖片資源?
let iExt = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'].map(f => '.' + f);
function isImage(url) {
  return iExt.reduce((ret, ext) => ret || url.endsWith(ext), false);
}

// 返回離線資源
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 caches.match(offlineURL);
  }
}

offlineAsset()方法檢查請求是否爲一個圖片,而後返回一個帶有「offline」文字的SVG文件。其餘請求將會返回 offlineURL 頁面。
Chrome開發者工具中的ServiceWorker部分提供了關於當前頁面worker的信息。其中會顯示worker中發生的錯誤,還能夠強制刷新,也可讓瀏覽器進入離線模式。
Cache Storage 部分例舉了當前全部已經緩存的資源。你能夠在緩存須要更新的時候點擊refresh按鈕。

4:建立可用的離線頁面

離線頁面能夠是靜態的HTML,通常用於提醒用戶當前請求的頁面暫時沒法脫機使用。然而,咱們能夠提供一些能夠閱讀的頁面連接。

Cache API能夠在main.js中使用。然而,該API使用Promise,在不支持Promise的瀏覽器中會失敗,全部的JavaScript執行會所以受到影響。爲了不這種狀況,在訪問/js/offlinepage.js的時候咱們添加了一段代碼來檢查當前是否在離線環境中:

// 加載腳本以填充脫機頁列表
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 中以版本號爲名稱保存了最近的緩存,獲取全部URL,刪除不是頁面的URL,將這些URL排序而後將全部緩存的URL展現在頁面上:

// 緩存名稱
const
  CACHE = '::PWAsite',
  offlineURL = '/offline/',
  list = document.getElementById('cachedpagelist');

// 獲取全部緩存
window.caches.keys()
  .then(cacheList => {
    // 按最近的查找緩存和排序
    cacheList = cacheList
      .filter(cName => cName.includes(CACHE))
      .sort((a, b) => a - b);
    // 打開第一個
    caches.open(cacheList[0])
      .then(cache => {
        // 獲取已經緩存的頁面
        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);
          });
      })

  });

ServiceWorker 在SPA中的應用

根據《深刻淺出Webpack》
只須要安裝 serviceworker-webpack-plugin 組件

//webpack config
import ServiceWorkerWebpackPlugin from 'serviceworker-webpack-plugin';

plugins: [
  new ServiceWorkerWebpackPlugin({
    entry: path.join(__dirname, 'src/sw.js'),
  }),
],
//mian.js
import runtime from 'serviceworker-webpack-plugin/lib/runtime';
 
if ('serviceWorker' in navigator) {
  const registration = runtime.register();
}
//sw.js
{
  assets: [
    './main.256334452761ef349e91.js',
    ....
  ],
}

事實上能構建除想要的結果也能達到,離線緩存. 可是離線緩存文件除了圖片等靜態變的資源外, 每次打包構建的hash 他也會隨之改變, 不可能每次都手動修改靜態文件資源列表. 因而:

推薦使用sw-precache-webpack-plugin

//webpack config
const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');

plugins: [
  new SWPrecacheWebpackPlugin(
    {
      cacheId: 'appName',
      dontCacheBustUrlsMatching: /\.\w{8}\./,
      filename: 'service-worker.js',
      minify: true,
      navigateFallback: '/index.html',
      staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/],
    }
  ),
]
//main.js
if ('serviceWorker' in navigator && process.env.NODE_ENV == "production") {
  navigator.serviceWorker.register('/service-worker.js');
}

每次編譯代碼以後會自動生成靜態資源列表.
能夠打開瀏覽器的調試器 Application -> Service Workers 看到 服務已經啓動

在Application -> Cache -> Cache Storage 裏面能夠看到緩存的靜態文件

Service Worker 本質上提供了相似 Web Worker 的功能,其做爲 Web Application 以及 Server 之間的代理服務器,能夠截獲用戶的請求。可是爲了實現離線緩存功能,還須要結合 Cache API。
使用 Cache Storage 還須要注意如下幾點:

  • 它只能緩存 GET 請求;
  • 每一個站點只能緩存屬於本身域下的請求,同時也能緩存跨域的請求,好比 CDN,不過沒法對跨域請求的請求頭和內容進行修改
  • 緩存的更新須要自行實現;
  • 緩存不會過時,除非將緩存刪除,而瀏覽器對每一個網站 Cache Storage 的大小有硬性的限制,因此須要清理沒必要要的緩存。

在切換到 Network -> all 就能夠看到被緩存的文件的Size 那欄 (from ServiceWorker 不一樣於 from disk cache)

爲了驗證網頁在離線時能訪問的能力,須要在開發者工具中的 Network 一欄中經過 Offline 選項禁用掉網絡,再刷新頁面能正常訪問,而且網絡請求的響應都來自 Service Workers,正常的效果如圖:

分享功能(Web Share API)

使用分享功能,須要知足如下幾點:

  • 網站必須基於 HTTPS
  • 註冊 Origin Trial,並將生成的 token 加入頁面 meta 中
  • 提供 text 或 url 中的一項,且值必須爲字符串
  • 分享事件必須由用戶事件觸發
// CommonService.js
export const isSupportShareAPI = () => !!navigator.share;

export const sharePage = () => {
    navigator
        .share({
            title: document.title,
            text: document.title,
            url: window.location.href
        })
        .then(() => console.info('Successful share.'))
        .catch(error => console.log('Error sharing:', error));
};

PWA消息推送

PWA消息推送(傳送門)

相關文章
相關標籤/搜索