实现文件上传功能引发的关联知识探究

前言

前段时间,在使用axios封装http请求工具库的时候,由于只考虑了http接口常用的表单数据和JSON数据类型的发送和接收,没有考虑到文件类型的数据发与收,被测试提了一个bug。在完善http接口发送和接收文件类型的过程中,引发了心中的一连串疑问,笔者对这些知识盲点进行了查询之后,感觉有所收获,在此分享一下自己摄入的一些新知识点。我们从http协议支持的数据类型说起。

axios常见传输数据类型

使用axios发送HTTP请求时,Content-Type头部可以设置的类型有很多种,常见的类型有:

  1. 普通文本 :当HTTP传输纯文本数据时,Content-Type应设置成text/plain。在Web开发中,text/plain 更多用于静态文本文件的呈现,在动态API的请求与响应中不常见。
js 复制代码
import axios from 'axios';
// 提交数据示例
axios.post('/api/set/plaintext', 'Your plaintext message here', {
  headers: {
    'Content-Type': 'text/plain',
    'Accept': 'text/plain'
  }
})
.then((response) => {
  // ...
})
.catch((error) => {
  // 错误处理...
});

// 接收数据示例
axios.get('/api/get/plaintext', {
  headers: {
    'Accept': 'text/plain' // 指定期望接收的数据类型为纯文本
  }
})
.then(function (response) {
  // 响应数据处理
  console.log(response.data); // 输出纯文本响应体
})
  1. 表单数据

application/x-www-form-urlencoded 是最常见的POST表单提交数据的方式,URL编码表单数据,键值对之间用等号=连接,多个键值对之间用&符号连接。特殊字符会被编码

js 复制代码
// 提交表单类型数据示例
import axios from 'axios';
axios({
  method: 'post',
  url: 'https://example.com/api/login',
  data: "userName=John%20Doe&password=admin",
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded'
  }
}).then((response) => {
  console.log(response.data);
}).catch((error) => {
  console.error('Error:', error);
});
  1. JSON数据:axios默认的数据传输类型,也是最常用的数据类型。当数据是JavaScript对象时,Axios会自动将其转换为JSON字符串。常用于RESTful API接口的数据交互,参数封装和解析使用都很方便。
js 复制代码
import axios from 'axios';
const user = {
  id: 1,
  name: 'John Doe',
  email: 'john.doe@example.com'
};

axios({
  method: 'post',
  url: 'https://example.com/api/users',
  // axios会自动调用JSON.stringify处理成json字符串
  data: user, 
  headers: {
    'Content-Type': 'application/json'
  }
})
.then(response => {
  console.log(response.data);
})
.catch(error => {
  console.error('Error:', error);
});
  1. XML数据 :在传输XML格式的数据时,Content-Type可设置为application/xmltext/xml
js 复制代码
import axios from 'axios';
// 假设我们有以下XML数据
const xmlData = `
<?xml version="1.0"?>
<root>
  <element1>Hello</element1>
  <element2>World</element2>
</root>
`;

axios({
  method: 'post',
  url: 'https://example.com/api/xml-endpoint',
  data: xmlData, // XML数据作为字符串传递
  headers: {
    'Content-Type': 'application/xml' // 设置请求头内容类型为XML
  }
})
.then(response => {
  console.log(response.data);
})
.catch(error => {
  console.error('Error:', error);
});

application/xmltext/xml类型数据解析示例:

js 复制代码
import axios from 'axios';

axios.get('https://example.com/api/data', {
  // Axios默认不处理XML,这里不需要特别设置Content-Type
})
.then(response => {
  const parser = new DOMParser();
  const xmlDoc = parser.parseFromString(response.data, 'text/xml');

  // 现在你可以像操作DOM一样处理xmlDoc
  const rootNode = xmlDoc.documentElement;
  console.log(rootNode.localName); // 输出XML根节点的名称
  // ... 其它XML解析和处理操作...

})
.catch(error => {
  console.error('Error fetching data:', error);
});
  1. 二进制数据

这个范围比较广,如图像、音频、视频,Word,Excel,Pdf等非文本媒体文件,Content-Type会根据文件类型设定,如image/jpegaudio/mpegvideo/mp4等。传输二进制流数据采用multipart/form-dataapplication/octet-stream类型, 两者之间的差异后面会讲。

发送二进制数据示例:

js 复制代码
import axios from 'axios';
// 假设有文件输入元素获取到的文件对象
let fileInput = document.getElementById('file-input');
let file = fileInput.files[0];

// 创建一个FormData对象
let formData = new FormData();
// 添加文件字段
formData.append('file', file, file.name); 

// 使用axios发送POST请求
axios({
  method: 'post',
  url: 'https://example.com/api/upload',
  data: formData,
  headers: {
   'Content-Type': 'multipart/form-data' 
  }
}).then((response) => {
  console.log(response.data);
}).catch((error) => {
  console.error('Error:', error);
});

采用'Content-Type': 'application/octet-stream'方式上传二进制数据示例。

js 复制代码
axios.post('https://example.com/api/binary', binaryData, {
  headers: {
    'Content-Type': 'application/octet-stream'
  }
});

接收二进制数据示例:

js 复制代码
import axios from 'axios';

axios.get('https://example.com/api/download-file', {
  responseType: 'blob' // 指定响应类型为二进制blob
})
.then(response => {
  const blob = response.data;

  // 创建一个隐藏的可下载链接
  const url = window.URL.createObjectURL(blob);
  const link = document.createElement('a');
  link.href = url;
  link.download = 'filename.ext'; // 设置文件名
  document.body.appendChild(link); // 必须添加到DOM树中才能触发点击事件
  link.click();
  link.remove(); // 下载完成后移除链接元素
  window.URL.revokeObjectURL(url); // 释放内存中的blob URL引用
})
.catch(error => {
  console.error('Error downloading binary data:', error);
});

接收服务器返回的二进制数据,XML数据时,都要进行处理,而接收普通文本,表单数据和JSON数据,服务器返回的数据格式正确的话,无需特别配置,axios能自行处理。由于XML格式数据在Web应用开发中不太常用,而上传下载文件却很常见,所以在封装axios请求拦截器的时候,要记得加上 responseType: 'blob判断流程。

js 复制代码
const headerType = {
  json: {
    'Content-Type': 'application/json',
  },
  file: {
    'Content-Type': 'multipart/form-data',
  },
  form: {
    'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
  },
};

