-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
serviceWorker.js
164 lines (132 loc) · 7.24 KB
/
serviceWorker.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
// 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' })
}