Workbox5+Webpack4構建離線應用

wxz在掘金發表於2020-02-18

離線快取優化

將應用中的靜態資源快取是目前最主流的效能優化方法,甚至能讓應用秒開!目前常見的快取方式有http快取memory cachedisk cahcelocalstorageService worker快取等方式,本文介紹的Workbox就是實現Service worker離線快取的一個工具。

那麼問題來了,Service worker離線快取和傳統的快取方式對比,有什麼優勢和劣勢呢,service worker之所以越來越流行,是因為它讓前端快取脫離了服務端,不需要服務端再額外做些什麼,前端工程師自己就可以實現快取,而且快取內容完全可控,下面是我搜尋的幾條主流快取方式的介紹和對比。

Workbox5+Webpack4構建離線應用

上圖從深入理解瀏覽器快取處參考

http快取依賴於服務端配置,memory cache和disk cache快取內容不可控,而且只快取一些靜態資源,push cache是臨時快取,localstorage適用於快取一些全域性的資料,對於靜態資源很少用它。

service worker快取的優缺點:

優點:

  • 非侵入式快取
  • 快取內容開發者完全可控
  • 持續性快取
  • 獨立於主執行緒之外,不堵塞程式

缺點:

  • 許可權太大,能攔截所有fetch請求,需要控制一下
  • 發版更新處理比較麻煩

Workbox簡介

Workbox 是 Google Chrome 團隊推出的一套 PWA 的解決方案,這套解決方案當中包含了核心庫和構建工具,因此我們可以利用 Workbox 實現 Service Worker 的快速開發。

引入方式

有兩種方式可以引入workbox:

第一種最為方便,就是通過importScripts()方法從谷歌官方CDN中引入。

importScripts('https://storage.googleapis.com/workbox-cdn/releases/5.0.0/workbox-sw.js');
if (workbox) {  
    console.log(`Yay! Workbox is loaded ?`);
} else {  
    console.log(`Boo! Workbox didn't load ?`);
}
複製程式碼

第二種方式就是從本地引入,本地需要從npm庫中下載相應的workbox包,然後通過import按需匯入,本文的例子就是這種方式。

import {precaching} from 'workbox-precaching';
import {registerRoute} from 'workbox-routing';
import {BackgroundSyncPlugin} from 'workbox-background-sync';

// 按需引入,然後使用對應模組...
複製程式碼

詳細介紹請查閱官方文件

配置

Workbox可以修改快取名稱,可以用setCacheNameDetails設定預快取和執行時快取的名稱,還可以通過workbox.core.cacheNames.precacheworkbox.core.cacheNames.runtime 獲取當前定義的預快取和動態快取名稱。

// 修改預設配置
workbox.core.setCacheNameDetails({
  prefix: 'app',
  suffix: 'v1',
  precache: 'precache',
  runtime: 'runtime'
})

// 列印修改結果

// 將列印 'app-precache-v1'
console.log(worbox.core.cacheNames.precache)
// 將列印 'app-runtime-v1'
console.log(workbox.core.cacheNames.runtime)
複製程式碼

更多配置下資訊請參考官方文配置文件

預快取功能

預快取功能可以在service worker安裝前將一些靜態檔案提前快取下來,這樣就能保證service worker安裝後可以直接存快取中獲取這些靜態資源,可以通過以下程式碼實現。

import {precacheAndRoute} from 'workbox-precaching';

precacheAndRoute([  
    {url: '/index.html', revision: '383676' }, 
    {url: '/styles/app.0c9a31.css', revision: null},
    {url: '/scripts/app.0d5770.js', revision: null},
]);
複製程式碼

更多預快取請參考官方預快取功能文件

路由功能

路由功能是workbx的核心功能,主要是匹配資源路徑後,採用相應的快取策略,或者自定義快取處理,使用方法如下所示:

import {registerRoute} from 'workbox-routing';
import {CacheFirst} from 'workbox-strategies';

registerRoute(  /\.(?:png|jpg|jpeg|svg|gif)$/,  new CacheFirst({    
    cacheName: 'my-image-cache',   
}));
複製程式碼

registerRoute有兩個引數,第一個引數是一個正規表示式,用來匹配路徑,第二個引數是對匹配路徑進行的處理函式,可以用workbox封裝的快取策略處理函式,也可以自定義,上述示例就是使用的workbox內部封裝的CacheFirst快取策略。

如果第二個引數使用自定義函式,那麼這個函式有三個預設引數,示例如下:

import {registerRoute} from 'workbox-routing';

const handler = async ({url, event, params}) => {   
    // Response will be "A guide to Workbox"  
    return new Response(`A ${params.type} to ${params.name}` );
};
registerRoute(/\.css$/, handler);
複製程式碼

快取策略

