Ctrl K
ComunidadGuías…

serviceWorker.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// 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' })
}