利用ServiceWorker實現頁面的快速加載和離線訪問

利用ServiceWorker實現頁面的快速加載和離線訪問

Service workers 本質上充當Web應用程序與瀏覽器之間的代理服務器,也能夠在網絡可用時做爲瀏覽器和網絡間的代理。它們旨在(除其餘以外)使得可以建立有效的離線體驗,攔截網絡請求並基於網絡是否可用以及更新的資源是否駐留在服務器上來採起適當的動做。他們還容許訪問推送通知和後臺同步API。(引用自:連接)html

簡單的來講,ServiceWorker(後文簡稱sw)運行在頁面後臺,使用了sw的頁面能夠利用sw來攔截頁面發出的請求,同時配合CacheAPI能夠將請求緩存到客戶本地linux

所以咱們能夠:webpack

  • 將頁面的文件存儲在客戶端,下次打開頁面能夠不向服務器發出資源請求,極大的加快頁面加載速度
  • 離線打開頁面的同時能夠在sw發出請求,更新本地的資源文件
  • 實現離線訪問頁面

可是也存在着一些問題ios

  • 因爲打開頁面再也不向服務器發出頁面請求,所以當服務器上的頁面有新版本的時候客戶端沒法及時升級
  • sw存在必定的兼容性問題

sw-compatible

IE全面撲街,pc上兼容性不太好,移動端安卓支持良好,ios要12+。但考慮到sw並不會影響的頁面的正常運行,因此項目上仍是能投入生產的。git

基本例子

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Sw demo</title>

</head>
<body>
    
</body>
<script src="index.js"></script>
<script>
    if(navigator.serviceWorker){
        navigator.serviceWorker.register('sw.js').then(function(reg){
            if(reg.installing){
                console.log('client-installing');
            }else if(reg.active){
                console.log('client-active')
            }
        })
    }
</script>
</html>

index.js

document.body.innerHTML="hello world!";

sw.js

var cacheStorageKey = 'one';

var cacheList = [
    "index.html",
    "index.js"
]
self.addEventListener('install', function (e) {
    console.log('sw-install');
    self.skipWaiting();
})
self.addEventListener('activate', function (e) {
    console.log('sw-activate');
    caches.open(cacheStorageKey).then(function (cache) {
        cache.addAll(cacheList)
    })
    var cacheDeletePromises = caches.keys().then(cacheNames => {
        return Promise.all(cacheNames.map(name => {
            if (name !== cacheStorageKey) {
                // if delete cache,we should post a message to client which can trigger a callback function
                console.log('caches.delete', caches.delete);
                var deletePromise = caches.delete(name);
                send_message_to_all_clients({ onUpdate: true })
                return deletePromise;
            } else {
                return Promise.resolve();
            }
        }));
    });
    e.waitUntil(
        Promise.all([cacheDeletePromises]
        ).then(() => {
            return self.clients.claim()
        })
    )
})
self.addEventListener('fetch', function (e) {
    e.respondWith(
        caches.match(e.request).then(function (response) {
            if (response != null) {
                console.log(`fetch:${e.request.url} from cache`);
                return response
            } else {
                console.log(`fetch:${e.request.url} from http`);
                return fetch(e.request.url)
            }
        })
    )
})

說明

這樣就完成了一個簡單的sw頁面了,如今經過服務器訪問頁面html、js資源將直接從客戶端本地讀取,實現頁面的快速打開和離線訪問github

  • 客戶端和sw都有不一樣的事件回調,這些事件將在不一樣的sw生命週期中被觸發,後續會有詳細介紹
  • 首次打開頁面的時候sw先進行install回調,執行self.skipWaiting()後將接着執行activate,activate內對緩存列表的文件進行緩存
  • cacheStorageKey是緩存的識別碼,當cacheStorageKey的值變化,sw的activate會將舊的緩存給刪除掉,從新調用cache.addAll設置緩存
  • sw的fetch事件會攔截頁面發出的請求,將根據緩存狀況做出不一樣的處理

生命週期與事件

sw應用的生命週期我簡單抽象爲三種web

  • 安裝:頁面首次打開,加載對應的sw文件
  • 活動:加載過sw文件後,打開頁面
  • 更新:當服務器的sw文件與客戶端的不一致的時候打開頁面

客戶端

名稱 installing active
安裝 觸發 不觸發
活動 不觸發 觸發
更新 不觸發 觸發

sw

名稱 install activate fetch
安裝 觸發 觸發 不觸發
活動 不觸發 不觸發 觸發
更新 觸發 觸發 不觸發

總結一下:npm

  • 客戶端除了在首次打開的時候觸發installing,其餘都是觸發的active
  • sw端活動狀態只執行fetch,安裝和更新狀態只執行install和activate

