123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263 |
- (function () {
- // 下载代理,使用 iframe,还是 navigate
- const downloadStrategy =
- window.isSecureContext // window.isSecureContext 判断是否为 https、wss 等安全环境
- || 'MozAppearance' in document.documentElement.style // 是否为 firefox 浏览器
- ? 'iframe' : 'navigate'
- // 中间传输器
- let middleTransporter = null
- // 是否使用 blob 替换 service worker 的能力
- // safari 不支持流式下载功能,https://github.com/jimmywarting/StreamSaver.js/issues/69
- let useBlobFallback = /constructor/i.test(window.HTMLElement) || !!window.safari || !!window.WebKitPoint
- try {
- new Response(new ReadableStream())
- if (window.isSecureContext && !('serviceWorker' in navigator)) {
- useBlobFallback = true
- }
- } catch (err) {
- useBlobFallback = true
- }
- // 是否支持转换器传输流 TransformStream,支持则直接使用他的读写流,完成下载数据的传输。都在需要通过 messageChannel 进行数据传输
- let isSupportTransformStream = false
- try {
- const { readable } = new TransformStream() // 创建读写传输流
- const messageChannel = new MessageChannel() // 创建消息通道,与 iframe 或 window.open 新建的页面中进行消息通信
- messageChannel.port1.postMessage(readable, [readable])
- messageChannel.port1.close()
- messageChannel.port2.close()
- isSupportTransformStream = true
- } catch (err) {
- console.log(err)
- }
- // 创建一个隐藏式的 Iframe,并通过 iframe 的 postMessage 进行消息传输
- function makeIframe(src) {
- console.log('makeIframe', src)
- const iframe = document.createElement('iframe')
- iframe.hidden = true
- iframe.src = src
- iframe.loaded = false
- iframe.name = 'iframe'
- iframe.isIframe = true
- // 调用 iframe 中的 postMessage 方法,即从 iframe 中发送消息
- iframe.postMessage = (...args) => iframe.contentWindow.postMessage(...args)
- iframe.addEventListener('load', () => {
- iframe.loaded = true
- }, { once: true }) // 该事件监听器只监听一次,自动回收
- document.body.appendChild(iframe)
- return iframe
- }
- // 创建一个弹出窗口,模拟iframe的基本功能
- // 使用 popup 新建弹窗,来模拟 iframe 的跨页面消息传输功能
- function makePopup(src) {
- console.log('makePopup', src)
- // 事件代理器,使用 createDocumentFragment 来实现 popup 中的消息监听效果。
- // 与 document 相比,最大的区别是它不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会对性能产生影响。
- const delegate = document.createDocumentFragment()
- const popup = {
- frame: window.open(src, 'popupTitle', 'width=200,height=100'),
- loaded: false,
- isIframe: false,
- isPopup: true,
- remove() { popup.frame.close() },
- // 适配器模式,使得 popup 对象与 iframe 对象有一样的表现。发送事件,监听事件,移除事件
- dispatchEvent(...args) { delegate.dispatchEvent(...args) },
- addEventListener(...args) { delegate.addEventListener(...args) },
- removeEventListener(...args) { delegate.removeEventListener(...args) },
- // 调用
- postMessage(...args) { popup.frame.postMessage(...args) }
- }
- // 监听 popup 是否就绪
- const onReady = evt => {
- // 如果接受到来自 popup 的事件,则证明 popup 已就绪
- if (evt.source === popup.frame) {
- popup.loaded = true
- window.removeEventListener('message', onReady)
- popup.dispatchEvent(new Event('load'))
- }
- }
- window.addEventListener('message', onReady)
- return popup
- }
- // 创建写入流
- function createWriteStream(filename) {
- let bytesWritten = 0 // 记录已写入的文件大小
- let downloadUrl = null // 触发下载时,需要访问的 url 地址
- let messageChannel = null // 消息传输通道
- let transformStream = null // 中间传输流
- if (!useBlobFallback) {
- // middleTransporter = middleTransporter || makeIframe(streamSaver.middleTransporterUrl) // https 环境下,则执行 iframe
- middleTransporter = middleTransporter || window.isSecureContext
- ? makeIframe(streamSaver.middleTransporterUrl) // https 环境下,则执行 iframe
- : makePopup(streamSaver.middleTransporterUrl) // 普通环境下,则通过 window.open 新建弹窗来完成
- messageChannel = new MessageChannel() // 创建消息通道
- // 处理文件名,使其为 url 格式
- filename = encodeURIComponent(filename.replace(/\//g, ':'))
- .replace(/['()]/g, escape)
- .replace(/\*/g, '%2A')
- // 如果支持 TransformStream,则将 TransformStream.readStream 传递给 port2
- if (isSupportTransformStream) {
- transformStream = new TransformStream(downloadStrategy === 'iframe' ? undefined : {
- // 流处理,中间转换器,监听每一个流分片的经过
- transform(chunk, controller) {
- // 传输的内容,仅支持 Uint8Arrays 格式
- if (!(chunk instanceof Uint8Array)) {
- throw new TypeError('Can only write Uint8Arrays')
- }
- bytesWritten += chunk.length // 记录已写入的内容消大小
- controller.enqueue(chunk) // 将消息推进队列
- if (downloadUrl) {
- location.href = downloadUrl // 由于在 response 中设置了返回类型为二进制流,可直接触发其下载。不会发生跳转
- downloadUrl = null
- }
- },
- // 结束写入时调用,如果数据量少,未经过 transform 就触发了 flush,则调用 location.href 触发下载
- flush() {
- if (downloadUrl) {
- location.href = downloadUrl
- }
- }
- })
- // 使用 port1 传递数据,将读数据端通过 channel Message 传递给 service worker
- // 由 write 暴露写端,供主线程写入数据。再在 service worker 中,通过 readStream 读取该数据。完成下载数据的传输。
- // 即下载数据,不需要通过 channel message 传输,而是通过 transformStream 进行传递。
- messageChannel.port1.postMessage({ readableStream: transformStream.readable }, [transformStream.readable])
- }
- // 监听给 port1 传递的消息
- messageChannel.port1.onmessage = evt => {
- // 接受 Service worker 发送的 url,并访问它
- if (evt.data.download) {
- // 为 popup 做的特殊处理
- if (downloadStrategy === 'navigate') {
- // 中间人完成使命,则删除中间人,后续传输通过 channelMessage,直接由主进程与 service worker 进行通信
- middleTransporter.remove()
- middleTransporter = null
- // 首次访问该 url
- if (bytesWritten) {
- location.href = evt.data.download
- } else {
- downloadUrl = evt.data.download
- }
- } else {
- if (middleTransporter.isPopup) {
- middleTransporter.remove()
- middleTransporter = null
- // Special case for firefox, they can keep sw alive with fetch
- if (downloadStrategy === 'iframe') {
- makeIframe(streamSaver.middleTransporterUrl)
- }
- }
- makeIframe(evt.data.download)
- }
- } else if (evt.data.abort) { // 消息终止
- chunks = []
- messageChannel.port1.postMessage('abort') //send back so controller is aborted
- messageChannel.port1.onmessage = null
- messageChannel.port1.close()
- messageChannel.port2.close()
- messageChannel = null
- }
- }
- // 往中间人容器中发送消息,将 messageChannel.port2 传递给中间人
- const response = {
- transferringReadable: isSupportTransformStream,
- pathname: Math.random().toString().slice(-6) + '/' + filename,
- headers: {
- 'Content-Type': 'application/octet-stream; charset=utf-8',
- 'Content-Disposition': "attachment; filename*=UTF-8''" + filename
- }
- }
- if (middleTransporter.loaded) {
- middleTransporter.postMessage(response, '*', [messageChannel.port2])
- } else {
- middleTransporter.addEventListener('load', () => {
- middleTransporter.postMessage(response, '*', [messageChannel.port2])
- }, { once: true })
- }
- }
- let chunks = [] // 需要传输下载的内容数组
- // 如果有 transformStream,则直接返回 transformStream 读写流的 WritableStream 实例
- if (!useBlobFallback && transformStream && transformStream.writable) {
- // writable 返回由这个 TransformStream 控制的 WritableStream 实例。
- // writable 返回的是一个实例,而不是一个 boolean 值
- return transformStream.writable
- }
- // 如果不支持 transformStream,则自行创建一个 WritableStream,监听 WritableStream 的写入事件。将数据通过 messageChannel 的两个 port 进行传输
- return new WritableStream({
- // 写入数据
- write(chunk) {
- // 检查写入流,仅支持 Uint8Arrays 格式
- if (!(chunk instanceof Uint8Array)) {
- throw new TypeError('Can only write Uint8Arrays')
- }
- // 如果使用 blob 功能进行下载,则仅存储该数据,无法使用流式边获取数据边下载
- if (useBlobFallback) {
- chunks.push(chunk)
- return
- }
- // service worker 可用,则通过信道传输该二进制流
- messageChannel.port1.postMessage(chunk)
- bytesWritten += chunk.length
- if (downloadUrl) {
- location.href = downloadUrl
- downloadUrl = null
- }
- },
- // 关闭写入流,将流式文件进行保存
- close() {
- // 使用 blob 实现功能,则将所有片段当做 blob 的内容,通过 createObjectURL 生成其链接,点击触发下载
- if (useBlobFallback) {
- const blob = new Blob(chunks, { type: 'application/octet-stream; charset=utf-8' })
- const link = document.createElement('a')
- link.href = URL.createObjectURL(blob)
- link.download = filename
- link.click()
- } else { // service worker 有效,则仅发出 end 事件,由 service worker 执行结束操作
- messageChannel.port1.postMessage('end')
- }
- },
- // 中断,不执行下载
- abort() {
- chunks = []
- messageChannel.port1.postMessage('abort')
- messageChannel.port1.onmessage = null
- messageChannel.port1.close()
- messageChannel.port2.close()
- messageChannel = null
- }
- })
- }
- // 全局挂载 streamSaver 对象
- window.streamSaver = {
- createWriteStream, // 创建写流
- middleTransporterUrl: 'https://live.fanmingming.com/m3u8/mitm.html',
- }
- })()
|