ServiceWorker 担任了服务器与浏览器的中间人角色,如果网站中注册了 ServiceWorker 那么它可以拦截当前网所有的请求,并做相应的处理动作。

ServiceWorker 内容就是一段 JavaScript 脚本,内部可以编写相应的处理逻辑,比如对请求进行缓存处理,能直接使用缓存的就直接返回缓存不再转给服务器,从而大大提高浏览体验。有些开源工具包存在多个 CDN 站点,使用 ServiceWorker 可以实现自动寻找访问最快的站点,如果某个站点发生错误,可以自动切换,FreeCDN 便是借此实现的。

一、概念

ServiceWorker 主要特性有:

  1. ServiceWorker 是一个独立于 JavaScript 主线程的独立线程,在里面执行需要消耗大量资源的操作不会堵塞主线程;
  2. 具有离线缓存的能力,可以访问 cache,可以让开发者自己控制和管理缓存内容和版本;
  3. 支持消息推送。

由于 ServiceWorker 的这些特殊功能,导致其有比较高的安全性控制。

  1. ServiceWorker 脚本的路径与当前站点同不能跨域;
  2. ServiceWorker 的作用域必须小于或等于 ServiceWorker 脚本所在的路径,如脚本在 js/ 路径下,则安装后的 ServiceWorker 最多也只能在该路径下生效;
  3. 如果站点是 Https 站点,那么 ServiceWorker 脚本也必须是 Https,并且证书必须针对当前域名有效;
  4. 或许还有其他限制……

ServiceWorker 采用事件触发的机制,接触比较多的主要有三个事件:

  1. install 事件,在脚本加载完成是执行;
  2. active 事件,在脚本激活时执行,一般在此处通过调用 self.clients.claim( ) 取得页面的控制权,也可以做一些缓存的更新操作。
  3. fetch 事件,拦截到请求时执行,ServiceWorker 最为重要的部分,决定了是采用缓存还是发起新的请求。

二、两个 ServiceWorker 的脚本实现

脚本来源于网络,本文摘抄记录了一下。

2.1 脚本一

ServiceWorker 脚本主要作用是用于监听 GitHubcombinenpm 开源库的访问,识别某个请求是否属于某个开源库,如果是的话将对该库的多个 CDN 站点同时发起请求,然后选响应最快的一个站点。

同时也具备脚本缓存功能,但不是很完善。

