前端 Vue 操作文件方法(导出下载、图片压缩、文件上传和转换)

无解的菜鸟晖 2024-06-21 10:03:01 阅读 77

一、前言

    本文对前端 Vue 项目开发过程中,经常遇到要对文件做一些相关操作,比如:文件导出下载、文件上传、图片压缩、文件转换等一些处理方法进行归纳整理,方便后续查阅和复用。

二、具体内容

1、后端的文件导出接口,返回数据是文件流 blob,转成 url 链接下载

浏览器 F12 调试器打开查看,返回数据长这样的。

注意:记得在定义的接口,响应头部加上 responseType: 'blob'

import request from '@/utils/request' // 一般基于 axios 封装的 requestexport function exportApi(data) { // 导出下载接口 return request({ url: '/list/export', method: 'post', data: data, responseType: 'blob', timeout: 120000 })}

通过 JavaScript 的 window.URL.createObjectURL(new Blob([...])),将 blob 转成可操作的 url 链接,然后模拟 <a> 标签链接点击下载。

import { exportApi } from '@/api/index' // 某 vue 文件,引入上一步定义的 apiexportApi().then(res => { // 对应的文件下载 / 导出的方法中,写入该部分代码 const url = window.URL.createObjectURL(new Blob([res.data])) // 文件流 blob 转成 URL const a = document.createElement('a') a.style.display = 'none' a.href = url a.download = '文件名.xlsx' // 自定义下载文件的名称 document.body.appendChild(a) a.click() document.body.removeChild(a)})

2、后端接口返回数据是文件下载的 url 链接

相对第 1 点来说去掉 responseType: 'blob'去掉 window.URL.createObjectURL(new Blob([...]))写上 a.href = res.data.url注意:a.download 是 href 属性地址和前端地址同源情况下,才会起作用;否则不同域的情况下,不会起作用,就需要采用文件流 blob 的形式下载来强制修改文件名直接模拟点击 <a> 标签链接下载,其实链接下载还能采用:

// 缺点:体验感不好,屏幕会闪一下,因为这个实际是打开新窗口的然后才关闭window.open(res.data.url)

3、文件 file 或文件流 blob 转成 base64

后端返回的文件是图片,需要转成 base64(或者第 1 点转成 url 链接)才能在前端展示出来,但这种用的比较少,因为后端一般专门存储图片会直接采用 url 链接的方式。

// file 或者 blob 转成 base64export function fileToBase64(file) { return new Promise((resolve, reject) => { const fileReader = new FileReader() fileReader.readAsDataURL(file) fileReader.onload = (e) => { resolve(e.target.result) } fileReader.onerror = () => { reject(new Error('文件异常')) } })}

import { exportApi } from '@/api/index' // 某 vue 文件,引入定义的 apiimport { fileToBase64 } from '@/utils/index' // 引入上一步封装的方法exportApi().then(res => { // 对应的文件下载 / 导出的方法中,写入该部分代码 const blob = new Blob([res.data]) fileToBase64(blob).then(str => { console.log('转换后的 base64', str) // 将获取的 base64 写入 <img> 标签的 src 属性可展示图片出来 })})

这里主要用了 FileReader 这个 API,具体内容可以参考:FileReader - Web API | MDN

4、base64 转成文件 file 或文件流 blob

