文章目录
前言
本文章总结了本人在网上和实际公司项目中遇到的有关前端文件上传功能的知识点,如有更好的方案或者发现错误,欢迎评论区中指出。
文章部分知识来源网上博客、gpt问答,以及b站up主【三十的前端课】的视频
前后端传输的文件格式主要有哪些
base64
base64编码只包含64个字符,它们由大小写字母、数字以及"+", "/"等特殊字符组成,但可以利用这64种字符来实现表示所有的字符。
为什么我们有时候在传输文本或者图片信息给后端时需要转成base64呢?
第一个原因
很多情况下前端的数据编码形式和后端的不一致,数据从一个编码转到另一个编码可能会出问题。例如前端传输的数据采用了UTF-8的编码方式,而后端使用的是GBK的编码方式,在传输的过程中就有可能导致数据传输错误或乱码,导致数据无法正常显示和处理。
当我们转成base64后,编码形式统一了。
那为啥偏偏是base64呢?不能是其他编码形式吗?因为base64很简单,就哪些常规的字符、字母,所以编码和解码的形式很简单,效率高。
第二个原因
如果需要对数据进行加密,加密后的数据也要面临不同编码形式的转换,也容易出现编码问题。当对base64的数据进行加密的时候,加密后的数据不会包含特殊字符和二进制码,能较少的出现编码问题。
缺点
base64也不是无敌的,他有以下缺点:
- 转换后的数据明显多了
- 后端拿到base64要有个解码的过程,对后端来说有点性能上的考虑
推荐使用第三方库js-base64
formData
这是一个可存有二进制blob的对象,当后端需要key-value的形式传递文件的时候,就可以采用这种方式。
使用方法:
let formDataObj = new FormData() // 实例化
formDataObj.append('fileKey', '123') // api的参数为key,value
formDataObj.append('file', file) // file为file对象的文件,下面会讲
console.log('FormData', formDataObj);
axiosApi(formDataObj) // 发送异步请求
当我们打印formData对象的时候,是看不到内容的
但是我们发起axios的post请求就可以看到
可以在请求头看到请求格式:
后面的表示每个key-value用什么字段区分开,可以在抓包工具中看完整的信息
文件上传基本方案
input标签获取文件
利用原生input标签就提供了文件上传的能力
<template>
<div class="">
<input type="file" @change="fileChange">
</div>
</template>
<script setup>
const fileChange = (e) => {
let file = e.target.files[0] // 单个文件,所以要取下标
console.log('file', file);
}
</script>
把获取到的上传文件打印出来,可以看到是一个File类型的对象,这个对象中有这个文件的很多信息,例如大小,文件类型。里面的文件本体的存储应该是二进制,但打印是看不到的。
这个File对象是Blob对象的一个子类。都是二进制的存储形式,他们之间可以相互转换。
let blobFile = new Blob([file])
console.log('blobFile', blobFile);
let file = new File([blobFile], 'wenjianming.word')
HTML5的API
利用showDirectoryPicker()
这个api可调出文件上传弹窗
const pickDirectory = async () => {
const dirHandle = await window.showDirectoryPicker();
for await (const entry of dirHandle.values()) {
if (entry.kind === "file") {
const file = await entry.getFile();
const text = await file.text();
console.log(text);
}
if (entry.kind === "directory") {
/* for file in this directory do something */
}
}
}
目前发现两个问题:
- 不知道为啥只能选整个文件夹,无法看到某个具体文件
- 必须手动触发函数(例如按钮触发),如果是在例如生命周期这样的钩子中被动触发,会报错。
本地文件读取
如果是源文件,例如存放base64的文本文件,在webpack中配置raw-loader,然后在组件中正常引入文件即可。
其他文件类型也是差不多原理。
切片上传大文件
一个大文件上传给后端,过程可能会十分漫长,我们可以采用切分的方式,分步骤上传。
能被切片的对象有FIle与Blob
let sliceFile = file.slice(0, 3000) // file对象可以进行切割
let sliceBlobFile = blobFile.slice(0, 3000) // blob对象可以进行切割
例子:
const postApi = (file) => {
let size = 2 * 1024 * 1024 // 每次切片的大小
let fileSize = file.size // 文件总大小
let current = 0 // 当前已上传大小
while(current < fileSize) {
let formData = new FormData()
formData.append('fileName', file.name)
formData.append('filekey', file.slice(current, current + size)) // key值给后端进行拼接的时候能够找对文件
await axios.post('xxx/xx', formData)
percent.value = Math.min((current / fileSize) * 100, 100) // 记录进度,取小,最大值100
current += size
}
}
如果上传到一半被中断了咋办?我们前端可以把每次的current存储在localStorage里面,网络恢复了,接续按照当前进度上传。
总结下切片上传的好处:
- 可上传大文件,且性能能被考虑到
- 能够知晓真实的进度
- 能够支持断点续传
blob数据转成base64
我们可以借助FileReader对象,将一些量较小的二进制数据转成base64,例如切片数据。
// sliceBlobFile 文件数据,例如input里的file,也可以是切片的数据
// 将切片数据转成base64,可以借助FileReader对象
let fr = new FileReader()
fr.readAsDataURL(sliceBlobFile) // 调用api转换,但是是个异步操作,需要我们去监听
fr.onload = (e) => { // 异步监听
let res = fr.result // 或者 e.target.result
// res可以直接给img回显
// 但是如果要base64元数据要把前面,号的东西截取掉
let base64res = res.split(',').pop()
}
// 这个对象还可以转换成文本信息
let frText = new FileReader()
frText.readAsText(sliceBlobFile) // 调用api转换,但是是个异步操作,需要我们去监听
frText.onload = () => { // 异步监听
let res = fr.result
// 可以直接放在标签中回显
}
excel上传
excel的上传方式用input标签即可,但是上传后的数据如果前端需要使用,还需要解析。
上传后如何解析
一般我们都会使用xlsx这个第三方库。这里只介绍一些简单的使用方式。
首先我们需要知道解析excel数据的流程,通过input标签我们能够拿到二进制流的File文件,和上面说的Blob对象一样,他们都有一个共同的api叫arrayBuffer
,这个方法可以将二进制转成ArrayBuffer对象。
const excelProccess = (file) => {
file.arrayBuffer().then(res=>{
console.log('arrayBuffer', res);
})
}
ArrayBuffer这个玩意有啥用?
ArrayBuffer是JavaScript中的一种数据类型,它是Web API提供的用于操作二进制数据的一种接口。我们就可以简单的理解为可以用来处理二进制数据的一种手段。
转成了ArrayBuffer对象后,xlsx才能够读取到数据
import { read, utils } from 'xlsx'
const excelProccess = (file) => {
file.arrayBuffer().then(res=>{
const workBook = read(res) // 调用xlsx的读取api
console.log('workBook', workBook);
})
}
此时数据已经可以在这个对象的sheets属性中查看,但是可读性比较低,还需要xlsx帮我们转换下数据格式
import { read, utils } from 'xlsx'
const excelProccess = (file) => {
file.arrayBuffer().then(res=>{
const workBook = read(res)
const sheet = workBook.Sheets.Sheet1 // 实例先读取表一
const data = utils.sheet_to_json(sheet) // 其中一种转换方式
console.log('data', data);
})
}
是不是数组就好看懂一些。这个转换工具函数还可以转成很多类型的格式,甚至能帮你生成html。具体的可以看官方文档。
从后端拿到文件怎么解析
首先在你封装的axios请求中,请求头需要配置{responseType: "blob"}
,这样拿到的才是二进制数据(一般是一个Blob对象),才能走和上面一样的解析流程。
请求头Content-Type格式
- base64:就正常的json对象传递就好了,所以是默认格式
- formData格式:听说Content-Type不用给默认值,当请求体是formData格式会自动加上类型以及分隔符。
- 二进制源数据:直接把file文件作为请求体即可,
Content-Type: application/octet-stream
,还要告诉服务器是什么类型文件,所以还有一个x-ext: .jpg
封装功能
例子
原生单个图片上传例子举例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.upload-container {
width: 200px;
height: 200px;
border: 2px dashed rgb(232, 231, 234);
border-radius: 10px;
overflow: hidden;
position: relative;
}
.upload-container>.preview {
width: 200px;
height: 200px;
position: absolute;
top: 0;
left: 0;
}
.upload-container:hover {
border-color: aqua !important;
}
.upload-select {
width: 100%;
height: 100%;
cursor: pointer;
background-image: url(https://img-home.csdnimg.cn/images/20230822025723.png);
background-size: 300%;
background-position: -200px -800px;
}
.upload-progress {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1;
}
#upload-input {
display: none;
}
.upload-result {
position: relative;
z-index: 1;
}
.upload-result-close {
position: absolute;
right: 5px;
top: 5px;
}
.progress .upload-select,
.progress .upload-result {
display: none;
}
.select .upload-progress,
.select .upload-result,
.select .preview {
display: none;
}
.result .upload-progress,
.result .upload-select {
display: none;
}
.upload-drag {
border-color: aqua !important;
}
</style>
</head>
<body>
<div class="upload-container select">
<div class="upload-select">
<input id="upload-input" type="file">
</div>
<div class="upload-progress">
<span class="upload-progress-percent">70%</span>
<progress id="upload-progress-line" value="70" max="100"></progress>
<button class="upload-progress-close">取消</button>
</div>
<div class="upload-result">
<button class="upload-result-close">X</button>
</div>
<img src="" alt="" srcset="" class="preview">
</div>
<script src="./validateFile.js"></script>
<script src="./request.js"></script>
<script>
let doms = {
uploadContainerDom: document.querySelector('.upload-container'),
uploadSelectDom: document.querySelector('.upload-select'),
uploadInputDom: document.querySelector('#upload-input'),
uploadProgressLineDom: document.querySelector('#upload-progress-line'),
uploadProgressPercentDom: document.querySelector('.upload-progress-percent'),
uploadProgressCloseDom: document.querySelector('.upload-progress-close'),
uploadResultDom: document.querySelector('.upload-result'),
uploadResultCloseDom: document.querySelector('.upload-result-close'),
previewDom: document.querySelector('.preview'),
}
// 切换文件上传样式 select progress result
function switchStyle(type) {
doms.uploadContainerDom.className = `upload-container ${type}`
}
// 手动触发input文件上传点击事件
doms.uploadSelectDom.onclick = function () {
doms.uploadInputDom.click()
}
// input文件上传修改事件触发
doms.uploadInputDom.onchange = changeHandler
function changeHandler() {
// 检验是否上传了文件
if (doms.uploadInputDom.files.length === 0) {
return
}
const file = doms.uploadInputDom.files[0]
if (!validateFile(file)) return
switchStyle('progress')
// 显示预览的背景图
const reader = new FileReader()
reader.onload = (e) => {
doms.previewDom.src = e.target.result
}
reader.readAsDataURL(file)
cancelUpload = upload(file, function (val) {
doms.uploadProgressLineDom.value = val
doms.uploadProgressPercentDom.textContent = val + '%'
}, function (res) {
switchStyle('result')
})
}
// 取消上传
let cancelUpload = null
function cancel() {
cancelUpload && cancelUpload()
switchStyle('select')
doms.uploadInputDom.value = null
}
doms.uploadProgressCloseDom.onclick = cancel
doms.uploadResultCloseDom.onclick = cancel
// 为了input的拖入上传功能兼容性,采用uploadSelectDom去代理input的拖入行为
doms.uploadSelectDom.ondragenter = (e) => {
e.preventDefault()
doms.uploadContainerDom.classList.add('upload-drag')
}
doms.uploadSelectDom.ondragover = (e) => {
e.preventDefault()
doms.uploadContainerDom.classList.add('upload-drag')
}
doms.uploadSelectDom.ondragleave = (e) => {
e.preventDefault()
doms.uploadContainerDom.classList.remove('upload-drag')
}
doms.uploadSelectDom.ondrop = (e) => {
e.preventDefault()
const files = e.dataTransfer.files
if (!e.dataTransfer.types.includes('Files')) {
alert('只能上传文件夹内的文件')
return
}
if (files.length !== 1) {
alert('只能上传一个文件')
return
}
doms.uploadInputDom.files = files // 把拖入的文件放到input标签
doms.uploadContainerDom.classList.remove('upload-drag')
changeHandler() // 触发input的change事件
}
</script>
</body>
</html>
检验文件./validateFile.js
:
// 校验文件类型
function validateFile(file) {
const sizeLimit = 1 * 1024 * 1024;
const legalExts = [".jpg", ".jpeg", ".bmp", ".webp", ".gif", ".png"];
if (file.size > sizeLimit) {
alert("文件尺寸不能大于1MB");
return false;
}
const name = file.name.toLowerCase();
if (!legalExts.some((ext) => name.endsWith(ext))) {
alert("文件类型不正确");
return false;
}
return true;
}
// input中accept的限制
const getInputAccept = (typeArr) => {
let res = typeArr
.map((item) => {
let nameEnd = "";
if (item === "pdf") {
nameEnd = "pdf";
} else if (item === "ppt") {
nameEnd = "vnd.ms-powerpoint";
} else if (item === "pptx") {
nameEnd =
"vnd.openxmlformats-officedocument.presentationml.presentation";
} else if (item === "xls") {
nameEnd = "vnd.ms-excel";
} else if (item === "xlsx") {
nameEnd = "vnd.openxmlformats-officedocument.spreadsheetml.sheet";
} else if (item === "doc") {
nameEnd = "msword";
} else if (item === "docx") {
nameEnd = "vnd.openxmlformats-officedocument.wordprocessingml.document";
}
return "application/" + nameEnd;
})
.join(",");
return res;
};
const inputAccept = getInputAccept(["pdf"]); // 限制文件类型 用于input标签accept属性
模拟接口请求文件./request.js
:
// 触发上传接口,模拟异步请求
function upload(file, onProgress, onFinish) {
let p = 0;
onProgress(p);
const timerId = setInterval(() => {
p++;
onProgress(p);
if (p === 100) {
onFinish("图片上传完成");
clearInterval(timerId);
}
}, 10);
// 返回一个终止上传函数
return function () {
clearInterval(timerId);
};
}
真实进度条怎么做
真实进度的实时获取很简单,百度下axios的onDownloadProgress
配置
真实上传中途取消
可以参考下我这篇文章:【场景方案】关于前端对接口行为的控制合集:轮番查询、并发请求、服务端通知、token无感刷新、请求取消
建议文件上传的axios单独封装成一个独立使用的,方便拓展维护。