serviceworker.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. // Service workers 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。
  2. // 它会拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源
  3. // 相当于网页端的正向代理,监听用户请求
  4. // Service worker 是一个注册在指定源和路径下的事件驱动 worker
  5. // Service workers 只能由 HTTPS 承载,毕竟修改网络请求的能力暴露给中间人攻击会非常危险。
  6. // self 在 web 主线程中等价于 windows,但 worker 是无窗口(no-window)环境,没有 window、需要通过 self 指向全局环境
  7. // self 是 worker 中的全局对象,https://www.zhangxinxu.com/wordpress/2017/07/js-window-self/
  8. // 整体运行流程,
  9. // 数据存储,stream -> mitm -> serviceWorker 进行存储;
  10. // 数据下载 mitm 发起请求,serviceWorker 监听请求,并返回二进制流。
  11. // serviceWorker 存在的意义,本质上在主进程层面,不支持流式下载,需要将完整的资源保存后才下载。
  12. // 而在 URL 层面,将请求交给 浏览器运行时,浏览器能自动识别 application/octet-stream 响应类型,触发下载
  13. // 且 new Response 可以传入 读写流 stream,实现流式数据传输,进行流式下载
  14. // 所以本 serviceWorker 只会被触发两次,一次是 onMessage 监听初始化,一次是 onFetch 拦截请求,触发下载
  15. // 通过 href 触发下载后,下载流程就由 ReadableStream 控制。
  16. // 即整个下载过程就是 ReadableStream 的生命周期,ReadableStream 这个流代表了下载进程
  17. // ReadableStream 通过 enqueue 函数,往下载进程中填充内容。
  18. // url 与 data 的映射 map
  19. const urlDataMap = new Map()
  20. // 创建数据读取流
  21. function createStream (port) {
  22. // 数据读取流
  23. return new ReadableStream({
  24. // controller 是 ReadableStreamDefaultController,https://developer.mozilla.org/zh-CN/docs/Web/API/ReadableStreamDefaultController
  25. start (controller) {
  26. // 监听 messageChannel port 的消息,获取传递过来,需要下载的数据
  27. port.onmessage = ({ data }) => {
  28. // 接受结束事件,关闭流
  29. if (data === 'end') {
  30. return controller.close()
  31. }
  32. // 终止事件
  33. if (data === 'abort') {
  34. controller.error('Aborted the download')
  35. return
  36. }
  37. // 将数据推送到队列中,等待下载
  38. controller.enqueue(data)
  39. }
  40. },
  41. // 取消
  42. cancel (reason) {
  43. console.log('user aborted', reason)
  44. port.postMessage({ abort: true })
  45. }
  46. })
  47. }
  48. // 监听 worker 注册完成事件,service worker 中所有状态如下:https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/state
  49. self.addEventListener('install', () => {
  50. // 如果现有 service worker 已启用,新版本会在后台安装,但不会被激活,这个时序称为 worker in waiting。直到所有已加载的页面不再使用旧的 service worker 才会激活新的 service worker。只要页面不再依赖旧的 service worker,新的 service worker 会被激活(成为active worker)。
  51. // 跳过等待环节,直接让当前 worker 为活跃状态,不再等待之前就得 worker 失效
  52. self.skipWaiting()
  53. })
  54. // 监听当前为用状态事件
  55. self.addEventListener('activate', event => {
  56. // self.clients 获取当前 worker 的客户端对象,可能是 web 主进程,也可能是其他的 worker 对象。
  57. // self.clients.claim() 将当前 worker 本身设置为所有 clients 的控制器,即从旧的 worker 中将控制权拿过来
  58. event.waitUntil(self.clients.claim()) // 保持当前状态为 activate 可用状态,直到
  59. })
  60. // 进行消息监听,监听外部传递进来的事件
  61. self.onmessage = event => {
  62. const data = event.data // 正则传输的数据
  63. const port = event.ports[0] // channelPort 端口,传递该消息时
  64. // 跳过 ping 心跳检查事件
  65. if (data === 'ping') {
  66. return
  67. }
  68. // 触发该数据下载对应的 url
  69. const downloadUrl = data.url || self.registration.scope + Math.random() + '/' + (typeof data === 'string' ? data : data.filename)
  70. const metadata = new Array(3) // [stream, data, port]
  71. metadata[1] = data
  72. metadata[2] = port
  73. // Note to self:
  74. // old streamsaver v1.2.0 might still use `readableStream`...
  75. // but v2.0.0 will always transfer the stream through MessageChannel #94
  76. if (data.readableStream) {
  77. metadata[0] = data.readableStream
  78. } else if (data.transferringReadable) { // 如果支持 TransformStream,则使用 TransformStream 双向流完成下载数据传输,关闭 messageChannel 的传输
  79. port.onmessage = evt => {
  80. port.onmessage = null
  81. metadata[0] = evt.data.readableStream
  82. }
  83. } else {
  84. // 如果没有外部传入的 readStream 对象,则自己创建一个,且本质是通过 messageChannel 进行数据监听与数据传输
  85. metadata[0] = createStream(port)
  86. }
  87. // 进行数据与 url 的映射记录
  88. urlDataMap.set(downloadUrl, metadata)
  89. // 进行消息响应,返回下载地址
  90. port.postMessage({ download: downloadUrl })
  91. }
  92. // service worker 的主要监听器,拦截监听该 web 下发起的所有网络请求,https://developer.mozilla.org/zh-CN/docs/Web/API/FetchEvent
  93. // 实际上,该 onfetch 除去 ping 请求外,只会被触发一次,用于拦截下载请求。
  94. // 下载请求,则返回一个 二进制流 响应,触发浏览器下载。
  95. self.onfetch = event => {
  96. // event request 获得 web 发起的请求对象,https://developer.mozilla.org/zh-CN/docs/Web/API/FetchEvent/request
  97. const url = event.request.url
  98. // 仅在 Firefox 中有效,监听到 心跳检查 ping 请求
  99. if (url.endsWith('/ping')) {
  100. return event.respondWith(new Response('pong'))
  101. }
  102. const urlCacheData = urlDataMap.get(url) // 获取之前缓存的 url 映射的信息
  103. if (!urlCacheData) return null
  104. const [
  105. stream, // 需要下载的数据二进制流
  106. data, // 配置信息
  107. port // 端口
  108. ] = urlCacheData
  109. urlDataMap.delete(url)
  110. // 构造响应体,并只获取外部传入的 Content-Length 和 Content-Disposition 这两个响应头
  111. const responseHeaders = new Headers({
  112. 'Content-Type': 'application/octet-stream; charset=utf-8', // 将响应格式设置为二进制流
  113. // // 一些安全设置
  114. 'Content-Security-Policy': "default-src 'none'",
  115. 'X-Content-Security-Policy': "default-src 'none'",
  116. 'X-WebKit-CSP': "default-src 'none'",
  117. 'X-XSS-Protection': '1; mode=block'
  118. })
  119. // 通过 data.headers 配置,生成 headers 对象,获取其内部值
  120. let headers = new Headers(data.headers || {})
  121. // 设置长度
  122. if (headers.has('Content-Length')) {
  123. responseHeaders.set('Content-Length', headers.get('Content-Length'))
  124. }
  125. // 指示回复的内容该以何种形式展示,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地。https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition
  126. if (headers.has('Content-Disposition')) {
  127. responseHeaders.set('Content-Disposition', headers.get('Content-Disposition'))
  128. }
  129. // 针对该请求进行响应
  130. event.respondWith(new Response(stream, { headers: responseHeaders }))
  131. port.postMessage({ debug: 'Download started' })
  132. }