javascript
  • 001
  • 002
  • 003
  • 004
  • 005
  • 006
  • 007
  • 008
  • 009
  • 010
  • 011
  • 012
  • 013
  • 014
  • 015
  • 016
  • 017
  • 018
  • 019
  • 020
  • 021
  • 022
  • 023
  • 024
  • 025
  • 026
  • 027
  • 028
  • 029
  • 030
  • 031
  • 032
  • 033
  • 034
  • 035
  • 036
  • 037
  • 038
  • 039
  • 040
  • 041
  • 042
  • 043
  • 044
  • 045
  • 046
  • 047
  • 048
  • 049
  • 050
  • 051
  • 052
  • 053
  • 054
  • 055
  • 056
  • 057
  • 058
  • 059
  • 060
  • 061
  • 062
  • 063
  • 064
  • 065
  • 066
  • 067
  • 068
  • 069
  • 070
  • 071
  • 072
  • 073
  • 074
  • 075
  • 076
  • 077
  • 078
  • 079
  • 080
  • 081
  • 082
  • 083
  • 084
  • 085
  • 086
  • 087
  • 088
  • 089
  • 090
  • 091
  • 092
  • 093
  • 094
  • 095
  • 096
  • 097
  • 098
  • 099
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
const cacheStorageKey = "check-dream-2.0" const origin = [ "https://blog.nineya.com/", ]; const cdn = { gh: { jsdelivr: "https://cdn.jsdelivr.net/gh", pigax_jsd: "https://u.pigax.cn/gh", pigax_chenyfan_jsd: "https://cdn-jsd.pigax.cn/gh", tianli: "https://cdn1.tianli0.top/gh", }, combine: { jsdelivr: "https://cdn.jsdelivr.net/combine", pigax_jsd: "https://u.pigax.cn/combine", pigax_chenyfan_jsd: "https://cdn-jsd.pigax.cn/combine", tianli: "https://cdn1.tianli0.top/combine", }, npm: { eleme: "https://npm.elemecdn.com", jsdelivr: "https://cdn.jsdelivr.net/npm", zhimg: "https://unpkg.zhimg.com", unpkg: "https://unpkg.com", pigax_jsd: "https://u.pigax.cn/npm", pigax_unpkg: "https://unpkg.pigax.cn/", pigax_chenyfan_jsd: "https://cdn-jsd.pigax.cn/npm", tianli: "https://cdn1.tianli0.top/npm", }, }; // 脚本加载完毕执行时 self.addEventListener("install", (event) => { event.waitUntil( caches.open(cacheStorageKey) .then(function () { return self.skipWaiting(); }) ) }); // 监听所有请求 self.addEventListener("activate", (event) => { event.waitUntil( //获取所有cache名称 caches.keys().then(function (cacheNames) { return Promise.all( //移除不是该版本的所有资源 cacheNames.filter(function (cacheName) { return cacheName !== cacheStorageKey }).map(function (cacheName) { return caches.delete(cacheName) }) ) }).then(function () { //在新安装的 SW 中通过调用 self.clients.claim( ) 取得页面的控制权,这样之后打开页面都会使用版本更新的缓存。 return self.clients.claim() }) ) }); self.addEventListener("fetch", (event) => { event.respondWith(caches.match(event.request).then(response => { if (response != null) { return response; } handleRequest(event.request) .then((result) => { caches .open(cacheStorageKey) .then(cache => { cache.put(event.request, result) }); return result; }) .catch(() => 0); })) }); // 返回响应 async function progress(res) { return new Response(await res.arrayBuffer(), { status: res.status, headers: res.headers, }); } function handleRequest(req) { const urls = []; const urlStr = req.url; let urlObj = new URL(urlStr); // 为了获取 cdn 类型 // 例如获取gh (https://cdn.jsdelivr.net/gh) const path = urlObj.pathname.split("/")[1]; // 匹配 cdn for (const type in cdn) { if (type === path) { for (const key in cdn[type]) { const url = cdn[type][key] + urlObj.pathname.replace("/" + path, ""); urls.push(url); } } } // 如果上方 cdn 遍历 匹配到 cdn 则直接统一发送请求 if (urls.length) return fetchAny(urls); // 将用户访问的当前网站与所有源站合并 let origins = [location.origin, ...origin]; // 遍历判断当前请求是否是源站主机 const is = origins.find((i) => { const {hostname} = new URL(i); const reg = new RegExp(hostname); return urlStr.match(reg); }); // 不是源站则直接请求返回结果 if (!is) return fetch(urlStr).then(progress); // 如果以上都不是,则将当前访问的url参数追加到所有源站后,统一请求。 // 谁先返回则使用谁的返回结果 origins = origins.map((i) => i + urlObj.pathname + urlObj.search); return fetchAny(origins); } // Promise.any 的 polyfill function createPromiseAny() { Promise.any = function (promises) { return new Promise((resolve, reject) => { promises = Array.isArray(promises) ? promises : []; let len = promises.length; let errs = []; if (len === 0) return reject(new AggregateError("All promises were rejected")); promises.forEach((p) => { if (!p instanceof Promise) return reject(p); p.then( (res) => resolve(res), (err) => { len--; errs.push(err); if (len === 0) reject(new AggregateError(errs)); } ); }); }); }; } // 发送所有请求 function fetchAny(urls) { // 中断一个或多个请求 const controller = new AbortController(); const signal = controller.signal; // 遍历将所有的请求地址转换为promise const PromiseAll = urls.map((url) => { return new Promise(async (resolve, reject) => { fetch(url, {signal}) .then(progress) .then((res) => { if (res.status !== 200) reject(null); controller.abort(); // 中断 resolve(res); }) .catch(() => reject(null)); }); }); // 判断浏览器是否支持 Promise.any if (!Promise.any) createPromiseAny(); // 谁先返回"成功状态"则返回谁的内容,如果都返回"失败状态"则返回null return Promise.any(PromiseAll) .then(res => res) .catch(() => null); }

