[前端] 常用文件下载方式

83 阅读3分钟

前端开发的场景中经常需要去下载服务中的文件,实现的方案也不少,各有各自的优缺点。这里主要了解两种较为常用的下载文件方式

  1. 将文件转为blob二进制类型,配合Url对象和a标签(href, download)属性完成

blob 二进制大数据类型, 可存储大量二进制类型数据, 数据是不可修改的(数据存储在原始缓冲内存中),可通过FileReader对象的属性读取其中的内容, 注意File类型是特殊的Blob类型

// 创建blob 
let b = new Blob([data1, data2], options)

// [data1, data2, ···] : 需要保存的为二进制的数据 --- > 以二进制数组的方式存储
// options: 配置对象 {
//   type: 保存文件的MIME类型
//   ending : 指定换行符的写入方式
//}
 
 如: 
 let blob = new Blob(['miu'], {
     type: 'text/plain'
 })
 
 console.log(blob)
 
 // 1.  Blob {size: 3, type: 'text/plain'}
 // 1.  size: 3
 // 1.  type: "text/plain"
 // 1.  [[Prototype]]: Blob
 
 // 属性
 blob.size --> 保存二进制数据大小字节
 blob.type --> 数据类型
 
 // 方法
 blob.arrayBuffer() --> 返回Promise对象, 保存的二进制数据以arrayBuffer视图存在
 blob.text() --- > 返回Promise, 获取到以utf8编码的数据
 blob.slice() ---> 截取指定范围的二进制数据(返回副本数据)

URL 用于创建和解析url的对象

// 创建Url 
let url = new URL('https://www.baidu.com/search?k=get&n=30')

  URL {origin: 'https://www.baidu.com', protocol: 'https:', username: '', password: '', host: 'www.baidu.com', …}
1.  hash: ""
1.  host: "www.baidu.com"
1.  hostname: "www.baidu.com"
1.  href: "https://www.baidu.com/search?k=get&n=30"
1.  origin: "https://www.baidu.com"
1.  password: ""
1.  pathname: "/search"
1.  port: ""
1.  protocol: "https:"
1.  search: "?k=get&n=30"
1.  searchParams: URLSearchParams {}
1.  username: ""

// 详细查看文档 https://developer.mozilla.org/zh-CN/docs/Web/API/URL

URL.createObjectURL([blob]) 接收一个二进制原始数据, 返回一个Url对象(指向原始缓冲内存地址)

URL.revokeObjectUrl(blobUrl) 接收指向原始缓存内存的地址, 移除产生的URL对象

实现

核心(需要后端配合返回blob类型数据)

let xhr = new XMLHttpRequest()

xhr.open('get', 'http://127.0.0.1:3000/imageBlob')

// 必须设置响应类型为blob,后台需返回二进制数据
xhr.responseType = 'blob';

xhr.addEventListener('load', () => {
    if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
        // 获取下载对象(blob)
        let blob = xhr.response
        // 下载文件触发方法
        createLink(URL.createObjectURL(blob), Date.now(), blob.type);
    }
})

xhr.addEventListener('error', () => {
    console.error('下载出错了');
})

xhr.addEventListener('progress', (e) => {
    console.log(e); // 可用于下载进度监控
})

xhr.send()


 /**
  * @description 创建下载链接
  * @param {string} url 下载请求地址
  * @param {string} fileName 文件名称
  * @param {string} fileType 下载文件类型
  */
function createLink(url, fileName, fileType) {
    // 构造a标签
    let link = doc.createElement('a')
    // 创建下载链接
    link.setAttribute('href', url)
    // 插入到body中
    doc.body.appendChild(link)
    // 匹配出文件类型
    let reg = new RegExp(/\/([\W|\w|\d]*)$/)
    // 设置下载后的文件名称
    link.download = `${fileName}.${reg.exec(fileType)[1]}`
    // 触发点击事件
    link.click()
    // 卸载a
    doc.body.removeChild(link)
    // 移除对应URL对象
    URL.revokeObjectURL(url)
}

QQ截图20221223181204.png


  1. 将文件转为base64类型,配合Url对象和a标签(href, download)属性完成

