前端文件篇

401 阅读8分钟

文件上传和下载是很常见的功能,今天就来梳理一下操作文件过程中常见的概念和api,加深对文件的理解。

概念

Blob 对象

当用户通过一个input元素选择文件时,浏览器会创建一个Blob对象代表该文件的二进制数据。如果要在将文件数据上传到服务器或者存储到本地之前对其进行操作,你可能需要使用 Blob对象 。

举个例子,你可以使用URL.createObjectURL方法创建一个URL代表这个Blob对象,然后用这个URL<img>video元素中展示这个文件。如:

const img = document.createElement('img');
img.src = URL.createObjectURL(file);

File 对象

当用户通过一个input元素选择文件时,浏览器会创建一个基于Blob对象的File对象,并添加文件名name、大小size、上次修改日期lastModifiedData等属性。如果要在将文件上传到服务器之前验证文件的属性,则可能需要使用File对象。

也就是说,File 对象是一种 Blob 对象,他包含了文件的附加信息,如 name、size、lastModifiedDate 等属性。开发人员无法直接访问底层的 Blob 对象,而是通过 api 与 File 对象进行交互。

base 64

base64 编码将二进制数据表示为一串 ASCII 字符「我们常说的字符串」。

有些场合并不能传输或者储存二进制流,这时候就需要使用 base64 编码。

比如,一个传输协议是基于 ASCII 文本的,那么他就不能传输二进制流,想要传输该二进制流就得编码。常用的 http 协议的 url 就是纯文本的,不能直接放二进制流。

大多数现代语言的 String 类型,都不能直接存储二进制流,但可以储存 base64 编码的字符串。

举个例子,你可以使用FileReader.readAsDataURL()方法读取 File 对象的内容并将其转换为 base64 编码的data:URL 格式的字符串,简称DataURL

这个 DataURL 表示所读取的文件内容,可以将其发送到服务器。然后服务器可以将 DataURL 解码回二进制数据并将其保存为文件。

/**
 * file 转 base64编码
 * @param {File} file 文件流
 * @param {base64Callback} callback 回调函数 - 处理响应
 */
export const fileByBase64 = (file, callback) => {
  var reader = new FileReader();

  // onload事件在读取操作完成时触发
  reader.onload = function (e) {
    // e.target.result/reader.target 该属性表示目标对象的DataURL
    console.log(e.target.result);
    callback && callback(e.target.result)
  };

  // 以 data:URL 的形式读取数据
  reader.readAsDataURL(file);
}

简单的文件上传

文件上传的传统形式,是使用type='file'input表单元素

<input type='file' id='file-input' accept='.jpg, .jpeg, .png' multiple />

可以添加change事件监听读取文件对象列表event.target.files

const fileInput = document.getElementById('file-input')
fileInput.addEventListener('change', (e) => {
  const files = e.target.files
})

File 对象:

文件上传前,可以通过File对象,验证文件大小、类型等信息,决定是否进行下一步,比如验证文件大小。

验证文件大小

<input type="file" id="file-input">
<button id="upload-btn">Upload File</button>
const fileInput = document.getElementById('file-input')
const uploadBtn = document.getElementById('upload-btn');

uploadBtn.addEventListener('click', () => {
  const selectedFile = fileInput.files[0];
  const fileSizeLimit = 5 * 1024; // 5KB in bytes

  if (selectedFile.size > fileSizeLimit) {
    alert('Selected file exceeds the size limit of 5 KB. Please select a smaller file.');
    return;
  }

  // If file size is within the limit, proceed with file upload
  // Your code for uploading the file to the server goes here
});

获取 files 的方式:

  • 在input元素的change事件中,可通过 e.target.files 获取;
  • 或者通过input元素直接获取,如:document.getElementById('file-input').files;

也就是说e.target === fileEl

显示读取进度(下载文件的场景)

FileReader.onprogress

pregress事件,在读取Blob时触发。在下载文件并显示进度这个场景下能够派上用场。

<input type="file" id="file-input">
<div>
  <label id="progress-label" for="progress">Upload File</label>
  <progress id="progress" value="0" max="100" value="0">0</progress>
  <button id="read-blob">读取Blob</button>
</div>

readAsDataURL能够读取Blob对象,然后监听FileReaderprogress事件,通过ProgressEvent.loadedProgressEvent.total计算读取的进度。

