寫在前面
本文首發於公眾號:符合預期的CoyPan
做過運營活動需求的同學都知道,一般一個運營活動中會用到很多的圖片資源。使用者訪問首頁時,都會看到一個loading態,表示頁面正在載入所需的所有圖片資源。像下面這樣:
手動載入一個圖片的程式碼也很簡單:
var img = new Image();
img.onload = function(){ ... }
img.src = '圖片地址';
複製程式碼
之所以要提前載入所有的圖片,是為了在後續的頁面中使用圖片時,不會因為需要載入圖片而產生耗時,導致體驗問題。本文所要討論的場景就是:怎麼樣做到在首頁載入圖片後,直接在後面的業務邏輯中直接使用提前載入好的圖片呢?答案就是:把圖片存下來。
快取首頁載入的圖片
我能想到的這種場景下的快取圖片方法有兩種:
- 使用瀏覽器的快取。圖片在第一次請求成功後,一般都會設定快取。在頁面後續的業務邏輯中,如果說想使用某圖片,直接正常發起圖片請求即可,瀏覽器會走快取,甚至是從記憶體中直接返回這個圖片。
- 將載入好的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);
});
複製程式碼
注意事項:
-
正常情況下,service worker剛註冊時,是不會控制頁面的,即無法攔截到頁面的請求。需要使用者重新整理頁面,再次訪問時,service worker才會攔截頁面請求。這與我們的需求場景不符合。我們的需求是:使用者首次訪問請求圖片資源時,就需要對返回的圖片進行快取。所以,需要在service worker進入activate狀態後,通過clients.claim()來獲得頁面的控制權。不過,這種方式並不被提倡。
-
service worker攔截到請求後,我們需要拷貝返回的資料流,才能存入快取。
-
在業務程式碼中,我們每次都需要呼叫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/web/fundamentals/primers/service-workers/high-performance-loading?hl=zh-cn
service worker最適合的場景還是資源離線化,使用者二次進入頁面時可以達到資源秒載入,不會受網路狀況的影響。
寫在後面
本文從業務的角度出發,輕度探索了service worker在文章開頭給出的業務場景中的應用。後續會考慮在合適的業務場景中進行應用。