mirror of
https://github.com/fanmingming/live.git
synced 2025-12-20 01:00:59 +08:00
165 lines
7.2 KiB
JavaScript
165 lines
7.2 KiB
JavaScript
// Service workers 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。
|
||
// 它会拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源
|
||
// 相当于网页端的正向代理,监听用户请求
|
||
|
||
// Service worker 是一个注册在指定源和路径下的事件驱动 worker
|
||
// Service workers 只能由 HTTPS 承载,毕竟修改网络请求的能力暴露给中间人攻击会非常危险。
|
||
|
||
// self 在 web 主线程中等价于 windows,但 worker 是无窗口(no-window)环境,没有 window、需要通过 self 指向全局环境
|
||
// self 是 worker 中的全局对象,https://www.zhangxinxu.com/wordpress/2017/07/js-window-self/
|
||
|
||
// 整体运行流程,
|
||
// 数据存储,stream -> mitm -> serviceWorker 进行存储;
|
||
// 数据下载 mitm 发起请求,serviceWorker 监听请求,并返回二进制流。
|
||
// serviceWorker 存在的意义,本质上在主进程层面,不支持流式下载,需要将完整的资源保存后才下载。
|
||
// 而在 URL 层面,将请求交给 浏览器运行时,浏览器能自动识别 application/octet-stream 响应类型,触发下载
|
||
// 且 new Response 可以传入 读写流 stream,实现流式数据传输,进行流式下载
|
||
|
||
// 所以本 serviceWorker 只会被触发两次,一次是 onMessage 监听初始化,一次是 onFetch 拦截请求,触发下载
|
||
|
||
// 通过 href 触发下载后,下载流程就由 ReadableStream 控制。
|
||
// 即整个下载过程就是 ReadableStream 的生命周期,ReadableStream 这个流代表了下载进程
|
||
// ReadableStream 通过 enqueue 函数,往下载进程中填充内容。
|
||
|
||
// url 与 data 的映射 map
|
||
const urlDataMap = new Map()
|
||
|
||
// 创建数据读取流
|
||
function createStream (port) {
|
||
// 数据读取流
|
||
return new ReadableStream({
|
||
// controller 是 ReadableStreamDefaultController,https://developer.mozilla.org/zh-CN/docs/Web/API/ReadableStreamDefaultController
|
||
start (controller) {
|
||
// 监听 messageChannel port 的消息,获取传递过来,需要下载的数据
|
||
port.onmessage = ({ data }) => {
|
||
// 接受结束事件,关闭流
|
||
if (data === 'end') {
|
||
return controller.close()
|
||
}
|
||
|
||
// 终止事件
|
||
if (data === 'abort') {
|
||
controller.error('Aborted the download')
|
||
return
|
||
}
|
||
|
||
// 将数据推送到队列中,等待下载
|
||
controller.enqueue(data)
|
||
}
|
||
},
|
||
// 取消
|
||
cancel (reason) {
|
||
console.log('user aborted', reason)
|
||
port.postMessage({ abort: true })
|
||
}
|
||
})
|
||
}
|
||
|
||
// 监听 worker 注册完成事件,service worker 中所有状态如下:https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/state
|
||
self.addEventListener('install', () => {
|
||
// 如果现有 service worker 已启用,新版本会在后台安装,但不会被激活,这个时序称为 worker in waiting。直到所有已加载的页面不再使用旧的 service worker 才会激活新的 service worker。只要页面不再依赖旧的 service worker,新的 service worker 会被激活(成为active worker)。
|
||
// 跳过等待环节,直接让当前 worker 为活跃状态,不再等待之前就得 worker 失效
|
||
self.skipWaiting()
|
||
})
|
||
|
||
// 监听当前为用状态事件
|
||
self.addEventListener('activate', event => {
|
||
// self.clients 获取当前 worker 的客户端对象,可能是 web 主进程,也可能是其他的 worker 对象。
|
||
// self.clients.claim() 将当前 worker 本身设置为所有 clients 的控制器,即从旧的 worker 中将控制权拿过来
|
||
event.waitUntil(self.clients.claim()) // 保持当前状态为 activate 可用状态,直到
|
||
})
|
||
|
||
// 进行消息监听,监听外部传递进来的事件
|
||
self.onmessage = event => {
|
||
const data = event.data // 正则传输的数据
|
||
const port = event.ports[0] // channelPort 端口,传递该消息时
|
||
|
||
// 跳过 ping 心跳检查事件
|
||
if (data === 'ping') {
|
||
return
|
||
}
|
||
|
||
// 触发该数据下载对应的 url
|
||
const downloadUrl = data.url || self.registration.scope + Math.random() + '/' + (typeof data === 'string' ? data : data.filename)
|
||
|
||
const metadata = new Array(3) // [stream, data, port]
|
||
|
||
metadata[1] = data
|
||
metadata[2] = port
|
||
|
||
// Note to self:
|
||
// old streamsaver v1.2.0 might still use `readableStream`...
|
||
// but v2.0.0 will always transfer the stream through MessageChannel #94
|
||
if (data.readableStream) {
|
||
metadata[0] = data.readableStream
|
||
} else if (data.transferringReadable) { // 如果支持 TransformStream,则使用 TransformStream 双向流完成下载数据传输,关闭 messageChannel 的传输
|
||
port.onmessage = evt => {
|
||
port.onmessage = null
|
||
metadata[0] = evt.data.readableStream
|
||
}
|
||
} else {
|
||
// 如果没有外部传入的 readStream 对象,则自己创建一个,且本质是通过 messageChannel 进行数据监听与数据传输
|
||
metadata[0] = createStream(port)
|
||
}
|
||
|
||
// 进行数据与 url 的映射记录
|
||
urlDataMap.set(downloadUrl, metadata)
|
||
|
||
// 进行消息响应,返回下载地址
|
||
port.postMessage({ download: downloadUrl })
|
||
}
|
||
|
||
// service worker 的主要监听器,拦截监听该 web 下发起的所有网络请求,https://developer.mozilla.org/zh-CN/docs/Web/API/FetchEvent
|
||
// 实际上,该 onfetch 除去 ping 请求外,只会被触发一次,用于拦截下载请求。
|
||
// 下载请求,则返回一个 二进制流 响应,触发浏览器下载。
|
||
self.onfetch = event => {
|
||
// event request 获得 web 发起的请求对象,https://developer.mozilla.org/zh-CN/docs/Web/API/FetchEvent/request
|
||
const url = event.request.url
|
||
|
||
// 仅在 Firefox 中有效,监听到 心跳检查 ping 请求
|
||
if (url.endsWith('/ping')) {
|
||
return event.respondWith(new Response('pong'))
|
||
}
|
||
|
||
const urlCacheData = urlDataMap.get(url) // 获取之前缓存的 url 映射的信息
|
||
|
||
if (!urlCacheData) return null
|
||
|
||
const [
|
||
stream, // 需要下载的数据二进制流
|
||
data, // 配置信息
|
||
port // 端口
|
||
] = urlCacheData
|
||
|
||
urlDataMap.delete(url)
|
||
|
||
// 构造响应体,并只获取外部传入的 Content-Length 和 Content-Disposition 这两个响应头
|
||
const responseHeaders = new Headers({
|
||
'Content-Type': 'application/octet-stream; charset=utf-8', // 将响应格式设置为二进制流
|
||
|
||
// // 一些安全设置
|
||
'Content-Security-Policy': "default-src 'none'",
|
||
'X-Content-Security-Policy': "default-src 'none'",
|
||
'X-WebKit-CSP': "default-src 'none'",
|
||
'X-XSS-Protection': '1; mode=block'
|
||
})
|
||
|
||
// 通过 data.headers 配置,生成 headers 对象,获取其内部值
|
||
let headers = new Headers(data.headers || {})
|
||
|
||
// 设置长度
|
||
if (headers.has('Content-Length')) {
|
||
responseHeaders.set('Content-Length', headers.get('Content-Length'))
|
||
}
|
||
|
||
// 指示回复的内容该以何种形式展示,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地。https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition
|
||
if (headers.has('Content-Disposition')) {
|
||
responseHeaders.set('Content-Disposition', headers.get('Content-Disposition'))
|
||
}
|
||
|
||
// 针对该请求进行响应
|
||
event.respondWith(new Response(stream, { headers: responseHeaders }))
|
||
|
||
port.postMessage({ debug: 'Download started' })
|
||
}
|