service worker輕度探索 - 解決運營活動需求中的圖片加載問題?

寫在前面

本文首發於公衆號:符合預期的CoyPan

作過運營活動需求的同窗都知道,通常一個運營活動中會用到不少的圖片資源。用戶訪問首頁時,都會看到一個loading態,表示頁面正在加載所需的全部圖片資源。像下面這樣:javascript

圖片描述

手動加載一個圖片的代碼也很簡單:java

var img = new Image();
img.onload = function(){ ... }
img.src = '圖片地址';

之因此要提早加載全部的圖片,是爲了在後續的頁面中使用圖片時,不會由於須要加載圖片而產生耗時,致使體驗問題。本文所要討論的場景就是:怎麼樣作到在首頁加載圖片後,直接在後面的業務邏輯中直接使用提早加載好的圖片呢?答案就是:把圖片存下來。web

緩存首頁加載的圖片

我能想到的這種場景下的緩存圖片方法有兩種:編程

  1. 使用瀏覽器的緩存。圖片在第一次請求成功後,通常都會設置緩存。在頁面後續的業務邏輯中,若是說想使用某圖片,直接正常發起圖片請求便可,瀏覽器會走緩存,甚至是從內存中直接返回這個圖片。
  2. 將加載好的Image對象直接保存在內存中。這種方法很適用canvas中畫圖的場景,直接把保存下來的Image對象扔到canvas的drawImage中便可。

作業務須要不斷的總結,思考。還能用什麼方法來實現圖片的緩存呢 ? 我嘗試了一下Service Worker,本文將介紹一下Service Worker在這種業務場景下的應用。canvas

本文只是輕輕嘗試了一下Service Worker,並未在線上項目中應用。

Service Worker

