StreamSaver.js

立即执行函数

这是一个立即执行函数表达式(IIFE),在函数内部定义了一个名为 streamSaver 的对象,然后将该对象赋值给全局对象(this[name]),或者将其导出到 CommonJS 或 AMD 模块,具体取决于运行环境。

javascript 复制代码
 ((name, definition) => {
     typeof module !== 'undefined'
       ? module.exports = definition()
       : typeof define === 'function' && typeof define.amd === 'object'
         ? define(definition)
         : this[name] = definition()
   })('streamSaver', () => {
     // ...
 })

全局变量和常量

在文件顶部定义了一些全局变量和常量,如 mitmTransporter、supportsTransferable、isSecureContext 等,用于后续函数的操作。

makeIframe 函数

makeIframe 函数用于创建一个隐藏的 iframe 元素,并将其添加到文档中。它通常用于加载指定的页面,以实现一些特定的功能,比如在浏览器中创建一个隐式的浏览器窗口以加载内容。

css 复制代码
 function makeIframe(src) {
   if (!src) throw new Error('meh'); // 如果没有提供 src 参数,则抛出错误

   // 创建一个新的 iframe 元素
   const iframe = document.createElement('iframe');

   // 将 iframe 设置为隐藏(不可见)
   iframe.hidden = true;

   // 设置 iframe 的 src 属性,即要加载的页面地址
   iframe.src = src;

   // 初始化一个 loaded 属性为 false,用于标记 iframe 是否已加载完成
   iframe.loaded = false;

   // 给 iframe 设置一个 name 属性为 'iframe'
   iframe.name = 'iframe';

   // 设置 iframe 的 isIframe 属性为 true,用于标记这是一个 iframe 元素
   iframe.isIframe = true;

   // 定义 iframe 的 postMessage 方法,用于向 iframe 发送消息
   iframe.postMessage = (...args) => iframe.contentWindow.postMessage(...args);

   // 给 iframe 添加一个 'load' 事件监听器,用于在加载完成后标记 loaded 为 true
   iframe.addEventListener('load', () => {
     iframe.loaded = true;
   }, { once: true });

   // 将创建的 iframe 元素添加到文档的 body 中
   document.body.appendChild(iframe);

   // 返回创建的 iframe 元素
   return iframe;
 }
  • 首先,函数接受一个参数 src,表示要加载的页面地址。
  • 函数创建一个新的 iframe 元素,设置其属性,包括 hidden、src、loaded、name 和 isIframe。
  • 定义了一个 postMessage 方法,这个方法可以用于向 iframe 发送消息,使用了 iframe 的 contentWindow.postMessage 方法。
  • 添加了一个 load 事件监听器,当 iframe 加载完成后,会触发该事件,设置 loaded 为 true,同时这个监听器只会执行一次。
  • 最后,将创建的 iframe 元素添加到文档的 body 中,并返回该 iframe 元素的引用。

这个函数的主要作用是创建一个隐藏的 iframe 并加载指定的页面,通常用于在不打扰用户的情况下进行某些操作,例如在后台加载数据或执行特定的浏览器功能。

{ once: true }

{ once: true } 是一个事件监听器选项,用于指定事件处理程序是否只执行一次。在源码中,它用于监听 <iframe> 的 load 事件,确保事件处理程序只在第一次 load 事件发生时执行,而后就会自动从事件监听器列表中移除,以防止多次执行。

这在某些情况下非常有用,特别是当你只关心事件的首次触发时。通过使用 { once: true },可以避免必须手动从事件中注销事件监听器,从而提高代码的可读性和简洁性。

在上述代码中,当 <iframe> 元素加载完成(即触发了 load 事件)时,iframe.loaded 被设置为 true,但由于事件监听器使用了 { once: true } 选项,它不会再次响应任何后续的 load 事件。这样可以确保你只在第一次加载完成时执行特定的操作。

makePopup 函数

makePopup 函数用于创建一个模拟基本 iframe 功能的弹出窗口(popup)。这个弹出窗口会打开指定的页面,然后模拟了一些 iframe 的基本行为。