/** * base64 转成 file 或者 blob * @param str {String} base64 字符串 * @param fileName {String} 自定义的文件名 */export function base64ToFile(str, fileName) { let arr = str.split(',') let mime = arr[0].match(/:(.*?);/)[1] let bStr = atob(arr[1]) let n = bStr.length let u8arr = new Uint8Array(n) while (n--) { u8arr[n] = bStr.charCodeAt(n) } return new File([u8arr], fileName, { type: mime }) // file // return new Blob([u8arr], { type: mime }) // blob}

5、压缩图片文件的方法封装

封装的工具方法,假设写在 utils 文件夹下的 index.js 里,然后在页面文件里通过 import 引入,注意要采用异步的形式(async/await 或 .then)调用,具体使用可以参考第 6 点:

/** * 压缩图片方法 * @param {file} file 文件 * @param {Number} quality 图片质量(取值 0-1 之间默认 0.52) */export function compressImg(file, quality) { let qualitys = 0.52 if (parseInt((file.size / 1024).toFixed(2)) < 1024) { qualitys = 0.85 } if (5 * 1024 < parseInt((file.size / 1024).toFixed(2))) { qualitys = 0.92 } if (quality) { qualitys = quality } if (file[0]) { return Promise.all(Array.from(file).map(e => this.compressImg(e, qualitys))) // 如果是 file 数组返回 Promise 数组 } else { return new Promise((resolve) => { if ((file.size / 1024).toFixed(2) < 300) { resolve({ file: file }) } else { const reader = new FileReader() // 创建 FileReader reader.readAsDataURL(file) reader.onload = ({ target: { result: src } }) => { const image = new Image() // 创建 img 元素 image.onload = async () => { const canvas = document.createElement('canvas') // 创建 canvas 元素 const context = canvas.getContext('2d') const originWidth = image.width const originHeight = image.height let targetWidth = image.width let targetHeight = image.height if (1 * 1024 <= parseInt((file.size / 1024).toFixed(2)) && parseInt((file.size / 1024).toFixed(2)) <= 10 * 1024) { var maxWidth = 1600 var maxHeight = 1600 targetWidth = originWidth targetHeight = originHeight // 图片尺寸超过的限制 if (originWidth > maxWidth || originHeight > maxHeight) { if (originWidth / originHeight > maxWidth / maxHeight) { // 更宽,按照宽度限定尺寸 targetWidth = maxWidth targetHeight = Math.round(maxWidth * (originHeight / originWidth)) } else { targetHeight = maxHeight targetWidth = Math.round(maxHeight * (originWidth / originHeight)) } } } if (10 * 1024 <= parseInt((file.size / 1024).toFixed(2)) && parseInt((file.size / 1024).toFixed(2)) <= 20 * 1024) { maxWidth = 1400 maxHeight = 1400 targetWidth = originWidth targetHeight = originHeight // 图片尺寸超过的限制 if (originWidth > maxWidth || originHeight > maxHeight) { if (originWidth / originHeight > maxWidth / maxHeight) { // 更宽,按照宽度限定尺寸 targetWidth = maxWidth targetHeight = Math.round(maxWidth * (originHeight / originWidth)) } else { targetHeight = maxHeight targetWidth = Math.round(maxHeight * (originWidth / originHeight)) } } } canvas.width = targetWidth canvas.height = targetHeight context.clearRect(0, 0, targetWidth, targetHeight) context.drawImage(image, 0, 0, targetWidth, targetHeight) // 绘制 canvas const canvasURL = canvas.toDataURL('image/jpeg', qualitys) const buffer = atob(canvasURL.split(',')[1]) let length = buffer.length const bufferArray = new Uint8Array(new ArrayBuffer(length)) while (length--) { bufferArray[length] = buffer.charCodeAt(length) } const miniFile = new File([bufferArray], file.name, { type: 'image/jpeg' }) resolve({ origin: file, file: miniFile, beforeSrc: src, afterSrc: canvasURL, beforeKB: Number((file.size / 1024).toFixed(2)), afterKB: Number((miniFile.size / 1024).toFixed(2)), qualitys: qualitys }) } image.src = src } } }) }}

6、通过第三方库 image-conversion 压缩图片

安装并学习使用,参考相关官方文档:image-conversion - npm

Vue2 + Vant2 的文件上传组件,示例代码如下(也可以用第 5 点封装的方法,相关代码注释了):

<template> <div> <!-- 图片上传区域 --> <van-uploader :before-read="uploadBefore" :after-read="uploadAfter" v-model="imgList"></van-uploader> </div></template><script>import * as imageConversion from 'image-conversion'import { Toast, Notify } from 'vant'import { compressImg } from '@/utils/compressImg'export default { data() { return { imgList: [] // 当前图片列表,用于页面回显 } }, methods: { uploadBefore(file) { // 文件读取前的回调函数,返回 false 可终止文件读取,支持返回 Promise return new Promise(async (resolve, reject) => { if (!/image\/[a-zA-z]+/.test(file.type)) { // 判断文件类型是否为图片,不是则取消上传 Notify({ type: 'warning', message: '请上传图片类型的文件' }) reject() } else { console.log(`当前选择的图片文件,大小为:${file.size / (1024 * 1024)} MB`, file) if (file.size / (1024 * 1024) >= 0.4) { // 大于 0.4 MB 的图片需要处理 Toast({ type: 'loading', message: '正在处理图片...', duration: 0, forbidClick: true }) // let handleFile = await compressImg(file, 0.8) // 若使用该方法,底下代码改成 resolve(handleFile.file) let handleFile = new File([await imageConversion.compressAccurately(file, 200)], file.name, { type: 'image' }) // 数值参数,表示指定压缩后图像的大小(KB) // let handleFile = new File([await imageConversion.compress(file, 0.7)], file.name, { type: 'image' }) // 0-1 数值参数,表示图片质量 Toast.clear() console.log('处理后的图片文件', handleFile) resolve(handleFile) // 返回处理后的 file } else { resolve(file) } } }) }, uploadAfter(param) { // 文件读取完成后的回调函数 console.log(`读取完成后得到的图片文件,大小为:${param.file.size / (1024 * 1024)} MB`, param) console.log('当前图片文件列表', this.imgList) } }}</script> 图片文件的相关获取和处理,都需要用到 JavaScript 本身提供的相关对象和 API,而这些 API 有涉及到异步操作且多层嵌套,所以一般采用 async/await 的形式该第三方库处理后返回结果是文件流 blob,需要通过 new 一个 File 对象实例来转换成文件,并 resolve,这样才能被文件读取完成后的回调函数接收对于 Vant 提供的上传组件,before-read 里如果校验方法涉及异步操作,校验不通过时采用 return false,会导致拦截失效,文件仍能上传,所以不建议用 return false,建议采用本文的 Promise 形式

运行结果如下图所示:

7、前端上传图片或文件,请求后端接口

有重要两点:

new FormData()HTTP 的请求头 Content-Type: multipart/form-data; boundary=----...string

    一般前端会通过 axios 请求后端接口,但 axios 发送 HTTP 请求头部里的 Content-Type(内容类型)默认是 application/json;charset=UTF-8,所以默认传参的数据类型是纯文本类型的 JSON 对象,不适合带有文件类型的数据,若需要传参带有文件数据,那么需要把 Content-Type 指定为 multipart/form-data,这样传参可以带上文件类型数据。那么这就需要 JavaScript 提供的 FormData 类型的对象数据,既可以上传文件等二进制数据,也可以上传表单键值对,会转换成为一条信息。示例代码如下:

<button @click="handleUpload()">上传文件</button>

// 结合上一步的内容,给出的关键代码部分handleUpload(){ // 确认上传文件 Toast({ type: 'loading', message: '正在提交...', duration: 0, forbidClick: true }) let postData = new FormData() postData.append('name', 'hxhpg') postData.append('gender', 'male') postData.append('height', '175cm') postData.append('weight', '60kg') for (let i = 0; i < this.imgList.length; i++) { // 之前已上传成功的图片一般返回的是 url postData.append(`imgFile${i + 1}`, this.imgList[i].file || this.imgList[i].content || this.imgList[i].url) } uploadImageFileApi(postData).then(res => { // 某后端接口,通过 axios 封装定义的 Toast.clear() console.log('响应结果', res) }).catch(err => { Toast.clear() })}

    这里简单说下 application/x-www-form-urlencoded,它是标准的默认编码格式(在原始的 AJAX 中,不是 axios),只能上传键值对,并且键值对都是间隔分开的,不能用于上传文件等二进制数据。当采用 get 方式时,会把表单数据转成一串由 key1=value1&key2=value2&key3=value3... 组成的字符串作为 URL 的参数拼接在后面。当采用 post 方式时,则会把表单数据加入 HTTP 的请求体 body 中。

小结:如果是需要键值形式的数据,有文件时采用 multipart/form-data,没有文件时采用 application/x-www-form-urlencoded


    这是我本人在工作学习中做的一些总结,同时也分享出来给需要的小伙伴哈 ~ 供参考学习,有什么建议也欢迎评论留言,转载请注明出处哈,感谢支持!



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。