<div id="content_views" class="htmledit_views">
<h3><a name="t0"></a>一、前言</h3>
本文对前端 Vue 项目开发过程中,经常遇到要对文件做一些相关操作,比如:文件导出下载、文件上传、图片压缩、文件转换等一些处理方法进行归纳整理,方便后续查阅和复用。
二、具体内容
1、后端的文件导出接口,返回数据是文件流 blob,转成 url 链接下载
浏览器 F12 调试器打开查看,返回数据长这样的。
注意:记得在定义的接口,响应头部加上 responseType: 'blob'
-
import request
from
'@/utils/request'
// 一般基于 axios 封装的 request
-
-
export
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 文件,引入上一步定义的 api
-
-
exportApi().
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 转成 base64
-
export
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 文件,引入定义的 api
-
import { 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
这是我本人在工作学习中做的一些总结,同时也分享出来给需要的小伙伴哈 ~ 供参考学习,有什么建议也欢迎评论留言,转载请注明出处哈,感谢支持!