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

CoyPan發表於2019-03-04

寫在前面

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

做過運營活動需求的同學都知道,一般一個運營活動中會用到很多的圖片資源。使用者訪問首頁時,都會看到一個loading態,表示頁面正在載入所需的所有圖片資源。像下面這樣:

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

手動載入一個圖片的程式碼也很簡單:

var img = new Image();
img.onload = function(){ ... }
img.src = '圖片地址';
複製程式碼

之所以要提前載入所有的圖片,是為了在後續的頁面中使用圖片時,不會因為需要載入圖片而產生耗時,導致體驗問題。本文所要討論的場景就是:怎麼樣做到在首頁載入圖片後,直接在後面的業務邏輯中直接使用提前載入好的圖片呢?答案就是:把圖片存下來。

快取首頁載入的圖片

我能想到的這種場景下的快取圖片方法有兩種:

  1. 使用瀏覽器的快取。圖片在第一次請求成功後,一般都會設定快取。在頁面後續的業務邏輯中,如果說想使用某圖片,直接正常發起圖片請求即可,瀏覽器會走快取,甚至是從記憶體中直接返回這個圖片。
  2. 將載入好的Image物件直接儲存在記憶體中。這種方法很適用canvas中畫圖的場景,直接把儲存下來的Image物件扔到canvas的drawImage中即可。

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

本文只是輕輕嘗試了一下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/web/fundamentals/primers/service-workers/?hl=zh-cn

需要注意的是,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輕度探索 - 解決運營活動需求中的圖片載入問題?

使用service-worker之後,網路請求是這樣的:

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

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

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

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

可以看到,我們成功使用service worker劫持了頁面的請求,並且將圖片快取到了瀏覽器的cache storage中。我們來看一下瀏覽器的快取。這裡的快取都是http response。

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

另外這裡多說一句,可以使用下面的程式碼,來檢視當前網站可以使用的瀏覽器本地儲存空間

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/web/fundamentals/primers/service-workers/high-performance-loading?hl=zh-cn

service worker最適合的場景還是資源離線化,使用者二次進入頁面時可以達到資源秒載入,不會受網路狀況的影響。

寫在後面

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


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

相關文章