javascript 复制代码
 function makePopup(src) {
   // 定义弹出窗口的选项,这里设置了窗口的宽度和高度
   const options = 'width=100,height=100';

   // 创建一个代理对象 delegate,用于处理事件代理
   const delegate = document.createDocumentFragment();

   // 创建一个包含弹出窗口相关信息的对象 popup
   const popup = {
     // frame 属性保存了弹出窗口的引用
     frame: window.open(src, 'popup', options),

     // loaded 属性用于标记弹出窗口是否已加载完成
     loaded: false,

     // isIframe 和 isPopup 用于标记弹出窗口的类型
     isIframe: false,
     isPopup: true,

     // remove 方法用于关闭弹出窗口
     remove() {
       popup.frame.close();
     },

     // 事件处理方法的代理
     addEventListener(...args) {
       delegate.addEventListener(...args);
     },
     dispatchEvent(...args) {
       delegate.dispatchEvent(...args);
     },
     removeEventListener(...args) {
       delegate.removeEventListener(...args);
     },

     // postMessage 方法用于向弹出窗口发送消息
     postMessage(...args) {
       popup.frame.postMessage(...args);
     }
   };

   // 定义 onReady 事件处理方法,用于处理弹出窗口加载完成后的操作
   const onReady = evt => {
     if (evt.source === popup.frame) {
       popup.loaded = true;
       // 移除窗口加载完成后的事件监听器
       window.removeEventListener('message', onReady);
       // 触发 'load' 事件以通知其他监听器
       popup.dispatchEvent(new Event('load'));
     }
   }

   // 添加一个 'message' 事件监听器,用于监听弹出窗口的消息
   window.addEventListener('message', onReady);

   // 返回创建的 popup 对象
   return popup;
 }
  • 函数接受一个参数 src,表示要在弹出窗口中加载的页面地址。
  • 函数创建了一个弹出窗口(通过 window.open 方法)并设置了窗口的宽度和高度。
  • 定义了一个代理对象 delegate,用于处理事件代理,可以用于事件的监听和派发。
  • 创建了一个包含弹出窗口相关信息的对象 popup,其中包括了弹出窗口的引用、加载状态和类型等信息。
  • remove 方法用于关闭弹出窗口。
  • addEventListener、dispatchEvent 和 removeEventListener 方法用于处理事件的监听、派发和移除。
  • postMessage 方法用于向弹出窗口发送消息。
  • 定义了 onReady 事件处理方法,用于监听弹出窗口加载完成后的操作。
  • 添加了一个 message 事件监听器,用于监听弹出窗口的消息,并在加载完成后触发 'load' 事件以通知其他监听器。

总的来说,makePopup 函数用于创建一个模拟基本 iframe 功能的弹出窗口,可以加载指定的页面,并处理相关的事件和消息通信。这个函数通常用于实现一些与弹出窗口相关的功能,例如弹出登录框、分享窗口等。

test 函数

这个函数用于在使用 test 函数进行一项功能性测试,主要测试浏览器是否支持可传输的流(transferable streams)。

php 复制代码
 test(() => {
     // Transfariable stream was first enabled in chrome v73 behind a flag
     const { readable } = new TransformStream()
     const mc = new MessageChannel()
     mc.port1.postMessage(readable, [readable])
     mc.port1.close()
     mc.port2.close()
     supportsTransferable = true
     // Freeze TransformStream object (can only work with native)
     Object.defineProperty(streamSaver, 'TransformStream', {
         configurable: false,
         writable: false,
         value: TransformStream
     })
 })

test(() => { ... }):这是一个测试函数,接受一个函数作为参数,然后执行这个函数。

在测试函数内部执行了以下步骤:

  • 创建了一个名为 readable 的可读流(ReadableStream),这是用于测试的基本流对象。
  • 创建了一个消息通道(MessageChannel),这是用于在不同上下文(例如,主页面和 iframe)之间传递消息的机制。
  • 通过 mc.port1.postMessage(readable, [readable]) 将可读流对象 readable 传递给了消息通道的一个端口(port1)。
  • 关闭了消息通道的两个端口,即 mc.port1mc.port2,以释放资源。
  • 将 supportsTransferable 标志设置为 true,表示浏览器支持可传输的流。

使用 Object.defineProperty 冻结了 streamSaver 对象的 TransformStream 属性,这个属性被设置为 TransformStream 构造函数对象。

这段代码的主要目的是检测浏览器是否支持可传输的流(transferable streams)。如果浏览器支持,就将 supportsTransferable 标志设置为 true,并将 streamSaver 对象的 TransformStream 属性设置为 TransformStream 构造函数对象,以便在后续的逻辑中使用。这是为了确保代码在支持 transferable streams 的浏览器中正常运行。

try 块

在 try 块中进行一些浏览器特性的测试,以确定是否支持一些高级特性。例如,检查是否支持 Service Worker、是否在安全上下文中等。

csharp 复制代码
 try {
     // We can't look for service worker since it may still work on http
     new Response(new ReadableStream())
     if (isSecureContext && !('serviceWorker' in navigator)) {
         useBlobFallback = true
     }
 } catch (err) {
     useBlobFallback = true
 }

首先,它尝试创建一个 Response 对象并传递一个新的可读流 ReadableStream 给它。这是一个用于处理 HTTP 响应的浏览器内置 API。

如果上述操作没有抛出异常(即在当前浏览器环境下支持 Response 和 ReadableStream),则继续执行下面的操作。

接下来,它检查当前页面是否在安全上下文中运行,即 isSecureContext 是否为 true。安全上下文通常是指使用 HTTPS 协议加载的页面。如果当前页面在不安全的上下文中(例如使用 HTTP 协议加载),或者浏览器不支持 service Worker ('serviceWorker' in navigator 返回 false),则将 useBlobFallback 设置为 true。

如果在上述任何一步中抛出了异常(例如因为浏览器不支持 Response 或 ReadableStream),则也将 useBlobFallback 设置为 true。

总之,这段代码的目的是检测浏览器是否支持某些功能(Response、ReadableStream 和 serviceWorker),并根据支持情况来设置 useBlobFallback 变量。如果浏览器不支持这些功能或者当前页面不在安全上下文中运行,那么 useBlobFallback 将被设置为 true,可能会触发回退机制,使用 Blob 进行文件下载。这是为了确保在不同浏览器和环境下能够提供稳定的文件下载体验。

loadTransporter 函数

loadTransporter 函数用于加载传输器(transporter)。在这个上下文中,传输器是用于处理文件下载的特殊载体,它可以是一个隐藏的 iframe 或一个弹出窗口,具体取决于浏览器环境和下载策略。

scss 复制代码
 function loadTransporter() {
   if (!mitmTransporter) {
     // 如果 mitmTransporter 不存在,则根据当前安全上下文选择创建一个隐藏的 iframe 或弹出窗口
     mitmTransporter = isSecureContext
       ? makeIframe(streamSaver.mitm)
       : makePopup(streamSaver.mitm);
   }
 }
  • loadTransporter 函数没有任何参数。
  • 函数首先检查 mitmTransporter 是否存在,如果不存在,则执行下面的逻辑。
  • isSecureContext 是一个变量,用于检测当前页面是否在安全上下文(即使用 HTTPS 协议)。如果是安全上下文,则通常会选择创建一个隐藏的 iframe,否则会创建一个弹出窗口。
  • 根据 isSecureContext 的值,通过调用 makeIframe 或 makePopup 函数来创建一个传输器对象,将传输器赋值给 mitmTransporter。

总之,loadTransporter 函数的目的是根据当前安全上下文来创建一个传输器对象,并将其存储在 mitmTransporter 变量中,以便后续在文件下载过程中使用。这个函数的作用是根据安全上下文选择合适的载体来处理文件下载。

createWriteStream 函数

这是一个重要的函数,用于创建可写流,以便将数据写入文件。它接受文件名、选项和大小等参数,并根据当前环境选择使用 iframe 或者其他方式进行文件下载。

函数内部包含了大量逻辑,涉及文件名的处理、创建 Blob 对象、发送消息等等。

