有一个困扰 web 用户多年的难题——丢失网络连接。即使是世界上最好的 web app,如果下载不了它,也是非常糟糕的体验。如今虽然已经有很多种技术去尝试着解决这一问题。而随着离线页面的出现,一些问题已经得到了解决。但是,最重要的问题是,仍然没有一个好的统筹机制对资源缓存和自定义的网络请求进行控制。
之前的尝试 — AppCache — 看起来是个不错的方法,因为它可以很容易地指定需要离线缓存的资源。但是,它假定你使用时会遵循诸多规则,如果你不严格遵循这些规则,它会把你的APP搞得一团糟。关于APPCache的更多详情,请看Jake Archibald的文章: Application Cache is a
Service worker 最终要去解决这些问题。虽然 Service Worker 的语法比 AppCache 更加复杂,但是你可以使用 JavaScript 更加精细地控制 AppCache 的静默行为。有了它,你可以解决目前离线应用的问题,同时也可以做更多的事。 Service Worker 可以使你的应用先访问本地缓存资源,所以在离线状态时,在没有通过网络接收到更多的数据前,仍可以提供基本的功能(一般称之为 Offline First)。这是原生APP 本来就支持的功能,这也是相比于 web app,原生 app 更受青睐的主要原因。
通常遵循以下基本步骤来使用 service workers:
service worker URL 通过 serviceWorkerContainer.register() 来获取和注册。
如果注册成功,service worker 就在 ServiceWorkerGlobalScope 环境中运行; 这是一个特殊类型的 woker 上下文运行环境,与主运行线程(执行脚本)相独立,同时也没有访问 DOM 的能力。
service worker 现在可以处理事件了。
受 service worker 控制的页面打开后会尝试去安装 service worker。最先发送给 service worker 的事件是安装事件(在这个事件里可以开始进行填充 IndexDB和缓存站点资源)。这个流程同原生 APP 或者 Firefox OS APP 是一样的 — 让所有资源可离线访问。
当 oninstall 事件的处理程序执行完毕后,可以认为 service worker 安装完成了。
下一步是激活。当 service worker 安装完成后,会接收到一个激活事件(activate event)。 onactivate 主要用途是清理先前版本的service worker 脚本中使用的资源。
Service Worker 现在可以控制页面了,但仅是在 register() 成功后的打开的页面。也就是说,页面起始于有没有 service worker ,且在页面的接下来生命周期内维持这个状态。所以,页面不得不重新加载以让 service worker 获得完全的控制。
下图展示了 service worker 所有支持的事件:
Promises 是一种非常适用于异步操作的机制,一个操作依赖于另一个操作的成功执行。这是 service worker 的核心工作机制。
Promises 可以做很多事情。但现在,你只需要知道,如果有什么返回了一个promise,你可以在后面加上 .then() 来传入成功和失败的回调函数。或者,你可以在后面加上 .catch() 如果你想添加一个操作失败的回调函数。
接下来,让我们对比一下传统的同步回调结构,和异步promise结构,两者在功能上是等效的:
try {
var value = myFunction();
console.log(value);
} catch(err) {
console.log(err);
}
myFunction().then(function(value) {
console.log(value);
}).catch(function(err) {
console.log(err);
});
在上面第一个例子中,我们必须等待 myFunction( ) 执行完成,并返回 value值,在此之前,后续其它的代码无法执行。在第二个例子中,myFunction( ) 返回一个promise对象,下面的代码可以继续执行。当promise成功resolves后,then( ) 中的函数会异步地执行。
现在来举下实际的例子 — 如果我们想动态地加载图片,而且要在图片下载完成后再展示到页面上,要怎么实现呢?这是一个比较常见的场景,但是实现起来会有点麻烦。我们可以使用 .onload 事件处理程序,来实现图片的加载完成后再展示。但是如果图片的 onload事件发生在我们监听这个事件之前呢?我们可以使用 .complete来解决这个问题,但是仍然不够简洁,如果是多个图片该怎么处理呢?并且,这种方法仍然是同步的操作,会阻塞主线程。
相比于以上方法,我们可以使用 promise 来实现。(可以看我们的 Promises test 示例源码, look at it running live.)
Note: service worker在实际使用中,会使用 caching 和 onfetch 等异步操作,而不是使用老旧的 XMLHttpRequest API。这里的例子使用 XMLHttpRequest API只是为了让你能将注意力集中于理解 Promise上。
function imgLoad(url) {
return new Promise(function(resolve, reject) {
var request = new XMLHttpRequest();
request.open('GET', url);
request.responseType = 'blob';
request.onload = function() {
if (request.status == 200) {
resolve(request.response);
} else {
reject(Error('Image didn\'t load successfully; error code:' + request.statusText));
}
};
request.onerror = function() {
reject(Error('There was a network error.'));
};
request.send();
});
}
我们使用 Promise( ) 构造函数返回了一个新的promise对象,构造函数接收一个回调函数作为参数。这个回调函数包含两个参数,第一个为成功执行(resolve)的回调函数,第二个为执行失败(reject)的回调函数。我们将这两个回调函数在对应的时机执行。在这个例子中,resolve会在请求返回状态码200的时候执行,reject会在请求返回码为非200的时候执行。上面代码的其余部分基本都是XHR的相关操作,现在不需要过多关注。
当我们调用 imgLoad( ) 函数时,传入要加载的图片url作为参数。然后,后面的代码与同步方式会有点不同:
var body = document.querySelector('body');
var myImage = new Image();
imgLoad('myLittleVader.jpg').then(function(response) {
var imageURL = window.URL.createObjectURL(response);
myImage.src = imageURL;
body.appendChild(myImage);
}, function(Error) {
console.log(Error);
});
在函数调用后面,我们串联了 promise 的 then() 方法。then() 接受两个函数 —— 第一个函数在 promise 成功执行的情况下执行,而第二个函数则在 promise 执行失败情况下执行。当执行成功时,在 myImage 中显示图片,并追加到 body 里面(它的参数就是传递给 promise 的 resolve 方法的 request.response );当执行失败时,在控制台返回一个错误。
注意: 你可以链式调用 promise,比如: myPromise().then(success, failure).then(success).catch(failure);
注意: 你可以阅读 Jake Archibald 的精彩的文章 JavaScript Promises: there and back again 了解更多关于 promise 的内容
我们 app 的 JavaScript 文件里 — app.js — 的第一块代码就像下面的一样。这是我们使用 service worker 的入口:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw-test/sw.js', { scope: '/sw-test/' }).then(function(reg) {
// registration worked
console.log('Registration succeeded. Scope is ' + reg.scope);
}).catch(function(error) {
// registration failed
console.log('Registration failed with ' + error);
});
}
这就注册了一个 service worker,它工作在 worker context,所以没有访问 DOM 的权限。在正常的页面之外运行 service worker 的代码来控制它们的加载。
让我们从一个代码示例来开始这个部分——这是 这是我们的 service worker 里的第一块代码 :
this.addEventListener('install', function(event) {
event.waitUntil(
caches.open('v1').then(function(cache) {
return cache.addAll([
'/sw-test/',
'/sw-test/index.html',
'/sw-test/style.css',
'/sw-test/app.js',
'/sw-test/image-list.js',
'/sw-test/star-wars-logo.jpg',
'/sw-test/gallery/',
'/sw-test/gallery/bountyHunters.jpg',
'/sw-test/gallery/myLittleVader.jpg',
'/sw-test/gallery/snowTroopers.jpg'
]);
})
);
});
你可以给 service worker 添加一个 fetch 的事件监听器,接着调用 event 上的 respondWith() 方法来劫持我们的 HTTP 响应,然后你用可以用自己的魔法来更新他们。
this.addEventListener('fetch', function(event) {
event.respondWith(
// magic goes here
);
});
我们可以用一个简单的例子开始,在任何情况下我们只是简单的响应这些缓存中的 url 和网络请求匹配的资源。
this.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
);
});
caches.match(event.request) 允许我们对网络请求的资源和 cache 里可获取的资源进行匹配,查看是否缓存中有相应的资源。这个匹配通过 url 和 vary header进行,就像正常的 http 请求一样。
让我们看看我们在定义我们的魔法时的一些其他的选项 (查看 Fetch API documentation 了解更多有关Request 和 Response 对象的更多信息。)
恢复失败的请求节
在有 service worker cache 里匹配的资源时, caches.match(event.request) 是非常棒的。但是如果没有匹配资源呢?如果我们不提供任何错误处理,promise 就会 reject,同时也会出现一个网络错误。
幸运的是,service worker 的基于 promise 的结构,使得提供更多的成功的选项变得微不足道。 我们可以这样做:
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
});
如果 promise reject了, catch() 函数会执行默认的网络请求,意味着在网络可用的时候可以直接像服务器请求资源。
如果我们足够聪明的话,我们就不会只是从服务器请求资源,而且还会把请求到的资源保存到缓存中,以便将来离线时所用!这意味着如果其他额外的图片被加入到 Star Wars 图库里,我们的 app 会自动抓取它们。下面就是这个诀窍:
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(resp) {
return resp || fetch(event.request).then(function(response) {
return caches.open('v1').then(function(cache) {
cache.put(event.request, response.clone());
return response;
});
});
})
);
});
这里我们用 fetch(event.request) 返回了默认的网络请求,它返回了一个 promise 。当网络请求的 promise 成功的时候,我们 通过执行一个函数用 caches.open(‘v1’) 来抓取我们的缓存,它也返回了一个 promise。当这个 promise 成功的时候, cache.put() 被用来把这些资源加入缓存中。资源是从 event.request 抓取的,它的响应会被 response.clone() 克隆一份然后被加入缓存。这个克隆被放到缓存中,它的原始响应则会返回给浏览器来给调用它的页面。
为什么要这样做?这是因为请求和响应流只能被读取一次。为了给浏览器返回响应以及把它缓存起来,我们不得不克隆一份。所以原始的会返回给浏览器,克隆的会发送到缓存中。它们都是读取了一次。
我们现在唯一的问题是当请求没有匹配到缓存中的任何资源的时候,以及网络不可用的时候,我们的请求依然会失败。让我们提供一个默认的回退方案以便不管发生了什么,用户至少能得到些东西:
this.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function() {
return fetch(event.request).then(function(response) {
return caches.open('v1').then(function(cache) {
cache.put(event.request, response.clone());
return response;
});
});
}).catch(function() {
return caches.match('/sw-test/gallery/myLittleVader.jpg');
})
);
});
因为只有新图片会失败,我们已经选择了回退的图片,一切都依赖我们之前看到的 install 事件侦听器中的安装过程。
更新你的service worker
如果你的 service worker 已经被安装,但是刷新页面时有一个新版本的可用,新版的 service worker 会在后台安装,但是还没激活。当不再有任何已加载的页面在使用旧版的 service worker 的时候,新版本才会激活。一旦再也没有更多的这样已加载的页面,新的 service worker 就会被激活。
你想把你的新版的 service worker 里的 install 事件监听器改成下面这样(注意新的版本号):
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('v2').then(function(cache) {
return cache.addAll([
'/sw-test/',
'/sw-test/index.html',
'/sw-test/style.css',
'/sw-test/app.js',
'/sw-test/image-list.js',
…
// include other new resources for the new version...
]);
})
);
});
当安装发生的时候,前一个版本依然在响应请求,新的版本正在后台安装,我们调用了一个新的缓存 v2,所以前一个 v1 版本的缓存不会被扰乱。
当没有页面在使用当前的版本的时候,这个新的 service worker 就会激活并开始响应请求。
删除旧缓存节
你还有个 activate 事件。当之前版本还在运行的时候,一般被用来做些会破坏它的事情,比如摆脱旧版的缓存。在避免占满太多磁盘空间清理一些不再需要的数据的时候也是非常有用的,每个浏览器都对 service worker 可以用的缓存空间有个硬性的限制。浏览器尽力管理磁盘空间,但它可能会删除整个域的缓存。浏览器通常会删除域下面的所有的数据。
传给 waitUntil() 的 promise 会阻塞其他的事件,直到它完成。所以你可以确保你的清理操作会在你的的第一次 fetch 事件之前会完成。
self.addEventListener('activate', function(event) {
var cacheWhitelist = ['v2'];
event.waitUntil(
caches.keys().then(function(keyList) {
return Promise.all(keyList.map(function(key) {
if (cacheWhitelist.indexOf(key) === -1) {
return caches.delete(key);
}
}));
})
);
});
[
资源出处:详细请跳转(https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API/Using_Service_Workers)