123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- // 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' })
- }
|