关于Service Worker
Service Worker
是浏览器的一个高级特性,本质是一个 Web Worker
,是独立于网页运行的脚本。 Web Worker
这个api被造出来时,就是为了解放主线程。因为,浏览器中的 JavaScript
都是运行在单一个线程上,随着web业务变得越来越复杂,js中耗时间、耗资源的运算过程则会导致各种程度的性能问题。 而Web Worker由于独立于主线程,则可以将一些复杂的逻辑交由它来去做,完成后再通过 postMessage
的方法告诉主线程。 Service Worker
则是 Web Worker
的升级版本,相较于后者,前者拥有了持久离线缓存的能力。
Service Workers
本质上充当 Web
应用程序、浏览器与网络(可用时)之间的代理服务器。这个 API
旨在创建有效的离线体验,它会拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API
。
Service Worker
运行在 Worker
上下文,因此它不能访问 DOM。相对于驱动应用的主 JavaScript
线程,它运行在其他线程中,所以不会造成阻塞。它设计为完全异步,同步 API
(如XHR
和localStorage
)不能在 Service Worker
中使用。
Service Worker
下文简称SW
上文是较为科学的解释,对我来说他的用处只有一个
就是加速博客
前言
因为作者看到好多人的博客都用上了SW。
而且博主万年没更新了 (其实是因为没时间 )
所以准备水 一篇文章。
本文应该只需要懂js基础的小白即可看懂(没有js基础应该也可以明白原理)
但建议配合MDN食用
本人不保证本文没有任何错误,
本文旨在教学/科普那想搞sw的人
本文是根据个人理解所写,不保证完全正确
正文
SW的组成(状态)
出于安全考量,Service workers 只能由 HTTPS 承载,毕竟修改网络请求的能力暴露给中间人攻击会非常危险。
但在本地的 localhost
和 127.0.0.1
是可以使用的(为了方便本地调试)
SW一共有很多种状态,但我们只需要知道其中4个(其他的基本上用不到)
install 安装
waiting 等待
activate 激活
fetch (额,这玩意我也不知道叫啥,大概可以算是请求)
SW在通过安装代码的注册方法 navigator.serviceWorker.register()
后
会下载SW代码
然后进入install
状态
如果不是首次使用sw他就会卡在waiting
状态
需要等待上一个sw停止工作
此时我们就要执行 self.skipWaiting()
来跳过
然后就会进入activate
状态
首次使用sw的时候,并不会直接捕获当前页面的请求
我们可以执行 self.clients.claim()
来立即管理当前页面(或者你安装的时候直接刷新也能达到此目的)
监听SW状态
我们将会在我们手搓的sw中使用 addEventListener()
(事件监听器)方法来监听SW的状态
注意:
在SW中可以用 this
来表示这个SW
在SW中食用的事件监听器是属于异步的,建议直接使用async function
然后就可以有如下操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 self.addEventListener ('install' , async () => { console .log ('[SW] 注册成功!' ); console .log ('[SW] 跳过等待!' ); await self.skipWaiting (); }); self.addEventListener ('activate' , async () => { console .log ('[SW] 激活成功!' ) await self.clients .claim (); console .log ('[SW] 立即管理请求!' ) });
下面的捕获请求也需要用到上面的知识
捕获请求
在学会捕获请求之前
你还需要知道一个新的东西
fetch()
方法
这玩意的功能就类似于 XHR
( XmlHTTPRequest
)
值得一提的是使用 Fetch
API 发送请求是会存在跨域问题的,一旦被跨域拦截,那么就上面都没有返回,会导致页面显示不了请求的内容(例如图片被跨域拦截了),而 img、script 标签它们是不会发生跨域请求问题的。
这玩意的MDN如下(虽然没必要看,因为搞sw不用管它的参数,基本上)
随便的学习了一波Fetch之后,我们就可以开始最激动人心的捕获请求环节
根据上上面的文章我们可以知道 SW
还有个 Fetch
状态
然后就可以有如下操作
注:
event.respondWith()
: 给浏览器一个响应,因为我们已经使用Fetch
API替浏览器发送了请求,并且得到了结果并且返回,那么自然是要返回给浏览器啦
当捕获的请求没有通过这个方法返回数据,浏览器就会假装sw不存在,使用默认的获取方法
而且当返回的数据不正确(比如发生跨域)的时候浏览器也默认跳过
1 2 3 4 5 6 7 8 9 self.addEventListener ('fetch' , async (event) => { const request = handleRequest (event.request ); if (request) { event.respondWith (request); }; });
篡改请求
上面我们都可以使用Fetch
API替浏览器发送请求了,那是不是可以篡改呢?
1 2 3 4 5 6 7 function handleRequest (req ) { const str = 'https://cdn.jsdelivr.net/npm/xhr-ajax/dist/' const url = req.url .replace (str + 'ajax.js' , str + 'ajax.min.js' ) return fetch (url) }
如上代码,我们就可以将 ajax 请求的第三方库 js 文件请求变为压缩后的请求,并返回给浏览器(篡改成功)
这边还有个前端资源并发的列子
也就是最重要的jsd并发竞速
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 function fetchAny (urls ) { const controller = new AbortController () const signal = controller.signal const PromiseAll = urls.map ((url ) => { return new Promise ((resolve, reject ) => { fetch (url, { signal }) .then (progress) .then ((res ) => { const r = res.clone () if (r.status !== 200 ) reject (null ) controller.abort () resolve (r) }) .catch (() => reject (null )); }) }) return Promise .any (PromiseAll ) .then ((res ) => res) .catch (() => null ); };
然后在手搓一点点的匹配代码
然后你就成功的搞出sw了
以下代码不能直接复制运行
因为没配置
大概能实现cw的功能
但没缓存功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 function handleRequest (req ) { const urls = []; const urlStr = req.url ; if (configs['redirect' ]) { for (let redirect of configs['redirect' ]) { if (redirect['rule' ].test (urlStr)) { const replaceurl = urlStr.replace (redirect['rule' ], redirect['repalce' ]) console .debug (`[SW] 请求 ${urlStr} 匹配到劫持规则! URL被替换成 ${replaceurl} ` ) return fetchOne (replaceurl) } }; }; if (configs['cdn' ]) { for (let cdn of configs['cdn' ]) { if (cdn['rule' ].test (urlStr)) { let rule_search = cdn['search' ] || cdn['rule' ]; if (rule_search == '_' ) { rule_search = cdn['rule' ]; }; for (let search_replace of cdn['replace' ]) { let push_url_str if (search_replace == '_' ) { push_url_str = urlStr; } else { push_url_str = urlStr.replace (rule_search, search_replace) }; urls.push (push_url_str); }; }; }; } else { console .warn ('[SW] 警告: 配置未包含cdn配置项!' ); }; if (urls.length ) return fetchAny (urls) console .debug (`[SW] 请求 ${urlStr} 没有匹配到任何规则,跳过此次请求。` ); return null ; };
然后大概就可以手搓出个大概这样的sw
注意: 本代码不可以直接运行
需要搭配配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 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 self.addEventListener ('install' , async () => { console .log ('[SW] 注册成功!' ); console .log ('[SW] 跳过等待!' ); await self.skipWaiting (); }); self.addEventListener ('activate' , async () => { console .log ('[SW] 激活成功!' ) await self.clients .claim (); console .log ('[SW] 立即管理请求!' ) }); self.addEventListener ('fetch' , async (event) => { const request = handleRequest (event.request ); if (request) { event.respondWith (request); }; }); 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 ; if (configs['redirect' ]) { for (let redirect of configs['redirect' ]) { if (redirect['rule' ].test (urlStr)) { const replaceurl = urlStr.replace (redirect['rule' ], redirect['repalce' ]) console .debug (`[SW] 请求 ${urlStr} 匹配到劫持规则! URL被替换成 ${replaceurl} ` ) return fetchOne (replaceurl) } }; }; if (configs['cdn' ]) { for (let cdn of configs['cdn' ]) { if (cdn['rule' ].test (urlStr)) { let rule_search = cdn['search' ] || cdn['rule' ]; if (rule_search == '_' ) { rule_search = cdn['rule' ]; }; for (let search_replace of cdn['replace' ]) { let push_url_str if (search_replace == '_' ) { push_url_str = urlStr; } else { push_url_str = urlStr.replace (rule_search, search_replace) }; urls.push (push_url_str); }; }; }; } else { console .warn ('[SW] 警告: 配置未包含cdn配置项!' ); }; if (urls.length ) return fetchAny (urls) console .debug (`[SW] 请求 ${urlStr} 没有匹配到任何规则,跳过此次请求。` ); return null ; }; function fetchAny (urls ) { const controller = new AbortController () const signal = controller.signal const PromiseAll = urls.map ((url ) => { return new Promise ((resolve, reject ) => { fetch (url, { signal }) .then (progress) .then ((res ) => { const r = res.clone () if (r.status !== 200 ) reject (null ) controller.abort () resolve (r) }) .catch (() => reject (null )); }) }) return Promise .any (PromiseAll ) .then ((res ) => res) .catch (() => null ); }; function fetchOne (url ){ return fetch (url) .then (progress) .then ((res ) => { const r = res.clone () if (r.status !== 200 ) return null return r }) }
缓存请求
派蒙: 前面的区域以后再来探索把~~
该区域正在施工中…