前端文件下载功能

2,500 阅读3分钟

前言

在工作中,经常会遇到一些文件下载的需求。在此,我总结了一下平时遇到和使用过的前端调试文件下载功能的方法。下面,让我们根据不同的请求类型(get和post),看一下前端如何实现下载功能。

node实现测使用接口

下面是本地使用eggjs搭建的框架中实现的下载接口。

 async downloadMenuData() {
    const { ctx } = this;
    // pdf文件地址
    const menuPath = path.join(this.config.baseDir, 'app/public/wm.pdf');
    // 通过a标签或者window.open方式下载时,attachment配置是关键
    ctx.attachment('wm.pdf');
    // 文件大小
    const fileSize = fs.statSync(menuPath).size;
    ctx.set('Content-Length', fileSize);
    // 设置下载文件的名称
    ctx.set('Content-Disposition', 'attachment;filename=wm.pdf');
    // 设置MIME类型,前端可以根据此类型判断文件类型,以此对文件进行解析
    ctx.set('Content-Type', 'application/pdf');
    // 设置暴露的请求头属性,这样前端在访问结果时才能访问到 Content-Disposition属性
    ctx.set('Access-Control-Expose-Headers', 'Content-Disposition');
    // 返回数据流
    ctx.body = fs.createReadStream(menuPath);
  }

get请求

如果后端给出的接口是get请求,那么前端可选的实现方式就比较多,可以利用a标签,也可以通过window.open,或者使用自定义的方法(本质也是利用a标签,只是需要对数据进行一些处理)。

a标签方式

<a href="http://127.0.0.1:8999/front/api/resource/downloadMenu" download="menu">a标签下载菜单资源</a>

这种方式需要注意,如txt,png,jpg等这些浏览器支持直接打开的文件是不会执行下载任务的,而是会直接打开文件

window.open方式

window.open('http://127.0.0.1:8999/front/api/resource/downloadMenu', '_self')

可以根据需求选择本页面打开还是新开页面打开

自定义方法

通过Blob处理数据,利用a标签实现下载功能。以下是简单写法,文件类型和名称确定的情况。下面代码中type: 'application/x-xls'指定文件类型,取值可以参考常见MIME类型列表,通过后缀找到对应的取值。

let blob = new Blob([resp], { type: 'application/x-xls' }) // resp为后端返回的文件流数据
let link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = '模板.xlsx'
link.click()

假如文件名称和类型不定,需要根据后端返回来做处理,通常在content-type中规定文件类型,在content-disposition中携带文件名称。 其中,content-type的取值可以参考常见MIME类型列表,通过后缀找到对应的取值。 而content-disposition属性可能会存在前端读取不到的问题,通过浏览器的网络请求工具可以看到,但在代码中无法读取,这时,需要后端设置Access-Control-Expose-Headers,将content-disposition属性暴露出来。具体可参考node实现测试用接口部分。

downFile(params) {
    return new Promise((resolve, reject) => {
        // 可以传入文件名称,文件类型,响应头,响应数据,其中文件名称和文件类型可选,不传的话会从content-type和content_disposition中获取
        const { fileName, fileType, headers, data } = params
        let filename = fileName
        let filetype = fileType
        if (!filetype) {
            if (headers['content-type']) {
                const fileTypes = headers['content-type'].split(';')
                fileTypes.map(item => {
                    if (item.indexOf('application/') !== -1 || item.indexOf('audio/') !== -1 ||
                        item.indexOf('video/') !== -1 || item.indexOf('image/') !== -1 ||
                        item.indexOf('text/') !== -1 || item.indexOf('font/') !== -1) {
                        filetype = item
                    }
                })
            } else {
                console.error('未指定文件类型:未传入filtType且响应头中不包含content-type属性!')
            }
        }
        const blob = new Blob([data], { type: filetype })
        if (!filename) {
            const disposition = headers['content-disposition'].split(';')
            disposition.map(item => {
                if (item.indexOf('filename') !== -1) {
                    const startNum = item.indexOf('=')
                    filename = decodeURI(item.substring(startNum + 1))
                }
            })
        }
        const blob = new Blob([data], { type: filetype })
        const objectUrl = URL.createObjectURL(blob)
        let link = document.createElement('a')
        link.href = objectUrl
        link.download = filename
        link.click()
    })
}

post请求

如果后端接口是post请求,a标签和window.open方式都不可用,只能通过自定义方法来实现,具体方式参考get请求下的自定义方法部分