前言
当你的网站或博客有多个部署点时,部署在某个平台的访问速度比较快,于是你就把你的域名解析到了这个平台上,但有时候还是会变得很慢,这时其它站点速度可能会变得比你当前使用的还快一点,难道还有来回解析域名吗?太麻烦了
有没有可以直接返回最快网站资源的办法呢?
- 使用域名管理平台,有些平台可以解析不同网络或地区的站点
例如腾讯云可以区分解析国内三大运营商、境内、境外、等一些解析选项(不太好用,还需要自己测试,难不成求使用其它运营商手机的朋友帮你测一下快不快嘛~) - 使用 js 拦截网站的所有请求,并篡改将请求发送到自己的所有站点,这些站点中如果哪个站点最快返回,那么就用最快返回的这个信息,与此同时将其它的请求全部切断
正文
本文会详细说明如何使用 Service Worker 优选请求资源让你的网站比以前更快,更稳定
Service Worker在接下来的内容中统一称呼为sw
Service Worker
更详细请看https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API
Service workers 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器
Service worker 运行在 worker 上下文,因此它不能访问 DOM。相对于驱动应用的主 JavaScript 线程,它运行在其他线程中,所以不会造成阻塞。
出于安全考量,Service workers 只能由 HTTPS 承载,毕竟修改网络请求的能力暴露给中间人攻击会非常危险。
注册
注册 sw 很简单,只需一行代码即可,如果注册成功,则 sw 会被下载到客户端并且安装和激活,这一步仅仅是注册而已,完整的是: 下载—>安装—>激活
注意: sw 的注册日志记录在 Chrome 浏览器中可以通过访问chrome://serviceworker-internals查看
1
| navigator.serviceWorker.register('/Service-Worker.js')
|
其中/Service-Worker.js
必须是当前域下的 js 文件,他不能是其它域下的,即使 js 文件内的内容完全相等,那也不行
如果你只想在某个路径写使用 sw 的话,你可以使用scope
选项,当然/Service-Worker.js
的位置也可以自定义(只要是同源且是 https 协议就可以),如下只有在/article/
文章页 sw 才启动,其它路径写 sw 不进行处理
1
| navigator.serviceWorker.register('/sw-test/Service-Worker.js', { scope: '/article/' })
|
并且必须是 https 协议,如果是本地127.0.0.1
或localhost
是被允许的
这是一个完整的注册代码
将安装代码放置在<head>
之后
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
| <script> ;(function () { if (navigator.serviceWorker) { navigator.serviceWorker .register('/sw.js') .then((result) => { if (localStorage.getItem('installSW')) return localStorage.setItem('installSW', true) const timer = setInterval(() => { if (result.active.state === 'activated') { clearInterval(timer) fetch(window.location.href) .then((res) => res.text()) .then((text) => { document.open() document.write(text) document.close() }) } }, 100) }) .catch(console.log) } })() </script>
|
生命周期
installing 状态
当注册成功后会触发install事件,然后触发activate事件,此时如果再次刷新页面,它俩都不会被触发了
直到/sw.js
发生了改变,它就会触发一次 install (不仅仅是代码改变,哪怕是多一个空格或是少一个空格,又或是写一个注释都会被触发),但是只执行了install事件,并没有执行activate事件
activing 状态
为什么activate事件不触发了?因为已经有一个 sw 了,它一种处于等待状态,至于什么时候才会被触发,那就是等之前的 sw 停止了才会触发activate事件
那有没有办法不让它等待呢?答案是: 有
使用skipWaiting()
跳过等待,它返回一个 promise 对象(异步的),防止还在执行skipWaiting()
的时候直接就跳到activate事件,我们需要使用async/await
,也可以使用event.waitUntil(skipWaiting())
方法把skipWaiting()
放到里面,和async/await
效果一样
1 2 3 4 5 6 7
|
self.addEventListener('install', async (event) => { await self.skipWaiting() })
|
触发activate事件后 ,当前这一次网页是不会被 sw 管理的,需要下次页面刷新才会被 sw 管理,那怎么让它立即管理页面呢?
更详细请看:
https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/skipWaiting > https://developer.mozilla.org/en-US/docs/Web/API/Clients/claim
1 2 3 4
| self.addEventListener('activate', async (event) => { await self.clients.claim() })
|
安装
详细上面生命周期已经详细说明了
1 2 3 4 5
| self.addEventListener('install', async (event) => { await self.skipWaiting() })
|
捕获请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| self.addEventListener('fetch', async (event) => { handleRequest(event.request) .then((result) => event.respondWith(result)) .catch(() => 0) })
function handleRequest(req) { return fetch(req.url) }
|
event.respondWith()
: 给浏览器一个响应,因为我们已经使用Fetch API
替浏览器发送了请求,并且得到了结果并且返回,那么自然是要返回给浏览器啦
Fetch API可以和XMLHttpRequest
一样,可以发送任何请求
值得一提的是使用 Fetch API 发送请求是会存在跨域问题的,一旦被跨域拦截,那么就上面都没有返回,会导致页面显示不了请求的内容(例如图片被跨域拦截了),而 img、script 标签它们是不会发生跨域请求问题的,所以上面 catch 捕获异常一个 0 和 null 差不多
既然没有用到 event.respondWith 那自然是没有给浏览器返回数据啦,那浏览器就自己请求(很好的避免了这个问题)
篡改请求
上面我们都可以使用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 文件请求变为压缩后的请求,并返回给浏览器(篡改成功)
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
| function handleRequest(req) { const urls = [ "https://cdn.jsdelivr.net/npm/xhr-ajax/dist/ajax.min.js", "https://unpkg.com/xhr-ajax/dist/ajax.min.js", ]; const controller = new AbortController(); const signal = controller.signal;
const PromiseAll = urls.map((url) => { return new Promise(async (resolve, reject) => { fetch(url, { signal }) .then( (res) => new Response(await res.arrayBuffer(), { status: res.status, headers: res.headers, }) ) .then((res) => { if (res.status !== 200) reject(null); controller.abort(); resolve(res); }) .catch(() => reject(null)); }); }); return Promise.any(PromiseAll) .then((res) => res) .catch(() => null); }
|
Promise.any 具体请看: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/any
AbortController 具体请看: https://developer.mozilla.org/zh-CN/docs/Web/API/AbortController
只要传入的迭代对象中的任何一个 promise
返回成功(resolve)状态,那么它就返回成功状态,如果其中的所有的 promises
都失败,那么就会把所有的失败返回
所以只要Promise.any
有一个成功状态的数据返回,那么我们就把这个数据响应给浏览器,而其它 的请求全部切断,这样就可以高效的在不同地区响应最快的资源给用户啦~
这也是正文开始前我们所需要解决的问题
完整 sw.js 文件
这是我总结写出的一个 sw.js 文件,你只需要将下面的origin
数组改成你的博客地址就可以了,其它的可以不用动,如果你想添加新东西,那就随便你啦~哈哈哈
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
| const origin = ['https://blog.imlete.cn', 'https://lete114.github.io']
const cdn = { gh: { jsdelivr: 'https://cdn.jsdelivr.net/gh', fastly: 'https://fastly.jsdelivr.net/gh', gcore: 'https://gcore.jsdelivr.net/gh', testingcf: 'https://testingcf.jsdelivr.net/gh', test1: 'https://test1.jsdelivr.net/gh', tianli: 'https://cdn1.tianli0.top/gh' }, combine: { jsdelivr: 'https://cdn.jsdelivr.net/combine', fastly: 'https://fastly.jsdelivr.net/combine', gcore: 'https://gcore.jsdelivr.net/combine', testingcf: 'https://testingcf.jsdelivr.net/combine', test1: 'https://test1.jsdelivr.net/combine', tianli: 'https://cdn1.tianli0.top/combine' }, npm: { jsdelivr: 'https://cdn.jsdelivr.net/npm', fastly: 'https://fastly.jsdelivr.net/npm', gcore: 'https://gcore.jsdelivr.net/npm', testingcf: 'https://testingcf.jsdelivr.net/npm', test1: 'https://test1.jsdelivr.net/npm', unpkg: 'https://unpkg.com', tianli: 'https://cdn1.tianli0.top/npm' } }
self.addEventListener('install', async () => { await self.skipWaiting() })
self.addEventListener('activate', async () => { await self.clients.claim() })
self.addEventListener('fetch', async (event) => { try { event.respondWith(handleRequest(event.request)) } catch (e) {} })
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) const path = urlObj.pathname.split('/')[1]
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) } } }
if (urls.length) return fetchAny(urls)
let origins = [...new Set([location.origin, ...origin])]
const is = origins.find((i) => new URL(urlStr).hostname === new URL(i).hostname)
if (is) { origins = origins.map((i) => i + urlObj.pathname + urlObj.search) return fetchAny(origins) } throw new Error('不是源站') }
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
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)) }) })
if (!Promise.any) createPromiseAny()
return Promise.any(PromiseAll) .then((res) => res) .catch(() => null) }
|