2.2 脚本二

该脚本主要功能是对指定的路径进行本地缓存,并可以对缓存进行版本指定。

javascript
  • 001
  • 002
  • 003
  • 004
  • 005
  • 006
  • 007
  • 008
  • 009
  • 010
  • 011
  • 012
  • 013
  • 014
  • 015
  • 016
  • 017
  • 018
  • 019
  • 020
  • 021
  • 022
  • 023
  • 024
  • 025
  • 026
  • 027
  • 028
  • 029
  • 030
  • 031
  • 032
  • 033
  • 034
  • 035
  • 036
  • 037
  • 038
  • 039
  • 040
  • 041
  • 042
  • 043
  • 044
  • 045
  • 046
  • 047
  • 048
  • 049
  • 050
  • 051
  • 052
  • 053
  • 054
  • 055
  • 056
  • 057
  • 058
  • 059
  • 060
  • 061
  • 062
  • 063
  • 064
  • 065
  • 066
  • 067
  • 068
  • 069
  • 070
  • 071
  • 072
  • 073
  • 074
  • 075
  • 076
  • 077
  • 078
  • 079
  • 080
  • 081
  • 082
  • 083
  • 084
  • 085
  • 086
  • 087
  • 088
  • 089
  • 090
  • 091
  • 092
  • 093
  • 094
  • 095
  • 096
  • 097
  • 098
  • 099
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
var assetsToCache = []; //对request url 进行匹配的,而不是当前的页面地址匹配 const caceheList = [ "themes/",//handsome内置js "upload/",// 文章中的图片 "vditor", "jquery", "bootstrap", "mathjax", "mdui", "?action=get_search_cache", "hm.js" //百度统计js ]; const notCacheList = [ "/admin/" ] //添加缓存 self.addEventListener('install', function(event) { event.waitUntil(self.skipWaiting()) //这样会触发activate事件 }); // self.addEventListener('message', function (event) { // console.log("recv message" + event.data); // if (event.data === 'skipWaiting') { // self.skipWaiting(); // console.log("skipwaiting"); // } // }) //可以进行版本修改,删除缓存 var version = "9.0.0"; var versionTag = "62625b61dfa93"; var CACHE_NAME = version+versionTag; self.addEventListener('activate', function(event) { // console.log('activated!'); var mainCache = [CACHE_NAME]; event.waitUntil( caches.keys().then(function(cacheNames) { return Promise.all( cacheNames.map(function(cacheName) { if ( mainCache.indexOf(cacheName) === -1) {//没有找到该版本号下面的缓存 // When it doesn't match any condition, delete it. console.info('version changed, clean the cache, SW: deleting ' + cacheName); return caches.delete(cacheName); } }) ); }) ); return self.clients.claim(); }); function isExitInCacheList(list,url){ return list.some(function (value) { return url.indexOf(value) !== -1 }) } var CDN_ADD = "" //博客本地图片资源替换 var BLOG_URL = "https://www.ihewro.com" //博客本地图片资源替换 function fetchLocal(event){ // console.log("fectch error",CDN_ADD,BLOG_URL) // 判断地址前缀是否是CDN_ADD,进行回退 if (CDN_ADD!="" && BLOG_URL!= CDN_ADD && event.request.url.indexOf(CDN_ADD)!==-1){ const new_request_url = event.request.url.replace(CDN_ADD,BLOG_URL); return caches.open(CACHE_NAME).then(function(cache) { return fetch(new_request_url).then(function (response) { if (response.status < 400) {//回退成功,则进行缓存,这个地方肯定是可以获取到status因为地址替换成本地的了 // console.log("【yes2】 put in the cache" + event.request.url); console.log("fetch retry [success],old_url:%s ,new_url:%s",event.request.url,new_request_url); cache.put(event.request, response.clone()); }else{ console.warn("fetch retry [error:%s],old_url:%s,new_url:%s",response.status,event.request.url,new_request_url); } // console.log(response); return response; }).catch(function (error){ console.warn("fetch retry [error2:%s],old_url:%s,new_url:%s",error,event.request.url,new_request_url); // throw error; }); }) }else{ console.warn("fetch error and [not retry]",event.request.url); return false; } } function is_same_request(urla,urlb){ const white_query =new Set([// 除了这这些参数,其它的查询参数必须要一致,才认为是同一个请求 "t", "v", "version", "time", "ts", "timestamp" ]); const a_url = urla.split('?'); const b_url = urlb.split('?'); if (a_url[0] !== b_url[0] ){ return false; } const a_params = new URLSearchParams('?' + a_url[1]); const b_params = new URLSearchParams('?' + b_url[1]); // 显示所有的键 for (var key of a_params.keys()) { if (white_query.has(key)){//对于版本号的key 忽略 continue; } if (a_params.get(key) !== b_params.get(key)){//其它key的值必须相等,比如type=POST 这种 return false; } } return true; } function getMatchRequestResponse(cache_response_list,request_url) { if (cache_response_list){ for (const cache_response of cache_response_list) { // console.log(cache_response.url,request_url) if (is_same_request(cache_response.url,request_url)){ return cache_response; } } } return null; } // 拦截请求使用缓存的内容 self.addEventListener('fetch', function(event) { // console.log('Handling fetch event for', event.request.url); if(event.request.method !== "GET") { return false; }else{ if (isExitInCacheList(caceheList, event.request.url) && !isExitInCacheList(notCacheList, event.request.url)){ // 只捕获需要加入cache的请求 // 劫持 HTTP Request // console.log(event.request.url); event.respondWith( caches.open(CACHE_NAME).then(function(cache) { // var start = performance.now(); // return cache.match(event.request,{"ignoreSearch":true}).then(function(cache_response) { return cache.matchAll(event.request,{"ignoreSearch":true}).then(function(cache_response_list) { const cache_response = getMatchRequestResponse(cache_response_list,event.request.url); // var end = performance.now(); // console.log("match all cost:",end - start,"ms",event.request.url) if (cache_response && cache_response.url === event.request.url) {//地址(包含查询参数)完全一致才返回缓存 // 使用 Service Worker 回應 // console.log("【cache】use the cache " + event.request.url) return cache_response; } else { // console.log("not use cache",event.request.url); return fetch(event.request) .then(function(response) { //判断查询参数里面是否存在type参数,如果存在 // 由于跨域访问导致获得response是非透明响应无法获取响应码(响应码是0 //https://fetch.spec.whatwg.org/#concept-filtered-response-opaque if (response.status < 400){//对于响应码为0,暂时无法进一步判断,只能全部认为加载成功 //跨域的地址 服务器端的错误目前不会回退,只能直接加到cache里面,如果服务器问题解除需要更新缓存 // console.log("【yes】 put in the cache" + event.request.url); if (cache_response && is_same_request(cache_response.url,event.request.url)){//删除旧版本号的资源 // console.log("存在缓存,但是可查询的字符串版本号不一致,所以需要删除缓存",cache_response.url,event.request.url) cache.delete(cache_response.url); } cache.put(event.request, response.clone()); }else{ console.warn("response is not ok",response.status,response.statusText,event.request.url); const new_response = fetchLocal(event); if (new_response){ return new_response; }else {//在获取response 失败的时候,优先考虑可以回退旧版本的response里面 if (cache_response && is_same_request(cache_response.url,event.request.url)){ return cache_response; } return response; } } return response; }) .catch(function(error) { console.error(error) const response = fetchLocal(event); if (response){ return response; }else { // console.log('Fetching request url ,' +event.request.url+' failed:', error); // throw error; } }); } }) }) ); }else{ return false; } } });