Workbox內部封裝了以下五種快取策略:

  • NetworkFirst:網路優先
  • CacheFirst:快取優先
  • NetworkOnly:僅使用正常的網路請求
  • CacheOnly:僅使用快取中的資源
  • StaleWhileRevalidate:從快取中讀取資源的同時傳送網路請求更新本地快取

五種快取策略使用方法一致,各適用於不同的場景,具體適用場景可在離線指南中檢視。

Webpack+Workbox構建離線應用

目前大部分前端專案都離不開webpack,為了方便我們使用workbox,谷歌官方給我們提供了workbox的webpack外掛,通過這個外掛,我們能在專案中快速引入workbox,通過配置來定製化我們的快取。

通過以下四個步驟,我們能將webpack引入到一個由webpack構建的應用中並實現快取。

第一步:使用workbox-webpack-plugin

  1. 安裝
npm install workbox-webpack-plugin
複製程式碼
  1. 在webpack 配置檔案中引入並配置

workbox-webpack-plugin有兩種配置方式:

第一種:GenerateSW

通過配置自動在專案中引入service-worker.js,程式碼如下:

const WorkboxPlugin = require('workbox-webpack-plugin');

module.exports = {  
    // Other webpack config...  
    plugins: [    
        // Other plugins...
        new WorkboxPlugin.GenerateSW({ 
            // Do not precache images
            exclude: [/\.(?:png|jpg|jpeg|svg)$/],
            // Define runtime caching rules.
            runtimeCaching: [{        
                // Match any request that ends with .png, .jpg, .jpeg or .svg.       
                 urlPattern: /\.(?:png|jpg|jpeg|svg)$/,        
                // Apply a cache-first strategy.        
                handler: 'CacheFirst',        
                options: {         
                     // Use a custom cache name.          
                    cacheName: 'images',          
                    // Only cache 10 images.          
                    expiration: {            
                        maxEntries: 10,          
                    },       
                },      
            }],    
        })  
    ]
};
複製程式碼

適用於:

  • 預快取靜態檔案
  • 只簡單的應用執行時的快取功能

不適用:

  • 需要使用service worker 其他功能的場景,如push等
  • 需要在service worker中匯入其他指令碼或新增其他邏輯
  • 具體的配置檔案可查閱官方文件,本文示例使用的是第二種方式

第二種:InjectManifest

通過已有的service-worker.js檔案生成新的service-worker.js,示例如下:

new workboxPlugin.InjectManifest({  
    // 目前的service worker 檔案
    swSrc: './src/sw.js',  
    // 打包後生成的service worker檔案,一般存到disk目錄 
    swDest: 'sw.js'
})
複製程式碼

適用於:

  • 預快取檔案
  • 更多的定製化快取需求
  • 使用service worker其他特性

如果你只想簡單的引入service worker,建議使用第一種方式

第二步:註冊Service Worker

配置好外掛之後,我們需要在專案中註冊service worker。

註冊比較簡單,只需要在專案入口檔案中進行註冊即可,程式碼如下:

// Check that service workers are supported
if ('serviceWorker' in navigator) { 
    // Use the window load event to keep the page load performant  
    window.addEventListener('load', () => {    
        navigator.serviceWorker.register('/sw.js');  
    });
}
複製程式碼

上述程式碼是最簡單的註冊方式,在我們的專案中我們使用register-service-workernpm包註冊service worker並新增一下自定義事件,方便後期進行更新和離線事件的處理。

程式碼如下:

import { register } from 'register-service-worker';

export default function(swDest) {
    console.log("註冊sw")
    register(`/${swDest}`, {
        ready(registration) {
            // 此方法是我們專案中自己封裝的建立自定義事件的公共方法
            dispatchServiceWorkerEvent('sw.ready', registration);
        },
        registered(registration) {
            dispatchServiceWorkerEvent('sw.registered', registration);
        },
        cached(registration) {
            dispatchServiceWorkerEvent('sw.cached', registration);
        },
        updatefound(registration) {
            dispatchServiceWorkerEvent('sw.updatefound', registration);
        },
        updated(registration) {
            dispatchServiceWorkerEvent('sw.updated', registration);
        },
        offline() {
            dispatchServiceWorkerEvent('sw.offline', {});
        },
        error(error) {
            dispatchServiceWorkerEvent('sw.error', error);
        },
    });
}
複製程式碼

在入口檔案中引入註冊檔案:

import registerSW from './sw-register';
registerSW('sw.js');
複製程式碼

第三步:自定義Service Worker快取配置

如果我們使用injectMainfest的方式引入servicce worker,需要在src目錄下建立一個sw.js(命名自定義,但需要和webpack配置中一致),在這個檔案中我們可以進行預快取等操作。程式碼示例如下(sw.js):

import { setCacheNameDetails, clientsClaim } from 'workbox-core';
import { precacheAndRoute } from 'workbox-precaching/precacheAndRoute';
import {createHandlerBoundToURL} from 'workbox-precaching';
import {NavigationRoute, registerRoute} from 'workbox-routing';
import {StaleWhileRevalidate,NetworkOnly} from 'workbox-strategies';

// 設定快取名稱
setCacheNameDetails({
    prefix: 'app',
    suffix: 'v1.0.2',
});

// 更新時自動生效
clientsClaim();

// 預快取檔案,self.__WB_MANIFEST是workbox生成的檔案地址陣列,專案中打包生成的所有靜態檔案都會自動新增到裡面
precacheAndRoute(self.__WB_MANIFEST || []);

// 單頁應用需要應用NavigationRoute進行快取,此處可自定義白名單和黑名單
// 跳過登入和退出頁面的攔截
const handler = createHandlerBoundToURL('/index.html');
const navigationRoute = new NavigationRoute(handler, {
  denylist: [
    /login/,
    /logout/,
  ],
});
registerRoute(navigationRoute);
// 執行時快取配置
// 介面資料使用服務端資料
registerRoute(/^api/,new NetworkOnly());

//圖片cdn地址,屬於跨域資源,我們使用StaleWhileRevalidate快取策略
registerRoute(/^https:\/\/img.xxx.com\//,new StaleWhileRevalidate());
複製程式碼

上述的程式碼有一段針對單頁應用處理的邏輯,應為單頁應用依靠路由變化來載入不同的內容,使用navigationRoute可以匹配導航請求,從而從換從中載入index.html,但預設情況會攔截所有導航請求,如果需要控制,可以在方法中新增白名單和黑名單加以控制。

第四步:處理Service Worker的更新和離線狀態

更新狀態

配置完成後,我們需要注意service worker的更新和離線狀態,service worker的更新較為複雜,如果處理不當回引發各種問題,目前主流的方式就是每次發版,提醒使用者更新,如果使用者點選確定更新,新發版的service worker會替換掉舊的service worker,此程式碼我們專案中放在了入口檔案中(webpack配置的入口檔案),示例程式碼如下:

// sw.updated是在註冊檔案中新增的自定義事件
window.addEventListener('sw.updated', event => {
    const e = event;

    const reloadSW = async () => {
      // Check if there is sw whose state is waiting in ServiceWorkerRegistration
      // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
      const worker = e.detail && e.detail.waiting;

      if (!worker) {
        return true;
      } // Send skip-waiting event to waiting SW with MessageChannel

      await new Promise((resolve, reject) => {
        const channel = new MessageChannel();

        channel.port1.onmessage = msgEvent => {
          if (msgEvent.data.error) {
            reject(msgEvent.data.error);
          } else {
            resolve(msgEvent.data);
          }
        };

        worker.postMessage(
          {
            type: 'skip-waiting',
          },
          [channel.port2],
        );
      }); 
      // Refresh current page to use the updated HTML and other assets after SW has skiped waiting

      window.location.reload(true);
      return true;
    };

    const key = `open${Date.now()}`;
    const btn = (
      <Button
        type="primary"
        onClick={() => {
          notification.close(key);
          reloadSW();
        }}
      >
        確定
      </Button>
    );
    notification.open({
      message: "新版本釋出",
      description: "Boss釋出新版本了,請點選頁面更新",
      btn,
      key,
      onClose: async () => {},
    });
});
複製程式碼

對應sw.js檔案裡面要監聽主執行緒傳遞過來的更新事件,程式碼如下:

// service worker通過message和主執行緒通訊
addEventListener('message', event => {
    const replyPort = event.ports[0];
    const message = event.data;
    console.log(message)
    if (replyPort && message && message.type === 'skip-waiting') {
      event.waitUntil(
        self.skipWaiting().then(
          () =>
            replyPort.postMessage({
              error: null,
            }),
          error =>
            replyPort.postMessage({
              error,
            }),
        ),
      );
    }
});
複製程式碼

離線狀態

對於離線狀態的監聽比較簡單,在入口檔案中新增以下程式碼即可:

window.addEventListener('sw.offline', () => {
    message.warning("當前處於離線狀態",0);
});
複製程式碼

檢查效果

經過上述四個步驟,我們就能將service worker引入到我們已有的用webpack構建的專案上。

如果正常引入,我們可以在控制檯中看到下圖:

Workbox5+Webpack4構建離線應用

總結

  • service worker實現快取有非侵入、持久化、快取內容可控等優點
  • Workbox可以理解為service worker的庫,利用它可以快速進行service worker開發
  • 通過workbox-webpack-plugin可以將workbox引入到現有的用webpack構建的專案中

本文對workbox的介面的解釋較少,需要各位去官網查閱api

參考文獻

相關文章