当前位置:网站首页>使用Service Worker优选请求资源 - 持续更新
使用Service Worker优选请求资源 - 持续更新
2022-06-13 04:24:00 【Lete乐特】
前言
长期更新请看我的个人博客,因为我没法在那么多的平台同步更新 https://blog.imlete.cn/article/Service-Worker-Preferred-Request-Resource.html
当你的网站或博客有多个部署点时,部署在某个平台的访问速度比较快,于是你就把你的域名解析到了这个平台上,但有时候还是会变得很慢,这时其它站点速度可能会变得比你当前使用的还快一点,难道还有来回解析域名吗?太麻烦了
有没有可以直接返回最快网站资源的办法呢?
- 使用域名管理平台,有些平台可以解析不同网络或地区的站点
例如腾讯云可以区分解析国内三大运营商、境内、境外、等一些解析选项(不太好用,还需要自己测试,难不成求使用其它运营商手机的朋友帮你测一下快不快嘛~) - 使用 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查看
navigator.serviceWorker.register('/Service-Worker.js')
其中/Service-Worker.js必须是当前域下的 js 文件,他不能是其它域下的,即使 js 文件内的内容完全相等,那也不行
如果你只想在某个路径写使用 sw 的话,你可以使用scope选项,当然/Service-Worker.js的位置也可以自定义(只要是同源且是 https 协议就可以),如下只有在/article/文章页 sw 才启动,其它路径写 sw 不进行处理
navigator.serviceWorker.register('/sw-test/Service-Worker.js', {
scope: '/article/' })
并且必须是 https 协议,如果是本地127.0.0.1或localhost是被允许的
这是一个完整的注册代码
将安装代码放置在<head>之后
<script> ;(function () {
if ('serviceWorker' in navigator) {
navigator.serviceWorker .register('/sw.js') .then((result) => {
// 判断是否安装了sw if (!localStorage.getItem('installSW')) {
localStorage.setItem('installSW', true) // 这里就不用清理setInterval了,因为页面刷新后就没有了 setInterval(() => {
// 判断sw安装后,是否处于激活状态,激活后刷新页面 if (result && result.active && result.active.state === 'activated') {
location.reload() } }, 100) } }) .catch((err) => {
console.log(err) }) } })() </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效果一样
// sw.js
// 在sw中可以使用this或是self表示自身
self.addEventListener('install', async (event) => {
// event.waitUntil(self.skipWaiting())
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
self.addEventListener('activate', async (event) => {
// event.waitUntil(self.clients.claim())
await self.clients.claim() // 立即管理页面
})
安装
详细上面生命周期已经详细说明了
// sw.js
self.addEventListener('install', async (event) => {
// event.waitUntil(self.skipWaiting())
await self.skipWaiting()
})
捕获请求
// sw.js
self.addEventListener('fetch', async (event) => {
// 所有请求都得转到 handleRequest 函数内处理
handleRequest(event.request)
// 如果 handleRequest 请求成功则将数据响应到网页
.then((result) => event.respondWith(result))
// 如果 handleRequest 请求失败,什么都不做(这是网页自己走自己的实际请求)
.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替浏览器发送请求了,那是不是可以篡改呢?
// 处理请求
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 文件请求变为压缩后的请求,并返回给浏览器(篡改成功)
// 批量请求
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();
// 可以中断请求
// https://developer.mozilla.org/zh-CN/docs/Web/API/AbortController#%E5%B1%9E%E6%80%A7
const signal = controller.signal;
// 遍历将所有的请求地址转换为promise
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));
});
});
// 使用 Promise.any 发送批量请求,它接收一个可迭代对象,例如数组就是一个可迭代对象
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数组改成你的博客地址就可以了,其它的可以不用动,如果你想添加新东西,那就随便你啦~哈哈哈
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',
eleme: 'https://npm.elemecdn.com',
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)
// 为了获取 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) {
origins = origins.map((i) => i + urlObj.pathname + urlObj.search)
return fetchAny(origins)
}
// 抛出异常是为了让sw不拦截请求
throw new Error('不是源站')
}
// 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((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))
})
})
// 判断浏览器是否支持 Promise.any
if (!Promise.any) createPromiseAny()
// 谁先返回"成功状态"则返回谁的内容,如果都返回"失败状态"则返回null
return Promise.any(PromiseAll)
.then((res) => res)
.catch(() => null)
}
边栏推荐
- Et framework -22 creating serverinfo entities and events
- Idea Download
- Forgotten fleeting years
- Collection of wrong questions in soft test -- morning questions in the first half of 2010
- Applet version update
- Common ways to traverse map sets
- Record a troubleshooting process - video call cannot be picked up
- Day 007: go language string
- Notes on uni app
- JSTL -- JSP standard tag library
猜你喜欢

Call C function in Lua

1-72 convert string to decimal integer

Tree array explanation

R: Employee turnover forecast practice

Redis

EIA map making - data processing + map making

Et framework -22 creating serverinfo entities and events

Differences and relations between three-tier architecture and MVC

Catalan number

VGA display based on de2-115 platform
随机推荐
Billions of data to determine whether the element exists
一款开源的Markdown转富文本编辑器的实现原理剖析
SS selector
Gets or sets the content in an object
120. 三角形最小路径和-动态规划
JS common array methods
MySQL index
[kubernetes series] pod chapter actual operation
This Sedata uses multiple methods to dynamically modify objects and values in arrays. Object calculation properties
建模杂谈系列143 数据处理、分析与决策系统开发的梳理
Detailed explanation of KOA development process
7-289 tag count (300 points)
The could not find com scwang. smart:refresh-layout-kernel:2.0.3. Required by: project: the app cannot load the third-party package
Redis数据持久化
Set properties \ classes
Interpretation and implementation of proxy mode
Data analysis report
Discussion sur la modélisation de la série 143
Redis
Consolidated figures