Bootstrap

在vue使用streamsaver+fetch下载大文件

  • 使用常用的 axios 进行下载时,会遇到以下一些问题:
  1. 超时问题:如果下载的文件较大或网络连接较慢,可能会导致请求超时。
  2. 内存问题:如果下载的文件非常大,将整个文件存储在内存中可能会导致内存溢出,下载的文件会出现截断或损坏。
  3. 进度跟踪问题:axios 默认不提供下载进度的功能。需要使用 axios-progress-bar 等第三方库来实现下载进度的监控和显示。
  4. 下载文件时,需要等待整个文件流都下载到内存中才会弹出浏览器的下载文件保存对话框,用户体验较差。
  • 使用Fetch下载相比Axios在文件下载方面的一些优势:
  1. 原生支持:Fetch是浏览器原生提供的API,不需要额外的第三方库或依赖。这意味着在现代浏览器中,无需额外配置即可直接使用Fetch进行文件下载。
  2. 内置的流支持:Fetch使用ReadableStream对象处理响应数据,这使得在下载大文件时能够有效地处理数据流,减少内存占用。相比之下,Axios默认将整个响应加载到内存中,对于大文件下载可能会导致内存问题。
  3. 下载响应开始就可以弹出浏览器下载保存对话框,并且不影响文件流下载,而axios需要等待整个文件流都下载到内存中才会弹出浏览器的下载文件保存对话框
  • StreamSaver:StreamSaver.js 是一个用于在浏览器中实现流式文件下载的 JavaScript 插件。它提供了一种简单而强大的方式来下载大文件或流式数据,而无需将整个文件加载到内存中
  1. 逐步下载:StreamSaver.js 通过逐步写入磁盘,实现了流式下载,避免了将整个文件加载到内存中
  2. 跨浏览器兼容性:StreamSaver.js 支持主流的现代浏览器,包括 Chrome、Firefox、Edge 和 Safari。
  3. 断点续传:通过在下载过程中保存已下载的部分,StreamSaver.js 支持断点续传,即使在网络中断或下载过程中停止,也可以从上次中断的地方恢复下载。

引入streamsaver:

npm install streamsaver

此处是直接封装在接口请求中  统一处理

import axios from 'axios'
import {MessageBox, Message} from 'element-ui'
import store from '@/store'
import {getToken} from '@/utils/auth'

const myAppConfig = window.appConfig

import Vue from 'vue'
import * as streamSaver from "streamsaver"

/**
 * 获取接口访问地址
 */
// create an axios instance
const service = axios.create({
    // baseURL:myAppConfig.ip, // url = base url + request url
    // withCredentials: true, // send cookies when cross-domain requests
    timeout: 30000 // request timeout
})
// request interceptor
service.interceptors.request.use(
    config => {
        config.baseURL = process.env.NODE_ENV === "development" ? window.visitUrl : window.visitUrl + '/api';
        // do something before request is sent
        if (process.env.NODE_ENV === "production") {
            config.headers['X-Token'] = getToken()
        } else if (process.env.NODE_ENV === 'development') {
            config.headers['X-Token'] = process.env.VUE_APP_BASE_TOKEN
            config.baseURL = window.visitUrl
        }
        return config
    },
    error => {
        return Promise.reject(error)
    }
)

// response interceptor
service.interceptors.response.use(
    response => {
        /**
         * @description 文档 content-type 类型
         *  - application/pdf
         *  - application/msword
         *  - application/vnd.openxmlformats-officedocument.wordprocessingml.document
         *  - application/vnd.ms-excel
         *  - application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
         *  - text/plain
         */
        /**
         * @description 图片 content-type 类型
         * - image/jpeg
         * - image/png
         * - image/gif
         * - image/bmp
         * - image/svg+xml
         * - image/webp
         */
        /**
         * @description 未知 content-type 类型
         * - application/octet-stream
         */
        const res = response.data
        console.log("response:", response.headers['content-type'])
        // if the custom code is not 20000, it is judged as an error.
        // 媒体流 return 响应数据 
        //需后端调整 如下:
        //response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");--下载
        //response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + 
        //fileName + ".xlsx");--文件名
        let docs = ['application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/msword', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'text/plain']
        let img = ['application/octet-stream', 'image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/svg+xml', 'image/webp']
        const header = response.headers['content-type'] || ''
        if ([...docs, ...img].includes(header.replace(';charset=utf-8', ''))) return res
        if (res.code != 200) {
            console.log("res.code:", response)
            Message({
                message: res.msg || 'Error',
                type: 'error',
                duration: 2 * 1000,
                center: true,
            })
            Vue.prototype.$onload(false);
            return res
        } else {
            return res
        }
    },
    error => {
        //console.log('err' + error) // for debug
        Message({
            message: error.message,
            type: 'error',
            duration: 1500
        })
        // Vue.prototype.$onload(false);
        return Promise.reject(error)
    }
)
/**
 * @description 导出大文件
 * @param  { String } url 下载路径
 * @param  { String } fileName 文件名字
 */
service.downloadResource = async function (url, fileName) {
    let filename = fileName
    if (!fileName) {
        filename = url.substring(url.lastIndexOf('/') + 1);
    }
    console.log(filename,'filename')
    return fetch(url, {
        method: 'GET',
        cache: 'no-cache',
    }).then(res => {
        const fileStream = streamSaver.createWriteStream(filename,{
            //增加小视图,体现下载进度条与总大小
            size : res.headers.get("content-length")
        })
        const readableStream = res.body
        if (window.WritableStream && readableStream.pipeTo) {
            return readableStream.pipeTo(fileStream)
        }
        window.writer = fileStream.getWriter()
        const reader = res.body.getReader()
        const pump = () => reader.read()
            .then(res => res.done
                ? window.writer.close()
                : window.writer.write(res.value).then(pump))
        pump()
    })
}

/**
 * @description 一般导出 post请求
 * @param  { String } url 下载路径
 * @param  { Object } data 下载参数
 */
service.download = function(url, data = {}) {
    service({ url, method: 'POST', data, responseType: 'blob' }).then(blob => {
        const fileNameMatch = blob['content-disposition'].match(/filename\*?=['"]?(?:UTF-\d['"]*)?([^;\r\n"']*)['"]?;?/i)
        const encodedFileName = fileNameMatch ? fileNameMatch[1] : null
        const decodedFileName = encodedFileName ? decodeURIComponent(encodedFileName) : null
        const downloadLink = document.createElement('a')
        downloadLink.href = URL.createObjectURL(blob)
        downloadLink.download = decodedFileName
        downloadLink.click()
        URL.revokeObjectURL(downloadLink.href)
    })
}

export default service

使用时

  • 可以将导出方法,全局注册后直接使用
  • 或者 直接将封装的这个service.js文件导入后使用
import request from 'xxx/request.js';

exportFiles(item){
let fileName=`${item.courseName}-${item.teacherName}.MP4`
  request.downloadResource(item.reviewUrl,fileName)
}

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;