Skip to main content

前端如何实现文件上传

前端如何实现文件下载

文件上传——通过浏览器所暴露出来的统一接口,由用户主动授权发起来访问文件动作,然后读取文件内容进指定内存里,最后执行提交请求操作,将内存里的文件内容数据上传到服务端,最后服务端解析前端传来的数据信息后存入文件里。

最简单的上传实现

通过form表单标签和类型为fileinput标签,需将表单数据编码格式置为multipart/form-data类型

multipart/form-data编码类型会对文件内容在上传时进行处理,以便服务端处理程序解析文件类型与内容,完成上传操作。

<form method="POST" enctype="multipart/form-data">
<input type="file" name="file" value="请选择文件" /><br />
<input type="submit" />
</form>
//...
//上传接口逻辑
if (url === '/upload' && method === 'POST') {
// 定义一个缓存区
const arr = []
req.on('data', buffer => {
// 将前端传来的数据进行存储进缓存区
arr.push(buffer)
})

req.on('end', () => {
// 前端请求结束后进行数据解析 处理
const buffer = Buffer.concat(arr) // 将数据变成string类型
const content = buffer.toString() // 从传来的数存进test的文件里
fileStream('test').write(buffer) // 返回前端请求完成
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
res.end('上传完成')
})
}
//...
------WebKitFormBoundary7YGEQ1Wf4VuKd0cE // WebKitFormBoundary 码 Content-Disposition: form-data; name="file"; filename="index.html" //
包含一些文件基本信息 Content-Type: text/html // 文件内容类型

<html>
<head>
<title>上传文件</title>
</head>
<body>
<form method="POST" enctype="multipart/form-data">
<input type="file" name="file" value="请选择文件" /><br />
<input type="submit" />
</form>
</body>
</html>
------WebKitFormBoundary7YGEQ1Wf4VuKd0cE-- // WebKitFormBoundary 码

最常用的上传实现

由于form表单提交操作网页会造成整体刷新,所以一般比较少用,而是利用熟悉的异步请求操作Ajax来完成上传动作

但是使用这种提交方式没有设置编码enctype="multipart/form-data"类型,如果直接将文件内容上传,会导致后端在解析form表单上传的文件时与Ajax上传的不一致,所以为了后端能够使用相同的代码就能解析前端这两种提交方式,前端需要通过浏览器自身提供的FormData构造函数来实例化的一个文件 fd,然后使用实例的append方法将文件内容插入进去,最后利用XMLHttpRequest的实例做出发送动作。

<div>
<input id="file" type="file" />
<input type="button" value="文件上传" onclick="uploadFile()" />
</div>
<script>
function uploadFile() {
const file = document.getElementById('file').files[0]
const xhr = new XMLHttpRequest()
const fd = new FormData()
fd.append('file', file)
xhr.open('POST', 'http://127.0.0.1:8000/upload', true)
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
alert(xhr.responseText)
}
}
xhr.send(fd)
}
</script>
/**
* @step1 过滤第一行
* @step2 过滤最后一行
* @step3 过滤最先出现Content-Disposition的一行
* @step4 过滤最先出现Content-Type:的一行
*/
const decodeContent = content => {
let lines = content.split('\n')
const findFlagNo = (arr, flag) => arr.findIndex(o => o.includes(flag))
// 查找 ----- Content-Disposition Content-Type 位置并且删除
const startNo = findFlagNo(lines, '------')
lines.splice(startNo, 1)
const ContentDispositionNo = findFlagNo(lines, 'Content-Disposition')
lines.splice(ContentDispositionNo, 1)
const ContentTypeNo = findFlagNo(lines, 'Content-Type')
lines.splice(ContentTypeNo, 1)
// 最后的 ----- 要在数组末往前找
const endNo = lines.length - findFlagNo(lines.reverse(), '------') - 1
// 先反转回来
lines.reverse().splice(endNo, 1)
return Buffer.from(lines.join('\n'))
}
if (url === '/upload' && method === 'POST') {
//文件类型
const arr = []
req.on('data', buffer => {
arr.push(buffer)
})
req.on('end', () => {
const buffer = Buffer.concat(arr)
const content = buffer.toString()
const result = decodeContent(content)
const fileName = content.match(/(?<=filename=").*?(?=")/)[0]
fileStream(fileName).write(result)
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
res.end('上传完成')
})
}

通过数据流的上传实现

Blob对象表示一个不可变、原始数据的类文件对象。构造函数File就是继承与基于Blob,继承了blob的功能并将其扩展,使其支持用户系统上的文件。

简单理解一下,它的构造结果是一块内存区,这块内存区以特定的格式存储我们所要上传的文件二进制数据,当我们上传文件时上传这块内存区里的数据即可。

<div>
<input id="file" type="file" />
<input type="button" value="文件上传" onclick="uploadFile()" />
</div>
<script>
function uploadFile() {
const file = document.getElementById('file').files[0]
const xhr = new XMLHttpRequest()
xhr.open('POST', `http://127.0.0.1:8000/upload?name=${file.name}`, true)
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
alert(xhr.responseText)
}
}
xhr.send(file)
}
</script>
// ...
if (reqUrl.pathname === '/upload' && method === 'POST') {
const fileName = qs.parse(reqUrl.query).name
req.pipe(fileStream(fileName))
req.on('end', () => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
res.end('上传完成')
})
}
// ...

这里的req.pipe其实和req.on('data', ()=> { })监听客户端数据然后组装完成,写进文件是一个效果,最终都是将客户端来的数据写入到了fileName的文件中,而且文件不用通过解析。

