当前位置:网站首页>使用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)
}
边栏推荐
- Message scrolling JS implementation
- 10 minutes to thoroughly understand how to configure sub domain names to deploy multiple projects
- EIA map making - data processing + map making
- The most detailed swing transformer mask of window attachment in history -- Shaoshuai
- [flutter problem Series Chapter 67] the Solution to the problem of Routing cannot be jumped again in in dialog popup Using get plug - in in flutter
- Collection of wrong questions in soft test -- morning questions in the first half of 2011
- Get parameters on link
- Detailed explanation of KOA development process
- 小程序基础入门(黑马学习笔记)
- Webpack system learning (VIII) how contenthash can prevent browsers from using cache files
猜你喜欢
出现Could not find com.scwang.smart:refresh-layout-kernel:2.0.3.Required by: project :app 无法加载第三方包情况
Ladder race
记录一次排查问题的经过——视频通话无法接起
Li Kou brush question 647 Palindrome substring
Tree array explanation
R: Airline customer value analysis practice
CTFSHOW SQL注入篇(211-230)
Principle, composition and functions of sensors of Dajiang UAV flight control system
The most detailed swing transformer mask of window attachment in history -- Shaoshuai
Simple static web page + animation (small case)
随机推荐
建模雜談系列143 數據處理、分析與决策系統開發的梳理
【剑指Offer】面试题25.合并两个有序的链表
Differences and relations between three-tier architecture and MVC
Sword finger offer 56 - I. number of occurrences in the array
php安全开发15用户密码修改模块
Time format method on the official demo of uniapp
Real time question answering of single chip microcomputer / embedded system
El expression
力扣刷题647.回文子串
1.4.2 Capital Market Theroy
[automated test] what you need to know about unittest
7-289 tag count (300 points)
Knife4j aggregation 2.0.9 supports automatic refresh of routing documents
Modeling discussion series 143 data processing, analysis and decision system development
Use the visual studio code terminal to execute the command, and the prompt "because running scripts is prohibited on this system" will give an error
SQL 进阶挑战(1 - 5)
Notes on software test for programmers -- basic knowledge of software development, operation and maintenance
Sword finger offer II 022 Entry node of a link in a linked list
Applet version update
基于DE2-115平台的VGA显示