4

背景

记得那一天,产品找到我:“我们要做一个可以下载各类型文件的功能,你有什么想法?”
我:“嗯,没问题,我都行!~”(内心:“我得做呀~我能怎么办呐”)
于是,就有了今天这篇小分享(小白的第一篇文章)


一、完整代码

为方便开发任务十分繁重的同学节省时间,完整代码放在顶端便于cv大法。

  1. 函数
import store from '../store'
/** 
 *获取浏览器类型及版本
 *(调用此方法判断浏览器类型及版本,处理火狐浏览器下载的文件没有后缀名的问题)
 */
function getBrowserInfo() {
    var Sys = {}
    var ua = navigator.userAgent.toLowerCase()
    var re = /(msie|firefox|chrome|opera|version).*?([\d.]+)/
    var m = ua.match(re)
    Sys.browser = m[1].replace(/version/, "'safari")
    Sys.ver = m[2]
    return Sys
}
/** 
 *根据application类型获取后缀名称
 *(处理火狐浏览器下载的文件没有后缀名的问题)
 */
function addNameSuffix(type) {
    let suffixName = ''
    switch (type) {
    case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
        suffixName = '.docx' // docx
        break
    case 'application/pdf':
        suffixName = '.pdf' // pdf
        break
    case 'application/zip':
        suffixName = '.zip' // zip
        break
    case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
        suffixName = '.xlsx' // '' xlsx
        break
    case 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
        suffixName = '.pptx' // pptx
        break
    case 'application/vnd.ms-excel':
        suffixName = '.xls' // xls
        break
    case 'application/msword':
        suffixName = '.doc' // doc
        break
    case 'application/vnd.ms-powerpoint':
        suffixName = '.ppt' // ppt
        break
    }
    return suffixName
}
/**
 * @param {string} url:后端接口地址
 * @param {Object} params:请求参数
 * @param {string} fileName:文件名称
 * @returns {Object}
 */
function downloadFile(url, params = null, fileName = '数据下载') {
    return new Promise((resolve, reject) => {
        try {
            let xmlhttp
            if (window.XMLHttpRequest) {
                xmlhttp = new XMLHttpRequest()
            } else {
                xmlhttp = new ActiveXObject('Microsoft.XMLHTTP')
            }
            fileName = fileName.replace(/\./g, '-') //处理文件名中的英文.会导致下载文件类型错误
            xmlhttp.withCredentials = true // 跨域请求携带cookie
            xmlhttp.responseType = 'arraybuffer'
            xmlhttp.open('POST', url, true)
            xmlhttp.setRequestHeader('Content-type', 'application/json;charset=UTF-8')
            xmlhttp.setRequestHeader('token', store.state.user.token)
            xmlhttp.setRequestHeader('currentTimeMillis', store.state.user.currentTimeMillis)
            if (params) {
                params = JSON.stringify(params)
            }
            xmlhttp.send(params)
            xmlhttp.onreadystatechange = () => {
                if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
                    if (xmlhttp.response) {
                        // 后端返回content-type格式为:application/************文件类型 */;charset=UTF8
                        const applicationConfig = xmlhttp.getResponseHeader('content-type').split(';')[0]
                        // 火狐浏览器非86版本处理下载添加后缀名称
                        const sys = getBrowserInfo()
                        if (sys.browser == 'firefox' && sys.ver != '86.0') {
                            const suffix = addNameSuffix(applicationConfig)
                            fileName = fileName + suffix
                        }
                        const content = xmlhttp.response
                        const url = window.URL.createObjectURL(new Blob([content], { type: applicationConfig }))
                        const link = document.createElement('a')
                        link.style.display = 'none'
                        link.href = url
                        link.setAttribute('download', decodeURIComponent(fileName))
                        document.body.appendChild(link)
                        link.click()
                        document.body.removeChild(link) // 下载完成移除元素
                        window.URL.revokeObjectURL(url) // 释放blob对象
                        resolve(true)
                    }
                }
            }
            xmlhttp.onprogress = (event) => {
                const total = xmlhttp.getResponseHeader('Content-length')
                const percent = ((event.loaded / total) * 100).toFixed(2)
                console.log(`下载进度:${percent}`)
                if (event.loaded == total) {
                    resolve(true)
                }
            }
        } catch (e) {
            reject(e)
        }
    })
 }
  1. 调用方式
const url = this.settings.httpService + 'autonomyreport/downloadreport.do' // 请求下载接口url
const params = {id:1} // 参数:下载文件的id
const fileName = '我是被下载的文件名称'
downloadFile(url, params, fileName).then(flag => {
    if(flag){
        // 下载成功 do something
    }
})

二、详细解析

文件标题FileName

因业务场景不同,fileName文件名称由前端传入,正常情况是应该通过XHR.getResponseHeader('content-disposition')方法去获取fileName;但是这种有几点注意事项:

1. 调用XHR.getResponseHeader('content-disposition')报错:(Refused to get unsafe header "Content-Disposition")

disposition报错信息
解决方式:需要后台配合添加context.Response.Headers.Add("Access-Control-Expose-Headers", "Content-Disposition")。
具体内容可查看:你真的会使用XMLHttpRequest吗?
2. 获取的fileName中文乱码问题:

下载进度onProgress

在下载大文件的时候一般会想要给用户提示一个下载进度的功能,这样我们就用到了XHR.onprogrees()方法来监听当前的下载进度:

xmlhttp.onprogress = (event) => {
    const total = xmlhttp.getResponseHeader('Content-length')
    const percent = ((event.loaded / total) * 100).toFixed(2)
    console.log(`下载进度:${percent}`)
    if (event.loaded == total) {
        resolve(true)
    }
}

上述代码中使用xmlhttp.getResponseHeader('Content-length')来获取了文件大小,实属无奈之举;本来onprogress函数的回调中event会返回loaded(当前下载大小)和total(文件大小),但是我本地监听到的total一直为0;查阅资料后得知与请求头中的accept-encoding: gzip相关;怨我才学浅陋,望知晓的大佬可以为我答疑解惑,提供解决方案,谢谢!谢谢!谢谢!

兼容性处理

之前一直没关注火狐浏览器的下载兼容问题,结果被我们测试小姐姐点出来了。一顿撒娇让我解决,~~~这谁受得了呀;那就帮她解决解决吧,毕竟我是个好人 -.-!
问题描述:火狐浏览器(除86.0版本)在调用此方法下载时会出现无后缀名的情况,导致下载的文件类型不明确无法打开。
问题原因:尚不明确;(望大神告知) 查阅过很多资料都说是因为文件名中有空格导致火狐浏览器下载解析时直接截断空格后的内容了;但我的文件没有空格。。。
解决方案:如上代码中getBrowserInfo()方法+addNameSuffix()方法。


三、总结

其实写一个下载方法不难,但我发现真的要把它分享出来很多东西都得往深研究研究,文中用到的参考文献连接已经给出,都是一些有用的文章,大家自行阅读吧~!


D_c
7 声望2 粉丝