Service Worker是PWA的重要組成部分,其包含安裝、激活、等待、銷燬等四個生命週期。主要有如下的特性:瀏覽器

  • 一個獨立的 worker 線程,獨立於當前網頁進程,有本身獨立的 worker context。
  • 一旦被 install,就永遠存在,除非被手動 unregister
  • 用到的時候能夠直接喚醒,不用的時候自動睡眠
  • 可編程攔截代理請求和返回,緩存文件,緩存的文件能夠被網頁進程取到(包括網絡離線狀態)
  • 離線內容開發者可控
  • 能向客戶端推送消息
  • 不能直接操做 DOM
  • 必須在 HTTPS 環境下才能工做( 或 http://localhost )
  • 異步實現,內部大都是經過 Promise 實現

在本文所描述的業務場景中,主要是應用service worker的攔截代理請求和返回的功能。緩存

關於service worker的基礎,谷歌開發者網站上有詳細的介紹,這裏就不贅述了。服務器

地址在這裏:https://developers.google.com...網絡

須要注意的是,service worker必定要謹慎使用,由於它過重要了,一旦註冊,站點的全部請求都會被控制。異步

Service Worker的示例

結合文章開頭所描述的場景,咱們先來寫一些必要的業務函數。

// 加載一個圖片
function loadImage(imgUrl) {
    return new Promise((resolve, reject)=>{
        const img = new Image();
        img.onload = function() {
            resolve();
        };
        img.src = imgUrl;
    });
}

// 加載一堆圖片
function loadImageList(imgList) {
    return Promise.all(imgList.map(function (imgUrl) {
        return loadImage(imgUrl);
    }));
}

下面是service worker的代碼:

self.addEventListener('install', function (event) {
    console.log('install');
});

self.addEventListener('fetch', function (evt) {
    evt.respondWith(
        caches.match(evt.request).then(function(response) {
            if (response) {
                return response;
            }
            const request = evt.request.clone();
            return fetch(request).then(function (response) {
                if (!response || response.status !== 200 || !response.headers.get('Content-type').match(/image/)) {
                    return response;
                }
                const responseClone = response.clone(); // 流數據須要克隆一份。注意事項②
                caches.open('test-cache').then(function (cache) { 
                    cache.put(evt.request, responseClone);
                });
                return response;
            });
        })
    )
});

self.addEventListener('activate', function () {
    console.log('activate');
    clients.claim(); // 首次activate後,就控制頁面。注意事項①
});

註冊完service worker後,咱們就劫持了頁面的全部請求。每一次請求通過service worker時,都會判斷剛請求是否已有緩存,若是有緩存,就直接返回結果。沒有緩存時,纔會向服務器發起請求,而且將圖片請求的結果緩存起來。

在業務代碼中,咱們註冊並使用這個service worker的代碼以下:

// 須要加載的圖片列表
const imgArr = ['http://xxx.jpg', '...'];

// 註冊service worker
function registerServiceWorker() {
    if ('serviceWorker' in navigator) {
        return navigator.serviceWorker.register('http://localhost:8080/service.js');
    } else {
        // 沒有service的處理邏輯省略
    }
}

registerServiceWorker().then(registration => { // 注意事項③
    let serviceWorker;
    if (registration.installing) {
        console.log('registration.installing');
        serviceWorker = registration.installing;
    } else if (registration.waiting) {
        console.log('registration.waiting');
        serviceWorker = registration.waiting;
    } else if (registration.active) {
        console.log('registration.active');
        serviceWorker = registration.active;
        loadImageList(imgArr);
    }
    if (serviceWorker) {
        serviceWorker.addEventListener('statechange', function (e) {
            if(e.target.state === 'activated') {
                // 首次註冊時
                console.log('首次註冊sw時,sw激活');
                loadImageList(imgArr);
            }
        });
    }
}).catch(e => {
    console.log(e);
});

注意事項:

  1. 正常狀況下,service worker剛註冊時,是不會控制頁面的,即沒法攔截到頁面的請求。須要用戶刷新頁面,再次訪問時,service worker纔會攔截頁面請求。這與咱們的需求場景不符合。咱們的需求是:用戶首次訪問請求圖片資源時,就須要對返回的圖片進行緩存。因此,須要在service worker進入activate狀態後,經過clients.claim()來得到頁面的控制權。不過,這種方式並不被提倡
  2. service worker攔截到請求後,咱們須要拷貝返回的數據流,才能存入緩存。
  3. 在業務代碼中,咱們每次都須要調用navigator.serviceWorker.register來拿到一個service worker。瀏覽器會判斷當前service worker的狀態,返回對應的對象。咱們須要保證在service worker準備無誤後,再發起圖片的請求。因爲server worker的自身邏輯須要必定的時間,因此咱們發起圖片請求的時間會被延後。

使用service worker後的效果

以我作的運營活動項目爲例,使用service worker以前,網絡請求是這樣的:

  • 活動頁首頁,首次集中請求圖片

圖片描述

  • 活動頁後續頁面中,使用加載好的圖片:

    圖片描述

使用service-worker以後,網絡請求是這樣的:

  • 活動頁首頁,首次集中請求圖片:

    圖片描述

  • 活動頁後續頁面中,使用加載好的圖片:

    圖片描述

能夠看到,咱們成功使用service worker劫持了頁面的請求,而且將圖片緩存到了瀏覽器的cache storage中。咱們來看一下瀏覽器的緩存。這裏的緩存都是http response。

圖片描述

另外這裏多說一句,可使用下面的代碼,來查看當前網站可使用的瀏覽器本地存儲空間

if ('storage' in navigator && 'estimate' in navigator.storage) {
  navigator.storage.estimate().then(({usage, quota}) => {
    console.log(`Using ${usage} out of ${quota} bytes.`);
  });
}

一些思考

在本文提到的場景中,咱們在用戶首次訪問頁面時,先註冊了service worker,而且使service worker當即控制頁面,而後再開始請求圖片。這種作法延後了圖片請求的發起時間,而且從上面的圖中能夠看到,經過service worker加載圖片的耗時比正常直接請求圖片耗時略長。這些因素致使首屏時間被延後了。另外,做爲運營活動頁,同一個用戶也不會在幾天內屢次訪問,所以service worker的【繞過網絡,當即響應請求】的特性並不能很好地發揮出來。所以,在本文描述的場景中,使用service worker來作緩存並非最佳實踐。

關於service worker作緩存的最佳實踐以及使用場景,能夠查看這篇文章:

https://developers.google.com...

service worker最適合的場景仍是資源離線化,用戶二次進入頁面時能夠達到資源秒加載,不會受網絡情況的影響。

寫在後面

本文從業務的角度出發,輕度探索了service worker在文章開頭給出的業務場景中的應用。後續會考慮在合適的業務場景中進行應用。


圖片描述

相關文章
相關標籤/搜索