Web文件上传总结
创始人
2025-05-28 07:58:35

文章目录

    • 指定文件类型
    • 多文件选择
    • 自定义样式
      • 通过 click() 方法使用隐藏的 file input 元素
      • 使用 label 元素来触发一个隐藏的 file input 元素
    • 基本上传方式
    • 访问文件
      • 传统的 DOM 选择器访问一个已经被选择的文件
      • 通过 change 事件访问被选择的文件
      • 动态添加change监听器
    • Ajax 上传
    • 监测上传进度
    • 分割上传
    • 拖拽上传

文件上传是 Web 开发常见需求,上传文件需要用到文件输入框。

指定文件类型

  • 一个以英文句号(“.”)开头的合法的不区分大小写的文件名扩展名。例如:.jpg.pdf.doc

  • 一个不带扩展名的 MIME 类型字符串。

  • 字符串 audio/*,表示“任何音频文件”。

  • 字符串 video/*,表示“任何视频文件”。

  • 字符串 image/*,表示“任何图片文件”。

accept 属性的值是包含一个或多个(用逗号分隔)唯一文件类型说明符的字符串。例如,一个文件选择器需要能被表示成一张图片的内容,包括标准的图片格式和 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方案。

 //只调用相机
 //只调用摄像机
 //只调用录音设备,苹果表现依然为调用摄像机 //调用相机与文件相册
 //调用摄像机与文件视频
 //调用录音设备与文件录音

多文件选择

如果给文件输入框添加一个 multiple 属性则可以一次选择多个文件


自定义样式

通过 click() 方法使用隐藏的 file input 元素

  1. 你可以通过给 input 元素添加 display:none 的样式,
  2. 再调用 元素的 click() 方法来实现,
  3. 同时监听 file 元素的change事件,获取上传的文件信息。

选择文件

使用 label 元素来触发一个隐藏的 file input 元素

  1. 允许在不使用 JavaScript(click() 方法)来打开文件选择器,可以使用 元素。
  2. 注意在这种情况下,input 元素最好不使用 display: none(或 visibility: hidden)隐藏,否则 label 将无法通过键盘访问。



这里不需要添加任何 JavaScript 代码来调用fileElem.click(),另外,这时你也可以给 label 元素添加你想要的样式。您需要在其 label 上提供隐藏 input 字段的焦点状态的视觉提示,比如上面用的轮廓,或者背景颜色或边框阴影。

基本上传方式

当把文件输入框放入表单中,提交表单的时候即可将选中的文件一起提交上传到服务器,需要注意的是由于提交的表单中包含文件,因此要修改一下表单元素的 enctype 属性为 multipart/form-data

这是传统的上传方式,且无法自定义请求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 事件访问被选择的文件

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 方法



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)}
}

上传进度详细实现



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 // 函数调用间隔时间,单位为soldTime = nowTime // 重新赋值,待下次计算// event.total是需要传输的总字节,event.loaded是已经传输的字节。如果event.lengthComputable不为真,则event.total等于0const perLoad = evt.loaded - oldLoaded // 函数调用间隔时间,新载入的字节大小oldLoaded = evt.loaded // 重新赋值,待下次计算let speed = perLoad / perTime // 原始速率,b/sconst originSpeed = speed // 不参与后续速率计算,b/slet unit = 'b/s' // 速率单位if (speed / 1024 > 1) {speed /= 1024unit = 'k/s'}if (speed / 1024 > 1) {speed /= 1024unit = 'M/s'}if (evt.lengthComputable) {const loadPercent = (evt.loaded / evt.total) * 100progressBarDom.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 = onprogressxhr.send(formData)
}

分割上传

使用文件对象的 slice 方法可以分割文件,给该方法传递两个参数,一个起始位置和一个结束位置,这会返回一个新的 Blob 对象,包含原文件从起始位置到结束位置的那一部分(文件 File 对象其实也是 Blob 对象,这可以通过 new File(['name'], 'testFile') instanceof Blob 确定,BlobFile 的父类)

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 应该返回一个 promisepos += 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()函数。在这之后,处理文件的方法与之前一致。

相关内容

热门资讯

淮南子简介 淮南子简介  在我们平凡的日常里,大家都有了解过淮南子吗?以下是小编帮大家整理的淮南子简介,供大家参...
清明的谚语 关于清明的谚语12篇  无论是身处学校还是步入社会,大家一定没少看到过谚语吧,谚语是民众的丰富智慧和...
新春对联 新春对联(精选130句)  在日常学习、工作抑或是生活中,大家都听说过或者使用过一些比较经典的对联吧...
牛年新年对联 牛年新年对联大全  无论是身处学校还是步入社会,许多人都接触过一些比较经典的.对联吧,对联对仗工整,...
五字春联 五字春联大全  在学习、工作或生活中,许多人对一些广为流传的'春联都不陌生吧,春联的张贴,要符合传统...