scss 复制代码
 function createWriteStream(filename, options, size) {
   let opts = {
     size: null,
     pathname: null,
     writableStrategy: undefined,
     readableStrategy: undefined
   }

   // 标准化参数
   if (Number.isFinite(options)) {
     [size, options] = [options, size]
     console.warn('[StreamSaver] Depricated pass an object as 2nd argument when creating a write stream')
     opts.size = size
     opts.writableStrategy = options
   } else if (options && options.highWaterMark) {
     console.warn('[StreamSaver] Depricated pass an object as 2nd argument when creating a write stream')
     opts.size = size
     opts.writableStrategy = options
   } else {
     opts = options || {}
   }

   // 如果不使用 Blob 回退机制(useBlobFallback 为 false),则执行以下操作
   if (!useBlobFallback) {
     loadTransporter()

     let bytesWritten = 0 // 由 StreamSaver.js(而不是serviceWorker)写入
     let downloadUrl = null
     let channel = new MessageChannel()

     // 使文件名符合 RFC5987 规范
     filename = encodeURIComponent(filename.replace(///g, ':'))
       .replace(/['()]/g, escape)
       .replace(/*/g, '%2A')

     const response = {
       transferringReadable: supportsTransferable,
       pathname: opts.pathname || Math.random().toString().slice(-6) + '/' + filename,
       headers: {
         'Content-Type': 'application/octet-stream; charset=utf-8',
         'Content-Disposition': "attachment; filename*=UTF-8''" + filename
       }
     }

     if (opts.size) {
       response.headers['Content-Length'] = opts.size
     }

     const args = [response, '*', [channel.port2]]

     if (supportsTransferable) {
       const transformer = downloadStrategy === 'iframe' ? undefined : {
         // 仅在不安全的上下文中使用的此转换器和刷新方法。
         transform(chunk, controller) {
           bytesWritten += chunk.length
           controller.enqueue(chunk)

           if (downloadUrl) {
             location.href = downloadUrl
             downloadUrl = null
           }
         },
         flush() {
           if (downloadUrl) {
             location.href = downloadUrl
           }
         }
       }
       var ts = new streamSaver.TransformStream(
         transformer,
         opts.writableStrategy,
         opts.readableStrategy
       )
       const readableStream = ts.readable

       channel.port1.postMessage({readableStream}, [readableStream])
     }

     channel.port1.onmessage = evt => {
       // service Worker发送了我们应该打开的链接。
       if (evt.data.download) {
         // 特殊处理弹出窗口...
         if (downloadStrategy === 'navigate') {
           mitmTransporter.remove()
           mitmTransporter = null
           if (bytesWritten) {
             location.href = evt.data.download
           } else {
             downloadUrl = evt.data.download
           }
         } else {
           if (mitmTransporter.isPopup) {
             mitmTransporter.remove()
             // Firefox 的特殊情况,它们可以使用 fetch 保持service Worker处于活动状态
             if (downloadStrategy === 'iframe') {
               makeIframe(streamSaver.mitm)
             }
           }

           // 我们永远不会删除这些 iframe,因为它可能会中断保存
           makeIframe(evt.data.download)
         }
       }
     }

     if (mitmTransporter.loaded) {
       mitmTransporter.postMessage(...args)
     } else {
       mitmTransporter.addEventListener('load', () => {
         mitmTransporter.postMessage(...args)
       }, {once: true})
     }
   }

   let chunks = []

   // 返回可写流
   return (!useBlobFallback && ts && ts.writable) || new streamSaver.WritableStream({
     write(chunk) {
       if (useBlobFallback) {
         // Safari... 新的 IE6
         // https://github.com/jimmywarting/StreamSaver.js/issues/69
         //
         // 即使它具有一切,它也无法下载来自service Worker的任何内容..!
         chunks.push(chunk)
         return
       }

       // 当准备将新数据块写入底层资源时调用
       // 它可以返回一个 Promise,以表示写入操作的成功或失败。流实现保证仅在先前的写入操作成功后才会调用此方法,永远不会在调用 close 或 abort 后调用。

       // TODO: service Worker在写入完成后回应很重要。否则,我们无法处理反压
       // 编辑:Transfarable 流解决了这个问题...
       channel.port1.postMessage(chunk)
       bytesWritten += chunk.length

       if (downloadUrl) {
         location.href = downloadUrl
         downloadUrl = null
       }
     },
     close() {
       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 {
         channel.port1.postMessage('end')
       }
     },
     abort() {
       chunks = []
       channel.port1.postMessage('abort')
       channel.port1.onmessage = null
       channel.port1.close()
       channel.port2.close()
       channel = null
     }
   }, opts.writableStrategy)
 }

createWriteStream 函数内部的主要步骤和操作:

  • 首先,它接受三个参数:filename(要保存的文件名)、options(选项,可包含写入策略等配置)和 size(文件大小,已不推荐使用)。
  • 然后,它标准化了参数,确保 options 参数是一个对象,以及处理了已弃用的用法。
  • 接着,它检查是否启用了 Blob 回退机制(useBlobFallback)。如果未启用 Blob 回退机制,将执行以下操作:

a. 调用 loadTransporter() 函数加载传输器(transporter),该函数会根据安全上下文选择创建一个隐藏的 iframe 或一个模拟弹出窗口。

b. 根据文件名和选项创建一个响应对象 response,该对象包含文件类型、Content-Disposition 头等信息。

c. 如果支持可传递流(Transferrable Streams),它创建一个可读流,并通过 MessageChannel 传递给 service Worker。

d. 通过 MessageChannel 处理 service Worker 返回的数据,根据下载策略执行不同的操作,包括打开链接或创建新的 iframe。

  • 如果启用了 Blob 回退机制,它将数据块存储在 chunks 数组中,以备稍后创建 Blob 并触发下载。
  • 最后,它返回一个可写流,该流允许将数据块写入底层资源。如果启用了 Blob 回退机制,它将创建一个 Blob 并触发下载。否则,它将通过 channel.port1 发送数据块,以便由 service Worker 处理。该流还处理了关闭和中止操作,以便正确处理文件的完成和中止。

总之,createWriteStream 函数的目标是创建一个可写入流,以便将数据写入客户端。它根据安全上下文、传输方式等因素选择不同的实现方式,并提供了对下载进度和操作的控制。此函数是 StreamSaver.js 中实现文件下载的关键部分。

附加

isSecureContext

最后,注意到一个 isSecureContext 属性,稍微介绍一下

isSecureContext 是一个 JavaScript 属性,用于检查当前页面是否运行在安全上下文(secure context)中。安全上下文是指使用 HTTPS 协议(通过 SSL/TLS 加密)加载的页面,而不是通过 HTTP 加载的页面。这个属性通常用于判断页面是否采用了安全的连接,因为在安全上下文中,敏感数据的传输更加安全。

在 Web 安全性中,一个页面被认为是在安全上下文中运行,当它满足以下条件之一:

  • 通过 HTTPS 协议加载:页面使用 HTTPS 加载,即通过安全的 SSL/TLS 连接传输数据。这通常用于加密敏感信息的传输,如登录凭据、支付信息等。当页面通过 HTTPS 加载时,window.isSecureContext 返回 true。
  • localhost 或者 file:// 协议:如果页面是通过 localhost 访问的,或者通过本地文件系统(file://)打开的,也被认为是在安全上下文中运行。在这些情况下,window.isSecureContext 也返回 true。

通过检查 window.isSecureContext 属性,开发者可以在代码中根据页面的安全上下文来采取不同的行动。例如,在安全上下文中,你可以放心地执行一些敏感操作,而在非安全上下文中,可能需要采取额外的安全措施或者限制某些功能。这有助于确保在安全的环境中处理敏感信息,以提高用户数据的保护和安全性。

作用
  • isSecureContext 属性用于检查当前网页的协议是否为 HTTPS。
  • 如果页面是通过 HTTPS 协议加载的,该属性的值将为 true。
  • 如果页面是通过 HTTP 协议加载的,该属性的值将为 false。
  • 通过检查这个属性,开发人员可以根据页面的安全性采取不同的操作或加载不同的资源。
用途
  • 在某些情况下,开发人员可能希望仅在安全上下文中执行某些操作,例如访问摄像头、麦克风、地理位置等敏感信息。
  • 通过检查 isSecureContext 属性,可以决定是否提供这些敏感操作或限制对它们的访问。
示例
arduino 复制代码
 if (window.isSecureContext) {
   // 运行在安全上下文中,可以执行敏感操作
   navigator.geolocation.getCurrentPosition(position => {
     console.log('Latitude:', position.coords.latitude);
     console.log('Longitude:', position.coords.longitude);
   });
 } else {
   // 非安全上下文,采取适当的措施,例如提醒用户使用安全连接
   console.warn('This page is not loaded over a secure connection (HTTPS).');
 }
兼容性
  • isSecureContext 是 Web 标准的一部分,但不是所有浏览器都支持它。一般来说,在现代的主流浏览器中,它有很好的支持,包括 Chrome、Firefox、Edge 等。
  • 对于那些不支持 isSecureContext 属性的浏览器,可以考虑使用其他方法来检查页面的协议,例如检查 window.location.protocol。

总之,isSecureContext 属性是用于检查当前页面是否在安全上下文中运行的一种方便的方式,有助于开发人员在安全和非安全上下文中采取适当的措施,以保护用户的数据和隐私。它在构建安全性要求较高的 Web 应用程序时非常有用。

postMessage

postMessage 是用于在不同的 JavaScript 上下文之间(例如,在主页面和 iframe 之间)传递消息的方法。它是 HTML5 中引入的 Web Messaging API 的一部分。通过 postMessage,你可以实现跨文档、跨窗口或跨浏览器标签页之间的通信。

语法
ini 复制代码
 otherWindow.postMessage(message, targetOrigin, [transfer]);
  • otherWindow:一个对窗口对象的引用,表示消息的目标窗口。通常是通过 window.openwindow.frames<iframe> 元素的 contentWindow 属性等方式获取的。
  • message:要发送的消息,可以是任何可序列化的数据类型,通常是对象或字符串。
  • targetOrigin:表示消息的目标窗口的源,通常是协议、主机和端口的组合,例如 http://example.com:8080。这个参数用于安全性检查,确保只有目标窗口可以接收消息。如果不确定目标窗口的源,可以使用通配符 '*',但这会降低安全性。
  • transfer(可选):一个可选的数组,包含要传递的 Transferable 对象,如 ArrayBuffer 或 MessagePort。这些对象将从发送方转移到接收方,而不是被复制。
工作原理

发送消息的窗口(通常是发送方)调用 postMessage 方法,并传递消息内容、目标窗口引用和目标源作为参数。

浏览器会检查目标源 targetOrigin 是否与目标窗口的实际源匹配。如果不匹配,消息将被忽略。这是一个关键的安全检查,用于防止跨站点脚本攻击(Cross-Site Scripting, XSS)等问题。

如果目标源匹配,消息将被传递给目标窗口的 JavaScript 上下文。

接收消息的窗口(通常是接收方)需要监听 message 事件,以便在接收到消息时执行特定的处理逻辑。事件处理程序可以访问到传递的消息内容和来源信息。

用途

postMessage 方法通常用于以下情况:

  • 在父窗口和 iframe 之间进行通信。
  • 在不同窗口或标签页之间进行跨页面通信。
  • 在主页面和 Web Workers 之间进行通信。
  • 在不同域之间的窗口或标签页之间进行跨域通信(跨源通信)。

这种跨文档或跨上下文的通信方式使得在现代 Web 应用程序中实现更高级的功能变得更加容易,例如单页面应用程序(SPA)中的组件通信、跨域身份验证、跨窗口事件处理等。

相关推荐
朝阳58135 分钟前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路43 分钟前
GeoTools 读取影像元数据
前端
ssshooter1 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
Jerry2 小时前
Jetpack Compose 中的状态
前端
dae bal3 小时前
关于RSA和AES加密
前端·vue.js
柳杉3 小时前
使用three.js搭建3d隧道监测-2
前端·javascript·数据可视化
lynn8570_blog3 小时前
低端设备加载webp ANR
前端·算法
LKAI.3 小时前
传统方式部署(RuoYi-Cloud)微服务
java·linux·前端·后端·微服务·node.js·ruoyi
刺客-Andy4 小时前
React 第七十节 Router中matchRoutes的使用详解及注意事项
前端·javascript·react.js
前端工作日常4 小时前
我对eslint的进一步学习
前端·eslint