ArrayBuffer 转 base64

前端请求二进制数据的时候需要设置数据响应格式responseTypearraybuffer,如果知道返回的是否是二进制数据类型,设为blob

ArrayBuffer对象用来表示通用的、固定长度的原始二进制数据缓冲区。

它是一个字节数组,通常在其他语言中称为byte array

它的诞生就是为了解决一个问题:操作二进制数据。

不能直接操作ArrayBuffer的内容,而是要通过类型数组对象(TypedArray)DataView对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容。

方法一: btoa()

const base64String = window.btoa(String.fromCharCode(...new Uint8Array(new ArrayBuffer(10))))
console.log(base64String) // "AAAAAAAAAAAAAA=="
let encodedData = window.btoa('Hello, world') // 从 String 对象中创建一个 base64 编码的 ASCII 字符
let decodedData = window.atob(encodedData) // 对经过 base64 编码的字符串进行解码

方法二(更快): FileReader

var blob = new Blob([arrayBuffer])
var reader = new FileReader()

reader.onload = function (event) {
var base64 = event.target.result
}

reader.readAsDataURL(blob)

用于图片的 base64

上面方法直接拿到的base64是不能直接放到img里面显示的。因为它的前半部分是不对的。

为了能正常显示,我们还需要在转化的base64字符串前面拼接上data:image/jpeg;base64,

const arrayBufferToBase64Img = buffer => {
const str = String.fromCharCode(...new Uint8Array(buffer))
return `data:image/jpeg;base64,${window.btoa(str)}`
}

上传文件时的进度条原理

xhr对象的upload.onprogress事件在上传过程中会多次回调,可以获取到当前上传的字节数、总字节数、时间戳等信息。

根据上传字节数和总字节数计算得出上传百分比

根据时间戳可以判断两次progress事件间隔,再判断此期间内的上传字节数,做个除法就是上传速度

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>文件上传 原生ajax上传</title>
<style type="text/css">
.container {
width: 200px;
height: 20px;
background-color: gray;
}
#progress {
height: 20px;
background-color: orange;
display: inline-block;
}
</style>
</head>
<body>
<form action="${pageContext.request.contextPath }/upload" enctype="multipart/form-data" method="post">
上传文件1: <input type="file" name="file1" id="file" /><br />
<div class="container">
<span id="progress"></span>
</div>
</form>
<br />
<button onclick="fileSelected()">文件信息</button><button onclick="uploadFile()">确认上传</button>
<div id="info">
<div id="fileName"></div>
<div id="fileSize"></div>
<div id="fileType"></div>
</div>
<div id="result"></div>
<script>
function fileSelected() {
var file = document.getElementById('file').files[0]
if (file) {
var fileSize = 0
if (file.size > 1024 * 1024) fileSize = (Math.round((file.size * 100) / (1024 * 1024)) / 100).toString() + 'MB'
else fileSize = (Math.round((file.size * 100) / 1024) / 100).toString() + 'KB'
document.getElementById('fileName').innerHTML = 'Name: ' + file.name
document.getElementById('fileSize').innerHTML = 'Size: ' + fileSize
document.getElementById('fileType').innerHTML = 'Type: ' + file.type
}
}
function uploadFile() {
var fd = new FormData()
fd.append('file', document.getElementById('file').files[0])
var xhr = new XMLHttpRequest()
xhr.upload.addEventListener('progress', uploadProgress, false)
xhr.addEventListener('load', uploadComplete, false)
xhr.addEventListener('error', uploadFailed, false)
xhr.addEventListener('abort', uploadCanceled, false)
xhr.open('POST', '${pageContext.request.contextPath }/upload') //修改成自己的接口
xhr.send(fd)
}

function uploadProgress(evt) {
if (evt.lengthComputable) {
var percent = Math.round((evt.loaded * 100) / evt.total)

document.getElementById('progress').innerHTML = percent.toFixed(2) + '%'
document.getElementById('progress').style.width = percent.toFixed(2) + '%'
} else {
document.getElementById('progress').innerHTML = 'unable to compute'
}
}
function uploadComplete(evt) {
/* 服务器端返回响应时候触发event事件*/
document.getElementById('result').innerHTML = evt.target.responseText
}
function uploadFailed(evt) {
alert('There was an error attempting to upload the file.')
}
function uploadCanceled(evt) {
alert('The upload has been canceled by the user or the browser dropped the connection.')
}
</script>
</body>
</html>
// ...
$.ajax({
url: '${pageContext.request.contextPath }/upload',
type: 'POST',
data: formData,
xhr: function () {
//获取ajaxSettings中的xhr对象,为它的upload属性绑定progress事件的处理函数
myXhr = $.ajaxSettings.xhr()
if (myXhr.upload) {
//检查upload属性是否存在
//绑定progress事件的回调函数
//progressHandlingFunction函数可以做改变进度条样式操作
myXhr.upload.addEventListener('progress', progressHandlingFunction, false)
}
return myXhr //xhr对象返回给jQuery使用
},
success: function (result) {
$('#result').html(result)
},
contentType: false, //必须false才会自动加上正确的Content-Type
processData: false //必须false才会避开jQuery对 formdata 的默认处理
})
// ...

更多相关知识点

ArrayBuffer对象、TypedArray视图和DataView视图是JavaScript操作二进制数据的一个接口。

它们都是以数组的语法处理二进制数据,所以统称为二进制数组。

知识传送门

JavaScript的二进制家族:BlobArrayBufferBufferBufferNode.js提供)

知识传送门