Web文件上传总结
文件上传是 Web 开发常见需求,上传文件需要用到文件输入框。
指定文件类型
一个以英文句号(“.”)开头的合法的不区分大小写的文件名扩展名。例如:.jpg、.pdf 或 .doc。
一个不带扩展名的 MIME 类型字符串。
字符串 audio/*,表示“任何音频文件”。
字符串 video/*,表示“任何视频文件”。
字符串 image/*,表示“任何图片文件”。
accept 属性的值是包含一个或多个(用逗号分隔)唯一文件类型说明符的字符串。例如,一个文件选择器需要能被表示成一张图片的内容,包括标准的图片格式和 PDF 文件,大概是这样的:
<input type="file" accept="image/*,.pdf" />
可以用 accept 属性指定可接受的文件类型,它是一个以逗号间隔的文件扩展名和 MIME 类型列表。一些例子如下所示:
accept=“image/png” 或 accept=“.png”——接受 PNG 文件。
accept=“image/png, image/jpeg” 或 accept=“.png, .jpg, .jpeg”——接受 PNG 或 JPEG 文件。
accept=“image/*”——接受任何带有 image/* MIME 类型的文件。(许多移动设备也允许用户在使用它时用摄像头拍照。)
accept=“.doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document”——接受类似于 MS Word 文档的任何文件。
capture 属性是一个字符串,如果 accept 属性指出了 input 是图片或者视频类型,则它指定了使用哪个摄像头去获取这些数据。值 user 表示应该使用前置摄像头和(或)麦克风。值 environment 表示应该使用后置摄像头和(或)麦克风。如果缺少此属性,则用户代理可以自由决定做什么。如果请求的前置模式不可用,则用户代理可能退回到其首选的默认模式。
不支持的浏览器会自动忽略这些属性
在实际开发中,安卓与苹果表现不一致,更多使用APP封装的调用方法,类似微信js-sdk方案。
<input type="file" accept="image/*" capture="camera" /> //只调用相机
<input type="file" accept="video/*" capture="camcorder" /> //只调用摄像机
<input type="file" accept="audio/*" capture="microphone" /> //只调用录音设备,苹果表现依然为调用摄像机
<input type="file" accept="image/*" /> //调用相机与文件相册
<input type="file" accept="video/*" /> //调用摄像机与文件视频
<input type="file" accept="audio/*" /> //调用录音设备与文件录音
多文件选择
如果给文件输入框添加一个 multiple 属性则可以一次选择多个文件
<input type="file" multiple />
自定义样式
通过 click() 方法使用隐藏的 file input 元素
你可以通过给 input 元素添加 display:none 的样式,
再调用 <input> 元素的 click() 方法来实现,
同时监听 file <input> 元素的change事件,获取上传的文件信息。
<!DOCTYPE html>
<html lang="Zh-CN">
<head>
<style type="text/css">
.file-choose {
color: white;
background: orange;
width: 200px;
height: 30px;
text-align: center;
line-height: 30px;
border-radius: 5px;
font-size: 14px;
cursor: pointer;
}
</style>
</head>
<body>
<input type="file" id="inputFile" style="display:none;" />
<div class="file-choose">选择文件</div>
</body>
<script>
const inputFile = document.querySelector('#inputFile'),
clickArea = document.querySelector('.file-choose')
clickArea.addEventListener('click',() => {
if(inputFile) inputFile.click()
})
inputFile.addEventListener('change',(e) => {
console.log(e.target.files) // FileList {0: File, length: 1}
}, false)
</script>
</html>
使用 label 元素来触发一个隐藏的 file input 元素
允许在不使用 JavaScript(click() 方法)来打开文件选择器,可以使用 <label> 元素。
注意在这种情况下,input 元素最好不使用 display: none(或 visibility: hidden)隐藏,否则 label 将无法通过键盘访问。
<!DOCTYPE html>
<html lang="Zh-CN">
<head>
<style type="text/css">
.visually-hidden {
position: absolute !important;
height: 1px;
width: 1px;
overflow: hidden;
clip-path: inset(5px);
}
/* Separate rule for compatibility, :focus-within is required on modern Firefox and Chrome */
input.visually-hidden:focus + label {
outline: thin dotted;
}
input.visually-hidden:focus-within + label {
outline: thin dotted;
}
.file-choose {
display: inline-block;
color: white;
background: orange;
width: 200px;
height: 30px;
text-align: center;
line-height: 30px;
border-radius: 5px;
font-size: 14px;
cursor: pointer;
}
</style>
</head>
<body>
<input type="file" multiple accept="image/*" id="inputFile" class="visually-hidden">
<label for="inputFile" class="file-choose">选择文件</label>
</body>
<script>
const inputFile = document.querySelector('#inputFile')
inputFile.addEventListener('change',(e) => {
console.log(e.target.files) // FileList {0: File, length: 1}
}, false)
</script>
</html>
这里不需要添加任何 JavaScript 代码来调用fileElem.click(),另外,这时你也可以给 label 元素添加你想要的样式。您需要在其 label 上提供隐藏 input 字段的焦点状态的视觉提示,比如上面用的轮廓,或者背景颜色或边框阴影。
基本上传方式
当把文件输入框放入表单中,提交表单的时候即可将选中的文件一起提交上传到服务器,需要注意的是由于提交的表单中包含文件,因此要修改一下表单元素的 enctype 属性为 multipart/form-data
<form action="#" enctype="multipart/form-data" method="post">
<input name="file" type="file" />
<input name="name" />
<button type="submit">上传</button>
</form>
这是传统的上传方式,且无法自定义请求header中的参数(如token信息),目前基本不会采用这种方式进行,仅做了解。
访问文件
File API 提供了访问文件的能力,files 属性访问,这会得到一个 FileList,这是一个集合,如果只选择了一个文件,那么集合中的第一个元素就是这个文件
传统的 DOM 选择器访问一个已经被选择的文件
const file = document.querySelector('input[type="file"]').files[0]
console.log(file.name) // 文件名称
console.log(file.size) // 文件大小
console.log(file.type) // 文件类型
通过 change 事件访问被选择的文件
<input type="file" onchange="handleFiles(this.files)" />
function handleFiles (files) {
console.log(files)
}
动态添加change监听器
你需要使用 EventTarget.addEventListener() 去添加 change 事件监听器,像这样:
const inputElement = document.getElementById("input");
inputElement.addEventListener("change", handleFiles, false);
function handleFiles() {
const fileList = this.files;
}
注意在这个例子里,handleFiles() 方法本身是一个事件处理器,不像之前的例子中,它被事件处理器调用然后传递给它一个参数。
Ajax 上传
由于可以通过 File API 直接访问文件内容,再结合 XMLHttpRequest 对象直接将文件上传,将其作为参数传给 XMLHttpRequest 对象的 send 方法即可。
const xhr = new XMLHttpRequest()
xhr.open('POST', '/upload/url', true)
xhr.send(file)
不过一些原因不建议直接这样传递文件,而是使用 FormData 对象来包装需要上传的文件,FormData 是一个构造函数,使用的时候先 new 一个实例,然后通过实例的 append 方法向其中添加数据,直接把需要上传的文件添加进去
const formData = new FormData()
formData.append('file', file, file.name) // 第 3 个参数是文件名称
formData.append('username', 'Mary') // 还可以添加额外的参数
数据准备好后,就是上传了,同样是作为参数传给 XMLHttpRequest 对象的 send 方法
<input type="file" id="fileInput" />
<button onclick="submit()">提交</button>
function submit() {
// 表单数据
const formData = new FormData()
const file = document.querySelector('#fileInput').files[0]
formData.append('file', file, file.name) // 第 3 个参数是文件名称
formData.append('appKey', 'xxx')
formData.append('appSecret', 'xxx')
const xhr = new XMLHttpRequest()
// 上传成功响应
function uploadComplete(evt) {
const data = JSON.parse(evt.target.responseText);
if (data.code === '200') {
console.log('上传成功!');
} else {
console.log('上传失败!');
}
}
// 上传失败
function uploadFailed() {
console.log('上传失败!');
}
// 取消上传
function cancelUploadFile() {
xhr.abort();
console.log('取消上传')
}
xhr.open('POST', 'http://www.xxx.com/url', true) // true 该参数规定请求是否异步处理。
xhr.onload = uploadComplete; // 请求完成
xhr.onerror = uploadFailed; // 请求失败
xhr.send(formData) // 开始上传,发送form数据
setTimeout(cancelUploadFile, 5000); // 5秒后,模拟取消上传操作
}
监测上传进度
XMLHttpRequest 对象还提供了一个 progress 事件,基于这个事件可以知道上传进度如何
const xhr = new XMLHttpRequest()
xhr.open('POST', '/upload/url', true)
xhr.upload.onprogress = progressHandler // 这个函数接下来定义
上传的 progress 事件由 xhr.upload 对象触发,在事件处理程序中使用这个事件对象的 loaded(已上传字节数) 和 total(总数) 属性来计算上传的进度
function progressHandler(e) {
const percent = Math.round((e.loaded / e.total) * 100)
}
上面的计算会得到一个表示完成百分比的数字,不过这两个值也不一定总会有,保险一点先判断一下事件对象的 lengthComputable 属性
function progressHandler(e) {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100)
}
}
上传进度详细实现
<input type="file" id="fileInput" />
<div>
<progress id="progressBar" value="0" max="100" style="width: 300px;"></progress>
<span id="percentage"></span>
<br/>
<span id="speed"></span>
<span id="remainTime"></span>
</div>
function submit() {
let oldTime = 0 // 上一次时间
let oldLoaded = 0 // 上一次加载数据字节大小
const progressBarDom = document.querySelector('#progressBar')
const percentageDom = document.querySelector('#percentage')
const speedDom = document.querySelector('#speed')
const remainTimeDom = document.querySelector('#remainTime')
function onprogress(evt) {
const nowTime = new Date().getTime() // 当前时间
const perTime = (nowTime - oldTime) / 1000 // 函数调用间隔时间,单位为s
oldTime = nowTime // 重新赋值,待下次计算
// event.total是需要传输的总字节,event.loaded是已经传输的字节。如果event.lengthComputable不为真,则event.total等于0
const perLoad = evt.loaded - oldLoaded // 函数调用间隔时间,新载入的字节大小
oldLoaded = evt.loaded // 重新赋值,待下次计算
let speed = perLoad / perTime // 原始速率,b/s
const originSpeed = speed // 不参与后续速率计算,b/s
let unit = 'b/s' // 速率单位
if (speed / 1024 > 1) {
speed /= 1024
unit = 'k/s'
}
if (speed / 1024 > 1) {
speed /= 1024
unit = 'M/s'
}
if (evt.lengthComputable) {
const loadPercent = (evt.loaded / evt.total) * 100
progressBarDom.value = Math.round(loadPercent) // 进度条百分比
percentageDom.innerText = `${(loadPercent).toFixed(1)}%` // 进度显示百分比
speedDom.innerText = speed.toFixed(1) + unit // 上传速率
remainTimeDom.innerText = `,还剩${((evt.total - evt.loaded) / originSpeed).toFixed()}秒` // 剩余时间
if (originSpeed === 0) remainTimeDom.innerHTML = '上传已取消'
}
}
const formData = new FormData()
formData.append('appKey', 'xxx')
formData.append('appSecret', 'xxx')
formData.append('file', document.querySelector('#fileInput').files[0])
const xhr = new XMLHttpRequest()
xhr.open('post', 'http://wwww.xxx.com/url', true)
xhr.upload.onprogress = onprogress
xhr.send(formData)
}
分割上传
使用文件对象的 slice 方法可以分割文件,给该方法传递两个参数,一个起始位置和一个结束位置,这会返回一个新的 Blob 对象,包含原文件从起始位置到结束位置的那一部分(文件 File 对象其实也是 Blob 对象,这可以通过 new File(['name'], 'testFile') instanceof Blob 确定,Blob 是 File 的父类)
const blob = file.slice(0, 1024 * 1024) // 文件从字节位置 0 到字节位置 1024*1024 即 1M
将文件分割成几个 Blob 对象分别上传就能实现将大文件分割上传
function upload(file) {
let formData = new FormData()
formData.append('file', file)
let xhr = new XMLHttpRequest()
xhr.open('POST', '/upload/url', true)
xhr.send(formData)
}
let pos = 0 // 起始位置
const size = 1024*1024 // 块的大小,即1M
// 通常用一个循环来处理更方便
while (pos < file.size) {
let blob = file.slice(pos, pos + size) // 结束位置 = 起始位置 + 块大小
upload(blob)
pos += size // 下次从结束位置开始继续分割
}
服务器接收到分块文件进行重新组装的代码就不在这里展示了
使用这种方式上传文件会一次性发送多个 HTTP 请求,那么如何处理这种多个请求同时发送的情况呢?方法有很多,可以用 Promise 来处理,让每次上传都返回一个 promise 对象,然后用 Promise.all 方法来合并处理,Promise.all 方法接受一个数组作为参数,因此将每次上传返回的 promise 对象放在一个数组中
var promises = []
while (pos < file.size) {
let blob = file.slice(pos, pos + size)
promises.push(upload(blob)) // upload 应该返回一个 promise
pos += size
}
同时改造一下 upload 函数使其返回一个 promise
function upload(file) {
return new Promise((resolve, reject) => {
let formData = new FormData()
formData.append('file', file)
let xhr = new XMLHttpRequest()
xhr.open('POST', 'http://www.xxx.com/url', true)
xhr.onload = () => resolve(xhr.responseText)
xhr.onerror = () => reject(xhr.statusText)
xhr.send(formData)
})
}
当一切完成后
Promise.all(promises)
.then((response) => {
console.log('Upload success!')
})
.catch((err) => {
console.log(err)
})
经实际测试,将大文件分割上传,可极大缩短总上传时间
拖拽上传
你还可以让用户将文件拖拽到你的网页应用中。
第一步是创建一个drop区域。虽然你网页内容的哪部分接受拖放取决于你的应用设计,但是使一个元素接收drop事件是很容易的。
const dropbox = document.getElementById("dropbox");
dropbox.addEventListener("dragenter", dragenter, false);
dropbox.addEventListener("dragover", dragover, false);
dropbox.addEventListener("drop", drop, false);
在这个例子中,我们将id为dropbox的元素变为了我们的drop区域。这是通过给元素添加dragenter dragover, 和drop 事件监听器实现的。
我们其实并不需要对dragenter and dragover 事件进行处理,所以这些函数都很简单。他们只需要包括禁止事件传播和阻止默认事件:
function dragenter(e) {
e.stopPropagation();
e.preventDefault();
}
function dragover(e) {
e.stopPropagation();
e.preventDefault();
}
真正的奥妙在drop()这个函数中:
function drop(e) {
e.stopPropagation();
e.preventDefault();
var dt = e.dataTransfer;
var files = dt.files;
handleFiles(files);
}
这里,我们从事件中获取到了dataTransfer
这个域,然后从中得到文件列表,再将它们传递给handleFiles()函数。在这之后,处理文件的方法与之前一致。
参考文章:http://blog.ncmem.com/wordpress/2023/11/05/web%e6%96%87%e4%bb%b6%e4%b8%8a%e4%bc%a0%e6%80%bb%e7%bb%93/
欢迎入群一起讨论