頁面與sw通訊

通訊方面我以前有翻譯過文章,連接地址,你們感興趣能夠看看。這裏我直接展現把封裝好的通訊接口接口瀏覽器

有了通訊接口,咱們就能夠優化不少事情,比方說在 cacheStorageKey發生變化的時候通知頁面給予客戶必定的響應緩存

客戶端

function send_message_to_sw(msg){
    return new Promise(function(resolve, reject){
        // Create a Message Channel
        var msg_chan = new MessageChannel();
        // Handler for recieving message reply from service worker
        msg_chan.port1.onmessage = function(event){
            if(event.data.error){
                reject(event.data.error);
            }else{
                resolve(event.data);
            }
        };
        // Send message to service worker along with port for reply
        navigator.serviceWorker.controller.postMessage(msg, [msg_chan.port2]);
    });
}

sw

function send_message_to_client(client, msg){
  return new Promise(function(resolve, reject){
      var msg_chan = new MessageChannel();
      msg_chan.port1.onmessage = function(event){
          if(event.data.error){
              reject(event.data.error);
          }else{
              resolve(event.data);
          }
      };
      client.postMessage(msg, [msg_chan.port2]);
  });
}
function send_message_to_all_clients(msg){
  clients.matchAll().then(clients => {
      clients.forEach(client => {
          send_message_to_client(client, msg).then(m => console.log("SW Received Message: "+m));
      })
  })
}

動態緩存資源文件

上述的作法須要事先寫好cacheList,有必定的維護量,如今介紹一種不須要維護cacheList的作法:

self.addEventListener('fetch', function (e) {
  e.respondWith(
    caches.match(e.request).then(res => {
      return res ||
        fetch(e.request)
          .then(res => {
            const resClone = res.clone();
            caches.open(cacheStorageKey).then(cache => {
              cache.put(e.request, resClone);
            })
            return res;
          })
    })
  )
});

這樣作的話緩存資源的操做將從activate轉移到fetch事件內,fetch事件先判斷有沒有緩存,沒有緩存的話將發出對應的請求並進行緩存

這樣的作法的缺點是沒法在首次加載頁面的時候就完成靜態化,由於sw的安裝聲明週期是不會觸發sw的fetch事件的。

頁面url帶參數

針對一些頁面渲染結果與url參數有關的狀況,上述的架構沒法完成對應的本地化需求。以前的作法是在cacheList加入了入口頁面的地址,沒法適應帶動態參數url的狀況。

在fetch內動態緩存請求

具體作法在動態緩存資源文件章節有描述,再也不重複描述。

使用通訊接口通知sw緩存入口頁面

客戶端

navigator.serviceWorker.register(file).then(function (reg) {
    if (reg.installing) {
        //send_message_to_sw({pageUrl:location.href})
    }
    if (reg.active) {
        send_message_to_sw({pageUrl:location.href})
    }
    return reg;
})

sw

self.addEventListener('message',function(e){
  var data=e.data;
  if(data.pageUrl){
    addPage(data.pageUrl)
  }
})
function addPage(page){
  caches.open(cacheStorageKey).then(function(cache){
    cache.add(page);
  })
}

在客戶端的active發消息給sw,sw就可以獲取到對應的頁面url進行緩存。

:客戶端的installing事件無法使用消息接口,你們能夠在sw的activate事件向客戶端發出一個消息請求獲取當前頁面url

常見問題

sw文件至少要與入口頁面文件在同一目錄下,如:

  • /sw.js 能夠管理 /index.html
  • /js/sw.js 不能管理 /index.html

筆者在這裏踩了好久的坑...

webpack-sw-plugin

介紹一個筆者寫的webpack的sw插件,在弄sw頁面的時候很方便,github地址

安裝

npm install --save-dev webpack-sw-plugin

webpack配置

const WebpackSWPlugin = require('webpack-sw-plugin');
module.exports = {
    // entry
    // output
    plugins:[
        new WebpackSWPlugin()
    ]
}

客戶端配置

import worker from 'webpack-sw-plugin/lib/worker';
worker.register({
    onUpdate:()=>{
        console.log('client has a new version. page will refresh in 5s....');
        setTimeout(function(){
            window.location.reload();
        },5000)
    }
});

效果

  • 自動生成頁面與sw交互的體系,無需提供額外的sw文件
  • 自動適配url帶參數的狀況
  • 當webpack輸出文件變化的時候,客戶端的onUpdate將會被觸發,上述例子中當輸出文件變化時,客戶端將會在5秒後進行刷新,刷新後將會使用全新的文件
相關文章
相關標籤/搜索