优化实战 第 45 期 - 基于二进制流实现文件下载

4,274 阅读1分钟

基于 原生JS 实现二进制流文件下载,支持下载资源地址的有效性校验;支持文件下载时的 文件进度 信息获取

download.jpg

校验下载资源的地址有效性

  • 实现方案

    通过 XMLHttpRequest 创建 同步HEAD 请求,只获取响应头而不需要获取响应体的方式校验请求地址是否有效

  • 方法封装

    const corsEnabled = url => {
      const xhr = new XMLHttpRequest()
      xhr.open('HEAD', url, false)
      try {
        xhr.send()
      } catch (e) {}
      return xhr.status >= 200 && xhr.status <= 299
    }
    

跟踪下载文件的进度

  • 实现方案

    第一步:通过响应头 header 中的 Content-LengthContent-Disposition 分别获取完整的响应长度和文件名称

    第二步:通过流读取器 stream.getReader() 循环读取并计算当前下载进度,直到响应体 body 下载完成

    第三步:返回所有块字节的 Uint8Array 数据 chunks 和文件名称

  • 方法封装

    const download = async (url, callback) => {
      const { ok, status, statusText, headers, body: stream } = await fetch(url, {
        mode: 'cors', credentials: 'same-origin'
      })
      if (!ok) return Promise.reject({ status, statusText })
    
      const reader = stream.getReader()
      const totalSize = headers.get('Content-Length')
      const filename = decodeURI(headers.get('Content-Disposition').split('filename=')[1])
    
      let receiveSize = 0, chunks = []
      while(true) {
        const { done, value } = await reader.read()
        if (done) break
        chunks.push(value)
        receiveSize += value.length
        callback(Number((receiveSize / totalSize * 100).toFixed(2)))
      }
      return Promise.resolve({ filedata: chunks, filename })
    }
    

    使用 decodeURI() 函数对 encodeURI() 编码过的内容进行解码,解决中文文件名称的乱码问题

实现二进制流文件下载

  • 实现方案

    第一步:通过 a 标签的 download 属性设置下载文件的名称

    第二步:通过 a 标签的 href 属性设置下载文件的地址,使用 URL.createObjectURL() 方法生成一个在内存中指向流文件的引用路径作为文件的下载地址

    第三步:创建可以 同步 执行的自定义事件,通过 dispatchEvent 进行事件的触发,让事件用于非 DOM 代码中

  • 方法封装

    const saveAs = (data, filename) => {
      const blob = data instanceof Blob ? data : new Blob(data)
      const node = document.createElement('a')
      Object.assign(node, {
        download: filename,
        href: URL.createObjectURL(blob),
      })
      try {
        node.dispatchEvent(new MouseEvent('click'))
      } catch(e) {
        const evt = document.createEvent('MouseEvents')
        evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null)
        node.dispatchEvent(evt)
      }
      URL.revokeObjectURL(node.href)
    }
    

    通过 外观模式 解决自定义事件的兼容性

    一起学习,加群交流看 沸点