const fileInput = document.getElementById('file-input')
const readBlobBtn = document.getElementById('read-blob');

// 显示文件读取进度
const reader = new FileReader()

reader.addEventListener('progress', (e) => {
  if (e.loaded && e.total) {
    // 计算完成百分比
    const percent = (e.loaded / e.total) * 100
    // 将值设置为进度组件
    progress.value = percent
  }
})

readBlobBtn.addEventListener('click', () => {
  const selectedFile = fileInput.files[0];
  // 以 URL 格式的字符串的形式读取数据
  reader.readAsDataURL(selectedFile);
})

显示上传进度

要显示文件的上传进度,可以使用 JavaScript 中的 XMLHttpRequest (XHR) 对象将文件上传到服务器并使用 XMLHttpRequest.upload.onprogress 事件跟踪进度。这是一个例子:

const fileInput = document.getElementById('file-input');
const uploadBtn = document.getElementById('upload-btn');
const uploadProgress = document.getElementById('upload-progress');

uploadBtn.addEventListener('click', () => {
  const selectedFile = fileInput.files[0];
  const xhr = new XMLHttpRequest();
  xhr.open('POST', '/upload');

  // Track the upload progress
  xhr.upload.onprogress = (event) => {
    const progress = (event.loaded / event.total) * 100;
    uploadProgress.value = progress;
  };

  xhr.onload = () => {
    console.log('File uploaded successfully');
  };

  xhr.onerror = () => {
    console.log('File upload failed');
  };

  const formData = new FormData();
  formData.append('file', selectedFile);

  xhr.send(formData);
});

上传目录

input元素的webkitdirectory属性,表示允许用户选择文件目录,而不是文件。

<input type="file" id="file-input" webkitdirectory>

选择目录时,该目录下的文件会全部选中(包括子孙文件)。

拖放上传

设置一个放置文件的目标元素。

<div id="drop-zone">Drop files here</div>

调用 event.preventDefault(),这使它能够接收 drop 事件。

const dropZone = document.getElementById('drop-zone');

// dragover 事件在可拖动元素被拖进放置目标时被触发
dropZone.addEventListener('dragover', (event) => {
  event.preventDefault();
  dropZone.classList.add('drag-over');
});

// dragleave 事件在可拖动元素离开放置目标时被触发
dropZone.addEventListener('dragleave', (event) => {
  event.preventDefault();
  dropZone.classList.remove('drag-over');
});

// drop 事件在可拖动元素放置在放置目标时被触发
dropZone.addEventListener('drop', (event) => {
  event.preventDefault();
  dropZone.classList.remove('drag-over');
  // 获取文件
  const files = event.dataTransfer.files;
  console.log('files: ', files);

  const xhr = new XMLHttpRequest();
  xhr.open('POST', '/upload');

  xhr.onload = () => {
    console.log('File uploaded successfully');
  };

  xhr.onerror = () => {
    console.log('File upload failed');
  };

  const formData = new FormData();
  formData.append('file', files[0]);

  xhr.send(formData);
});

Content-Type(表单的enctype属性)

当 method 属性值为 post 时,enctype 就是将表单的内容提交给服务器的数据编码类型。可能的取值有:

  • application/x-www-form-urlencoded:未指定属性时的默认值。
  • multipart/form-data:当表单包含 type=file 的Input元素时使用此值
  • text/plain:HTML5 引入的类型。

application/x-www-form-urlencoded

这种数据编码类型只支持传输文本数据

POST http://www.example.com HTTP/1.1
Content-Type: application/x-www-form-urlencoded;charset=utf-8

title=test&sub%5B%5D=1&sub%5B%5D=2&sub%5B%5D=3

首先,Content-Type 被指定为 application/x-www-form-urlencoded;其次,提交的数据按照 key1=val1&key2=val2 的方式进行编码,keyval 都进行了 URL 转码。大部分服务端语言都对这种方式有很好的支持。

此类型不适合用于传输大型二进制数据或者包含非ASCII字符的数据。平常我们使用这个类型都是把表单数据使用url编码后传送给后端,二进制文件当然没办法一起编码进去了。所以multipart/form-data就诞生了。

multipart/form-data

为了支持文件上传,表单数据必须使用multipart/form-data内容类型进行编码。这种编码格式允许二进制数据作为请求主体的一部分发送。