axios.interceptors.request.use((config: TAxiosConfig) => {
  const { method, dataType = 'json', params, timeout } = config;

  if (dataType === 'file') {
    config.responseType = 'blob';
    config.data = params;
    return config;
  }
  // dataType === 'file' 设置头部会出错
  config.headers = headerType[dataType];
  //...
 }

multipart/form-dataapplication/octet-stream类型的差异

再说 multipart/form-data之前,顺便温故一下FormData对象。上传文件为什么要使用FromData对象。文件上传的本质是对文件内容的二进制数据进行传输,而JSON和其他文本格式并不直接支持二进制数据的内嵌。当上传文件时,HTTP请求体需要遵循multipart/form-data格式,这种格式能够将多个部分(parts)打包在一起,每个部分可以有自己的元数据(如Content-Type、文件名等)。FormData正是为了创建和管理这种格式的数据而设计的。FormData不仅支持文件,还能添加其他非文件类型的键值对,这样可以在一次请求中同时上传文件和其它相关参数,保持数据完整性。所以文件上传通常都采用FormData对象而非其他类型。

'Content-Type': 'application/octet-stream' 通常用于直接上传二进制流数据,而不需要额外的元数据或多个字段,更适合单一的大块二进制数据的上传。

multipart/form-data 用于上传文件或包含二进制数据的表单提交,这种类型会把表单数据分割成多个部分(parts),每个部分有自己的Content-Type。|

在使用Axios上传二进制数据时,通常推荐使用 'Content-Type': 'multipart/form-data'。这是因为 'multipart/form-data' 适合上传文件时添加一些额外信息,它可以将不同的字段分开,每个部分可以有不同的Content-Type,传输数据相对'Content-Type': 'application/octet-stream'而言更灵活。

为什么文件上传要使用二进制数据而不是文本或json数据?

之前一直不明白为什么一涉及到文件的上传下载,就绕不过二进制文件流,为什么文件多采用二进制数据,而不是接口经常使用的json和文本数据。

文件之所以采用二进制数据而不是文本或JSON,主要是出于以下几个原因:

存储效率

文本和JSON格式的数据通常需要额外的空间来存储非二进制数据,比如在JSON中,数字、布尔值甚至字符串都需要额外的引号和转义字符。

  • 数字 : 在二进制格式中,可以直接存储整数或浮点数的原始二进制位表示。但在JSON中,即使是简单的数字也需要额外的文本字符来包裹它,例如数字 42 在JSON中表示为 "42" ------ 注意到了额外的双引号。
  • 布尔值 : 在二进制格式中,布尔值可能只需一位即可存储(0代表False,1代表True)。而在JSON中,每个布尔值都必须写作 "true""false",这包括了额外的引号以及固定的字符串长度。
  • 字符串 : 二进制格式可以直接存储字符串的原始字符序列。但在JSON中,字符串必须被双引号包围,并且其中包含的特殊字符(如反斜杠 ``、双引号 " 等)需要进行转义,如 "Hello, "world"!",这里的 " 就是用来转义内部的双引号。
  • 数组和对象 : JSON中的数组和对象同样需要额外的字符来表示结构,比如方括号 [ ] 和花括号 { },以及逗号 , 用于分隔元素。

二进制格式可以直接存储原始数据,不需要任何额外的字符或标记,因此在相同数据量的情况下,二进制格式通常更加紧凑,占用的存储空间更小。

数据完整性

许多类型的文件(如图像、音频、视频)包含了大量的原始比特数据,这些数据必须以二进制格式精确存储,以保留所有的细节信息。若转换成文本或JSON,不仅会增加大小,还可能导致精度损失或根本无法准确表示这些数据。 JSON也不能直接描述JPEG比特流中的编码细节,如霍夫曼编码、ZigZag扫描等JPEG特有的编码过程。ZigZag扫描是一种将8x8 DCT系数矩阵重新排列的策略,目的是将相邻的低频系数聚集在一起,便于霍夫曼编码时更有效地压缩数据。原本二维的矩阵会被转换为一维序列, 编码后的二进制数据流中,通过读取特定顺序的比特位组合,可以还原出对应的DCT系数,这是因为霍夫曼编码表已经预先定义好了每个系数与其对应的二进制码字之间的映射关系。而json数据因为在转换的过程中需要插入许多额外的符号,这些符号也需要用二进制数表示,从而破坏了原有二进制数据的位置关系。导致使用了压缩算法之后的二进制数据在解码时会出现细节丢失。

0x3F800000 0x40000000 0x40400000 0x40800000 ... 0x41400000
json 复制代码
[
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
]

性能和效率

对于程序而言,直接操作二进制数据通常更快,因为不需要进行字符编码或解码的过程。特别是在处理大量数据时,直接读写二进制文件能大大提升性能。

JSON编码(序列化)过程:

  1. 对象转换: 当我们有一个JavaScript对象,例如:

    js 复制代码
    const obj = {
      name: "Alice",
      age: 30,
      hobbies: ["reading", "painting"]
    };

    我们需要将其转换为JSON字符串,以便于存储或传输。JSON.stringify()方法可以完成这一任务:

    js 复制代码
    const jsonString = JSON.stringify(obj);
    // 结果:'{"name":"Alice","age":30,"hobbies":["reading","painting"]}'

此过程包括遍历对象属性,对特殊类型(如函数、日期、正则表达式)进行处理,并将所有数据转换为字符串形式。JSON.stringify()在内部需要创建一个新的字符串,这涉及到内存分配和字符串拼接操作,对于大型数据结构,这个过程可能会消耗一定的时间和资源。

JSON解码(反序列化)过程:

  1. 字符串解析: 收到JSON字符串后,我们需要将其转换回JavaScript对象,以便在程序中使用。JSON.parse()方法可以完成这个任务:

    js 复制代码
    const parsedObj = JSON.parse(jsonString);

    解析过程包括读取字符串,识别JSON语法结构,将字符串解析成对应的值类型(如字符串、数字、布尔值、数组和嵌套对象)。

  2. 内存分配: 解析过程中,为生成的对象和数组分配内存,对于大型数据结构,这也是一项性能开销。

对比操作二进制数据,比如直接使用ArrayBuffer或TypedArray,可以直接对内存中的原始数据进行操作,无需进行字符串转换,这减少了字符编码和解码带来的性能损耗。尤其是在处理大量数据时,这种性能差异尤为显著。例如,一个Float32Array可以直接读写内存中的32位浮点数,而不需要通过JSON字符串中间态。

专用功能和特性

二进制文件格式经常包含专为特定类型数据设计的功能和特性,比如图像文件中的颜色深度、透明度、压缩算法,音视频文件中的帧率、采样率、编码方式等。这些信息很难在文本或JSON格式中完整表达或者表达效率不高。

arduino 复制代码
png图片的头部信息示例: (十六进制):
00 00 00 0D // 数据块长度 Chunk length (13 bytes)
49 48 44 52 // 数据块类型 Chunk type (IHDR)
00 00 00 80 // 图片宽度 Width (128 pixels)
00 00 00 80 // 图片高度 Height (128 pixels)
08          // 颜色深度 Bit depth (8 bits per channel)

二进制流,Blob对象、ArrayBuffer关系

Blob对象、ArrayBuffer和二进制流在JavaScript中都与处理二进制数据相关,它们之间的关系和区别如下:

二进制流(Binary Streams)

  • 在JavaScript中,通常提到的二进制流是指通过Node.js环境的Stream API(如ReadableStream、WritableStream等)来处理连续的二进制数据流。
  • 在浏览器环境中,Fetch API返回的ReadableStream也可以看作是二进制流的一种,可用于处理大型数据流的分块加载或写入。示例:
js 复制代码
fetch('https://example.com/large-image.jpg')
  .then(response => {

    
    // 获取可读流
    const reader = response.body.getReader();

    // 定义一个读取流的迭代函数
    function readStream() {
      return reader.read().then(result => {
        if (result.done) {
          // 如果读取完毕,则不再处理
          return;
        }
        
        // 处理接收到的二进制数据
        const { value, done } = result;
        let chunk = new Uint8Array(value);
        
        // 在此处可以将二进制数据用于任何目的,例如:
        // 1. 写入到新的Blob对象,最终生成新的文件
        let blob = new Blob([chunk], { type: 'image/jpeg' });
        
        // 2. 显示预览(仅限小文件)
        if (blob.size < MAX_PREVIEW_SIZE) {
          let objectURL = URL.createObjectURL(blob);
          let previewImg = document.getElementById('preview-image');
          previewImg.src = objectURL;
        }

        // 继续读取剩余的数据
        return readStream();
      });
    }

    // 启动流的读取
    return readStream();
  })
  .catch(error => {
    console.error('Error while reading the stream:', error);
  });
  • 二进制流允许数据逐步读取或写入,而非一次性全部加载到内存,这对于处理大数据非常有用,可以有效避免内存溢出问题。

Blob对象(Blob)

  • Blob代表"Binary Large Object",即二进制大对象。它是一个包含不可变原始数据的类文件对象,可以容纳任意类型的二进制数据,比如图片、音频、视频或者其他类型的文件内容。
  • Blob对象不提供直接操作内部数据的能力,但可以通过slice()方法创建子Blob,或者通过FileReader API读取Blob内容。
  • Blob可以用于创建URL引用(通过URL.createObjectURL(blob))供HTML元素(如<img><audio><video>)使用,或者通过Ajax上传至服务器。

ArrayBuffer

  • ArrayBuffer是JavaScript中用于表示通用、固定长度的原始二进制数据缓冲区的接口。它是一个底层的API,提供了对原始二进制数据的直接访问,但并非直接操作字符或字节,而是通过Int8ArrayUint8Array等TypedArray视图或DataView来读写。
  • ArrayBuffer的优势在于可以高效、灵活地处理二进制数据,例如在处理WebSocket消息、文件读写、网络传输等场景。

例如音频数据通常是 PCM 格式的,可以通过 Float32Array 来表示单精度浮点数的音频样本。并对内容进行裁剪。

js 复制代码
let audioBuffer = ... // 音频数据
let audioData = new Float32Array(audioBuffer);

// 获取音频总时长
let sampleRate = 44100; // 假设音频采样率为44.1kHz
let duration = audioData.length / sampleRate;

// 裁剪音频数据
let startIndex = 10 * sampleRate; // 从第10秒开始
let endIndex = 30 * sampleRate; // 到第30秒结束
let clippedData = audioData.subarray(startIndex, endIndex);

// 进一步处理或发送到音频处理API
doSomethingWithClippedData(clippedData);

在现代浏览器中,fetch API的响应体可以被处理为Blob、ArrayBuffer或是ReadableStream,无论是Blob对象还是ArrayBuffer,它们都可以用于存储或临时持有从网络请求、文件系统或用户上传等来源获取的二进制数据流。Blob对象和ArrayBuffer在JavaScript中可以相互转换。例如,可以将ArrayBuffer传递给Blob构造函数创建Blob对象,反之亦然,通过FileReader的readAsArrayBuffer方法可以从Blob对象中读取到ArrayBuffer。

图片预览功能 为什么不直接使用文件对象?

如下代码片段,实现上传图片的预览功能,

js 复制代码
const inputElement = document.querySelector('input[type=file]');
inputElement.addEventListener('change', (event) => {
  const file = event.target.files[0];
  const reader = new FileReader();
  reader.onload = function(event) {
    const imgTag = document.querySelector('#preview-img');
    imgTag.src = event.target.result; // 这里的event.target.result就是图片的Data URL
  };
  reader.readAsDataURL(file);
});

这里的event.target.files[0] 表示是用户通过 <input type="file"> 选择的文件对象,而这个文件对象并不能直接用于图片预览或展示在网页上。浏览器的安全策略不允许直接从文件对象创建图像源(src)地址,主要是出于以下安全考虑:

  1. 防止恶意脚本注入 : 如果浏览器允许直接将文件路径或内容设置为<img>标签的src属性,那么恶意用户可以通过提交恶意脚本文件作为图像源,从而可能引发跨站脚本攻击(XSS)风险。浏览器默认阻止这种行为,以防止潜在的安全漏洞。
  2. 保护用户隐私: 直接访问文件系统可能会暴露用户的私人文件。如果浏览器允许随意读取文件系统中的文件并显示在网页上,用户的敏感信息(如个人照片、文档等)可能会在用户不知情的情况下被网站获取和展示。
  3. 沙箱模型和同源策略 : 浏览器基于沙箱模型运行网页内容,同源策略限制了不同源之间的交互,防止一个网页未经授权访问另一个源的数据。文件对象来源于用户的本地文件系统,不在网页的同源范围内,因此浏览器不允许直接使用文件对象的路径或内容作为src属性。

所以我们不能直接把文件对象赋给 <img> 标签的 src 属性。为了能在网页上预览图片,我们需要将图片文件的内容转换成可以在浏览器中显示的数据格式,通常是 Data URL(以 data:image/*;base64, 开头的字符串)。FileReader API 的 readAsDataURL 方法就可以帮助我们实现这个转换过程。

最后

感觉许多知识点都不是孤立的,而是一个知识体系中的组成要素,只看到树木而看不到森林,一是考虑问题,开发功能时容易出现纰漏,二是会被与之相关的知识点搞得有些晕乎,同样一个功能,有多种实现方式,该如何选择才是正确的。每种方式的应用场景是什么,有哪些区别。只有了解了整个知识体系,应用起来才不会困惑。

相关推荐
凌晨五点的星11 分钟前
网络安全-webshell绕过,hash碰撞,webshell绕过原理
开发语言·前端·javascript
天心天地生28 分钟前
【bugfix】-洽谈回填的图片消息无法显示
开发语言·前端·javascript
啧不应该啊38 分钟前
element plus 按需导入vue
前端·javascript·vue.js
Gungnirss42 分钟前
vue中提示Parsing error: No Babel config file detected
前端·vue.js·ubuntu
梅秃头1 小时前
vue2+elementUI实现handleSelectionChange批量删除-前后端
前端·javascript·elementui
请叫我欧皇i1 小时前
el-form动态标题和输入值,并且最后一个输入框不校验
前端·javascript·vue.js
小小李程序员1 小时前
css边框修饰
前端·css
我爱画页面2 小时前
使用dom-to-image截图html区域为一张图
前端·html
忧郁的西红柿2 小时前
HTML-DOM模型
前端·javascript·html