Base64,顾名思义,就是包括小写字母a-z、大写字母A-Z、数字0-9、符号"+"、"/"一共64个字符的字符集,(另加一个“=”,实际是65个字符,至于为什么还会有一个“=",这个后面再说)。任何符号都可以转换成这个字符集中的字符,这个转换过程就叫做base64编码。

实现 后端需要返回拼接好的base64字符 如 data:image/png;base64,/2ffs/2332···

/**
 * @description 请求获取图片
 */
function requestImgBase() {
    let xhr = new XMLHttpRequest()

    xhr.open('get', 'http://127.0.0.1:3000/imageBase')

    xhr.addEventListener('load', () => {
        if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
            // 获取下载对象(blob)
            let base64 = xhr.response

            // 下载文件触发方法
            createLinkBase(base64, Date.now())
        }
    })

    xhr.addEventListener('error', () => {
        console.error('下载出错了');
    })

    xhr.addEventListener('progress', (e) => {
        console.log(e);
    })

    xhr.send()
}

/**
 * @description 创建下载链接
 * @param {string} url 下载请求地址
 * @param {string} fileName 文件名称
 * @param {string} fileType 下载文件类型
 */
function createLinkBase(url, fileName) {
    // 构造a标签
    let link = doc.createElement('a')
    // 创建下载链接
    link.setAttribute('href', url)

    // 插入到body中
    doc.body.appendChild(link)

    // 用于匹配文件类型的正则
    let reg = new RegExp(/\/([\W|\w|\d]*)$/)

    // 找到包含文件类型的字符串
    let rawType = url.slice(5, url.indexOf('base64,') - 1);

    // 设置下载后的文件名称
    link.download = `${fileName}.${reg.exec(rawType)[1]}`

    // 触发点击事件
    link.click()
    // 卸载a
    doc.body.removeChild(link)
}

QQ截图20221223181204.png


3、from 表单实现下载文件

/**
* 下载文件 
* @param {String} path - 请求的地址
* @param {String} fileName - 文件名
*/ 
function downloadFile (downloadUrl, fileName) { 
    // 创建表单 
    const formObj = document.createElement('form');
    formObj.action = downloadUrl; 
    formObj.method = 'get'; 
    formObj.style.display = 'none'; 
    
    // 创建input,主要是起传参作用
    const formItem = document.createElement('input'); 
    formItem.value = fileName; // 传参的值
    formItem.name = 'fileName'; // 传参的字段名
    
    // 插入到网页中 
    formObj.appendChild(formItem);
    document.body.appendChild(formObj); 
    formObj.submit(); // 发送请求
    document.body.removeChild(formObj); // 发送完清除掉 
}

服务

// 获取图片接口 (二进制数据)
app.get('/imageBlob', async (req, res, next) => {
    try {
        // 文件所在路径
        let path = resolve(__dirname, 'upload/img', '01.jpg')
        // 读取文件操作(默认读取二进制数据)
        let result = await readFilePromiseIfy(path);
        // 获取文件后缀
        let ext = extname(path).slice(1);
        // 设置响应文件类型
        res.header('Content-Type', `image/${ext}`);
        // 返回响应
        res.status(200).send(result);
    } catch (error) {
        next(error) // 错误处理中间件
    }
})
// 获取图片接口 (base64)
app.get('/imageBase', async (req, res, next) => {
    try {
        // 文件所在路径
        let path = resolve(__dirname, 'upload/img', '01.jpg')
        // 读取文件操作(默认读取二进制数据)
        let result = await readFilePromiseIfy(path, 'base64');
        // 获取文件后缀
        let ext = extname(path).slice(1);
        // 设置响应文件类型
        res.header('Content-Type', `image/${ext}`);
        // 返回响应
        res.status(200).send(`data:image/${ext};base64,${result}`);
    } catch (error) {
        next(error)
    }
})


/**
 * @description 读取文件操作
 * @param {string} path 读取文件路径
 * @param {string} coding 读取文件的编码格式
 * @returns {Promise<unknown>} 
 */
function readFilePromiseIfy(path, coding = undefined) {
    return new Promise((res, rej) => {
        readFile(path, coding, (err, data) => {
            if (err) rej(err);
            else res(data)
        })
    })
}