POST http://www.example.com HTTP/1.1
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryrGKCBY7qhFd3TrwA

------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="text"

title
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="file"; filename="chrome.png"
Content-Type: image/png

PNG ... content of chrome.png ...
------WebKitFormBoundaryrGKCBY7qhFd3TrwA--

Content-Type被指定为multipart/form-databoundary----WebKitFormBoundaryrGKCBY7qhFd3TrwAboundary用于分割提交的数据。

Content-Disposition包含文件的基本信息,Content-Type表示文件内容类型。

第一部分是一个名为text的表单字段,该字段的内容是字符串title。第二部分是名为file的字段,文件名为chrome.png,文件内容类型为image/png,内容为PNG ... content of chrome.png ...,然后以boundary为结尾。

FormData()

这是一个传统的最简单的form表单上传如下:

<form method="POST" enctype="multipart/form-data">
  <input type="file" name="file" value="请选择文件"><br />
  <input type="submit">
</form>

FormData的由来:

当使用 Ajax 上传文件时,如果不指定enctype="multipart/form-data",会导致后端在解析Form表单的数据格式时与Ajax上传的数据格式不一致的问题。为了后端能够使用相同的代码解析这两种提交方式,所以出现了FormData

FormData 接口提供了一种表示表单数据的键值对key/value的构造方式,可以轻松的将数据通过 Ajax 发送出去。

const formData = new FormData();

// 单文件
formData.append('files', file);

// 多文件
_files.forEach((file) => {
  formData.append('files', file);
});

使用 FormData 上传文件时,无需手动设置Content-Type='multipart/form-data',FormData 会自动设置正确的 Content-Type 和 数据类型。

实际开发过程中也是如此,通常会使用 FormData 格式保存文件,关于Content-Type需要什么类型,取决于后端是怎么设计的。

下载

window.open

open()方法,用于将指定的资源加载到浏览器新的窗口或者标签页。

当我们指定一个图片链接时,浏览器会自动下载该资源,下载后自动关闭该窗口。

const imgUrl = 'https://nd-news-mangement.oss-cn-hangzhou.aliyuncs.com/2023/04/274a8263e05f37a5d8663193b86e1a0583.png'
window.open(imgUrl);

location.href

location.href表示将当前页面的URL设置为一个新的值。它是一个字符串,包含当前页面的完成URL,包括协议、域名、路径、查询参数和片段标识符。

const imgUrl = 'https://nd-news-mangement.oss-cn-hangzhou.aliyuncs.com/2023/04/274a8263e05f37a5d8663193b86e1a0583.png'
location.href = imgUrl

window.openlocation.href的区别:

  • window.open会打开一个新窗口或选项卡,而location.href会替换当前页
  • window.open 打开太多新窗口可能会对用户体验产生负面影响
  • location.href可能会刷新整个页面(但如果在输入框中输入了文本,加载新页面时文本内容不会丢失,这是因为浏览器通常将表单数据保存在浏览器的缓存中)

a标签

a 标签通常用于用户启动的交互,而location.href通常用于响应用户事件。

a 标签的方式属性更多,如downloadtarget,也更为灵活,在实际开发过程中,我们通常会使用a标签封装一个下载功能的函数来使用,如下:

function downloadFile(url, fileName) {
  const link = document.createElement('a');
  link.href = url;
  link.target = '_blank';
  // download仅适用于同源 URL
  link.download = fileName;
  link.style.display = 'none';
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
}

总结

  • File 对象是基于 Blob 对象的,只不过多了一些附加信息,如 namesize
  • 想要显示文件读取进度时,我们可以通过 FileReader 的 readAsDataURL(file) 方法,然后监听FileReaderprogress事件
  • 想要显示上传到服务器的进度时,可以通过xhr.upload.onprogress事件
  • 使用 FormData 上传文件时,无需手动设置Content-Type='multipart/form-data',FormData 会自动设置正确的 Content-Type 和 数据类型

实际项目应用中,大多数的表单场景都是手动上传文件到服务器,也就是说在提交到服务器之前,我们是不需要使用后端接口的。但现实中遇到的情况往往是后端提供一个上传接口,再提供一个提交表单数据的接口,前一个接口就显得很多余,而且存在数据库内存被乱用的风险。

参考: