乐趣区

service worker轻度探索 – 解决运营活动需求中的图片加载问题?

写在前面
本文首发于公众号:符合预期的 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…
需要注意的是,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…
service worker 最适合的场景还是资源离线化,用户二次进入页面时可以达到资源秒加载,不会受网络状况的影响。
写在后面
本文从业务的角度出发,轻度探索了 service worker 在文章开头给出的业务场景中的应用。后续会考虑在合适的业务场景中进行应用。

退出移动版