Content-Type的类型详解/前后端对于不同Content-type数据类型的处理

Content-Type的类型详解

引言

在前端开发中,我们经常需要与后端进行数据交互。然而,在发送网络请求时,很多开发者可能会遇到一个共同的问题:如何正确地设置请求头中的Content-Type以及后端如何处理接收到的数据。与数据类型相关的请求/响应头主要是两个:Accept和Content-Type,其中Content-Type在请求头和响应头中都存在。我们从请求头的角度来介绍各个数据类型。

本博客将深入解析Content-Type的含义,介绍它们的分类和原因,并从前端和后端两个角度分别说明如何使用和处理。

Content-Type介绍

Content-Type作用

Content-Type头部是在客户端向服务器发送请求时,指定请求体的媒体类型。服务器据此判断请求体的格式,从而正确解析数据。

Content-Type 分类

Content-Type的分类是基于MIME类型的,MIME类型通常由两部分组成,一部分是媒体类型(media type),另一部分是子类型(subtype),用斜杠分隔。例如,text/html表示数据的大类是文本,子类是HTML。

MIME 类型有很多种,不同的应用场景可能会使用不同的 MIME 类型。

一般来说,常见的 MIME 类型有以下几类:

  • text:表示纯文本或者文本格式的数据,如 text/plain, text/html, text/css, text/xml 等。

  • image:表示图像或者图形格式的数据,如 image/jpeg, image/png, image/gif, image/svg+xml 等。

  • audio:表示音频或者声音格式的数据,如 audio/mpeg, audio/wav, audio/ogg 等。

  • video:表示视频或者动画格式的数据,如 video/mp4, video/webm, video/ogg 等。

  • application:表示其他类型的数据,通常是二进制格式或者特定应用程序的格式,如 application/pdf, application/zip, application/json, application/javascript 等。

  • multipart:表示多个部分组成的数据,每个部分可以有自己的 MIME 类型,如 multipart/form-data, multipart/mixed 等。

  • message:表示电子邮件或者其他消息格式的数据,如 message/rfc822, message/http 等。

几种经典的Content-type的介绍

application/x-www-form-urlencoded

这是一种用于发送表单数据的类型,它会将数据以键值对的形式编码,例如name=Tom&age=18。键和值都会进行URL编码,以避免特殊字符的影响。

这种类型的优点是可以发送任何类型的字符,但缺点是不能发送二进制数据,如文件上传,而且URL编码会增加数据的长度,可能会超过服务器的限制。

表单发送

这是默认的表单编码类型,它会将表单中的数据经过URL编码后,用&符号分隔,发送到服务器。

例如,如果表单中有两个字段,fname=张,lname=san,html内容为:

<form
	action="http://localhost:3000/test"
	method="post"
	enctype="application/x-www-form-urlencoded"
>
	First name: <input type="text" name="fname" /><br />
	Last name: <input type="text" name="lname" /><br />
	<input type="submit" value="Submit" />
</form>

那么发送的数据就是:

fname=%E5%BC%A0&lname=san
fetch发送

总的来说这种方式使用fetch比较少:

我们在请求体中传入编码后的字符串(在body字段中添加形如name=Tom&age=18的数据即可),具体来说有这几种方式。

直接手写

这种方式其实不好,因为可能会对某些字符忽略了url编码

fetch('https://example.com/api', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded'
  },
  body: 'name=Tom&age=18'
})
.then(response => response.json())
.then(data => console.log(data));
手动拼接

手动拼接之前为合法字符串,首先进行url的编码才行

var details = {
    'userName': 'test@gmail.com',
    'password': 'Password!',
    'grant_type': 'password'
};

var formBody = [];
for (var property in details) {
  var encodedKey = encodeURIComponent(property);
  var encodedValue = encodeURIComponent(details[property]);
  formBody.push(encodedKey + "=" + encodedValue);
}
formBody = formBody.join("&");

fetch('https://example.com/login', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
  },
  body: formBody
})
使用URLSearchParams统一的处理
const formData = new URLSearchParams({
    username: "test@gmail.com",
    password: "Password",
    grant_type: "password",
});
// 发送Fetch请求
fetch("https://example.com/api/login1", {
    method: "POST",
    headers: {
        "Content-Type": "application/x-www-form-urlencoded",
    },
    body: formData.toString(),
})
    .then((response) => response.json())
    .then((data) => console.log(data))
    .catch((error) => console.error("Error:", error))
后端处理

// 后端使用express接收application/x-www-form-urlencoded类型的数据
const express = require('express');

const app = express();

// 使用body-parser中间件的urlencoded方法来解析请求体
app.use(express.urlencoded({extended: false}));

app.post('/api', (req, res) => {
  // req.body是一个对象,其中每个键值对对应一个表单字段和值
  console.log(req.body); // {name: 'Tom', age: '18'}
})

application/json

这是一种用于发送JSON数据的类型,它会将数据以JSON字符串的形式编码,例如{"name":"Tom","age":18}

使用起来其实和www-form-urlencoded类似,感觉更加的方便一些。

fetch发送

使用JSON.stringify转一下即可

// 前端使用fetch发送application/json类型的数据
fetch('https://example.com/api', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({name: 'Tom', age: 18})
})
.then(response => response.json())
.then(data => console.log(data));
后端处理
// 后端使用express接收application/json类型的数据
const express = require('express');

const app = express();

// 使用epress自带的中间件的json方法来解析请求体
app.use(express.json());

app.post('/api', (req, res) => {
  // req.body是一个对象,其中每个键值对对应一个JSON字段和值
  console.log(req.body); // {name: 'Tom', age: 18}
})

multipart/form-data

这是一种用于上传文件的类型,它会将数据以多部分的形式编码,每个部分都有一个边界和一个内容类型。这种类型用于需要发送二进制数据的情况,如文件上传

这种类型的优点是可以发送任何类型的数据,包括二进制数据,但缺点是数据的格式比较复杂,需要额外的分隔符和描述信息,可能会增加数据的开销。

表单上传

它会将表单中的数据分成多个部分,每个部分都有一个分隔符和一个描述信息,发送到服务器。

例如表单代码为:

<form
	action="http://localhost:3000/files"
	method="post"
	enctype="multipart/form-data"
>
	姓名:<input type="text" name="name">
	<br/>
	请选择要上传的文件:<input type="file" name="file" /> <br>
	<input type="submit" value="上传" />
</form>

表单格式如下:

image-20231109163221475

上传的数据为:

image-20231109163249606

上述数据是一个多部分表单数据格式的字符串,它由以下几个部分组成:

  • ------WebKitFormBoundarylSNPeJJRkGsWkuKA,这是一个分隔符,用于区分不同的表单数据部分,它是由浏览器自动生成的,每个浏览器可能有不同的分隔符。

  • Content-Disposition: form-data; name="name"
    

    ,这是一个表单数据部分的头部,用于描述这个部分的信息,它有以下几个部分:

    • Content-Disposition,这是一个 HTTP 头部字段,用于指定这个部分的处理方式,它的值是 form-data,表示这是一个表单数据部分。
    • name,这是一个参数,用于指定这个部分的名称,它的值是 name,表示这是用户输入的姓名。
  • 张三,这是一个表单数据部分的内容,用于存储这个部分的数据,它的值是 张三,表示用户输入的姓名是张三。

  • Content-Disposition: form-data; name="file"; filename="EDG.jpg"
    

    ,这是另一个表单数据部分的头部,用于描述这个部分的信息,它有以下几个部分:

    • Content-Disposition,这是一个 HTTP 头部字段,用于指定这个部分的处理方式,它的值是 form-data,表示这是一个表单数据部分。
    • name,这是一个参数,用于指定这个部分的名称,它的值是 file,表示这是用户选择的文件。
    • filename,这是另一个参数,用于指定这个部分的文件名,它的值是 EDG.jpg,表示用户选择的文件名是 EDG.jpg。
  • Content-Type: image/jpeg,这是另一个 HTTP 头部字段,用于指定这个部分的内容类型,它的值是 image/jpeg,表示这是一个 JPEG 格式的图像文件。

  • ------WebKitFormBoundarylSNPeJJRkGsWkuKA--,这是一个结束符,用于标记多部分表单数据的结束,它是由分隔符加上两个连字符组成的。

fetch发送

使用FromData的实例来转化为该类型,如果是文件,直接将对应的二进制作为键即可。不需要在请求头中设置Content-Type,浏览器会自动添加。

// 前端使用fetch发送multipart/form-data类型的数据
const formData = new FormData();
formData.append('name', 'Tom');
formData.append('age', 18);
formData.append('file', input.files[0]); // input是一个文件输入框

fetch('https://example.com/api', {
  method: 'POST',
  body: formData
})
.then(response => response.json())
.then(data => console.log(data));

后端处理

对于multipart/form-data类型,可以使用multer中间件,它会将请求体中的文件数据解析为一个对象,存放在req.file或req.files属性中,而其他的文本数据则存放在req.body属性中。例如:

// 后端使用express接收multipart/form-data类型的数据
const express = require('express');
const multer = require('multer');

const app = express();

// 使用multer模块来解析请求体
const upload = multer({dest: 'uploads/'});

app.post('/api', upload.any(), (req, res) => {
  // req.body是一个对象,其中每个键值对对应一个表单字段和值
  console.log(req.body); // {name: 'Tom', age: '18'}
  // req.files是一个数组,其中每个元素是一个文件对象
  console.log(req.files); // [{fieldname: 'file', originalname: 'test.jpg', ...}]
});

multer会自动存储到后端目录中的uploads/中,并且以二进制的文件格式存储。

打印的req.file内容如下:

{
  fieldname: 'file',
  originalname: '9dd38fb5ly1h6728s4qegj22yo1o0af3.jpg',
  encoding: '7bit',
  mimetype: 'image/jpeg',
  destination: 'uploads/',
  filename: '616f17e7a4206b72df293fdd918fd064',
  path: 'uploads\\616f17e7a4206b72df293fdd918fd064',
  size: 1420173
}

上传后的文件

我们可以看到文件是二进制格式的,至于如何处理为源文件类型,可以参考下面的资料:

除此之外,还有一些关于multer的资料如下:

使用express响应数据的格式

Express中,res.send()、res.json()、res.end()都是用来向客户端发送HTTP响应的方法,但是它们有一些区别。

  • res.send()可以发送任何类型的数据,包括字符串、对象、数组、布尔值或Buffer。它会根据数据的类型自动设置响应头的Content-Type,比如发送JSON对象时,会设置为application/json。它还会自动结束响应,所以不需要再调用res.end()。
  • res.json()和res.send()类似,但是它只能发送JSON对象或数组。它会将数据转换为JSON字符串,并设置响应头的Content-Type为application/json。它也会自动结束响应,所以不需要再调用res.end()。
  • res.end()只能发送字符串或Buffer类型的数据,它不会设置响应头的Content-Type,也不会转换数据的格式。它只是用来快速结束没有任何数据的响应,或者在使用res.write()多次发送数据后,结束响应。它不能和res.send()或res.json()同时使用,否则会报错。

字符串类型

如果您想发送字符串类型的数据,您可以使用res.send()、res.end()或res.render()方法。

  • res.send()方法会自动设置响应头的Content-Type为text/html,除非您手动更改。它还会自动结束响应,所以不需要再调用res.end()。这个方法适合发送简单的文本内容,比如一些提示信息或者HTML代码。
  • res.end()方法不会设置响应头的Content-Type,也不会转换数据的格式。它只是用来快速结束没有任何数据的响应,或者在使用res.write()多次发送数据后,结束响应。这个方法适合发送一些原始的字符串数据,比如二进制数据的十六进制表示。
  • res.render()方法会使用模板引擎来渲染一个视图,并将生成的HTML字符串发送给客户端。它会自动设置响应头的Content-Type为text/html,除非您手动更改。它也会自动结束响应,所以不需要再调用res.end()。这个方法适合发送一些动态的HTML内容,比如根据用户的请求或者数据库的数据来生成的网页。
  • 前端接收字符串类型的数据时,可以使用response.text()方法来解析数据为字符串,或者使用response.html()方法来解析数据为HTML文档。
// 后端使用res.send()发送一个字符串
app.get('/send', (req, res) => {
  let str = 'Hello world';
  res.send(str); // 自动设置响应头的Content-Type为text/html
});

// 前端使用fetch接收数据,并打印在控制台
fetch('/send')
  .then(response => response.text()) // 解析数据为字符串
  .then(data => console.log(data)); // 打印Hello world

// 后端使用res.end()发送一个字符串
app.get('/end', (req, res) => {
  let str = Buffer.from('Hello world').toString('hex'); // 将字符串转换为十六进制表示
  res.end(str); // 不设置响应头的Content-Type
});

// 前端使用fetch接收数据,并打印在控制台
fetch('/end')
  .then(response => response.text()) // 解析数据为字符串
  .then(data => console.log(data)); // 打印48656c6c6f20776f726c64

// 后端使用res.render()发送一个字符串
app.get('/render', (req, res) => {
  let name = req.query.name || 'Bing'; // 获取请求参数中的name,如果没有就默认为Bing
  res.render('index', {name: name}); // 使用模板引擎渲染index视图,并传入name变量
});

// 前端使用fetch接收数据,并在网页中显示
fetch('/render?name=Bob') // 请求参数中传入name=Bob
  .then(response => response.html()) // 解析数据为HTML文档
  .then(data => document.body.innerHTML = data); // 将网页的内容替换为渲染后的HTML

对象或者数组

如果您想发送对象或数组类型的数据,您可以使用res.send()res.json()方法。

  • res.send()方法会自动将对象或数组转换为JSON字符串,并设置响应头的Content-Type为application/json,除非您手动更改。它还会自动结束响应,所以不需要再调用res.end()。这个方法适合发送一些简单的JSON数据,比如一些配置信息或者状态码。
  • res.json()方法和res.send()类似,但是它只能发送JSON对象或数组。它会将数据转换为JSON字符串,并设置响应头的Content-Type为application/json。它也会自动结束响应,所以不需要再调用res.end()。这个方法适合发送一些复杂的JSON数据,比如一些数据库的查询结果或者API的返回值。
  • 前端接收对象或数组类型的数据时,可以使用response.json()方法来解析数据为JSON对象,或者使用response.arrayBuffer()方法来解析数据为ArrayBuffer对象。
// 后端使用res.send()发送一个对象
app.get('/send', (req, res) => {
  let obj = {name: 'Bing', age: 10};
  res.send(obj); // 自动设置响应头的Content-Type为application/json
});

// 前端使用fetch接收数据,并打印在控制台
fetch('/send')
  .then(response => response.json()) // 解析数据为JSON对象
  .then(data => console.log(data)); // 打印{name: 'Bing', age: 10}

// 后端使用res.json()发送一个数组
app.get('/json', (req, res) => {
  let arr = [1, 2, 3];
  res.json(arr); // 自动设置响应头的Content-Type为application/json
});

// 前端使用fetch接收数据,并打印在控制台
fetch('/json')
  .then(response => response.json()) // 解析数据为JSON对象
  .then(data => console.log(data)); // 打印[1, 2, 3]

布尔类型

如果您想发送布尔值类型的数据,您可以使用res.send()或res.json()方法。

  • res.send()方法会自动将布尔值转换为字符串,并设置响应头的Content-Type为text/html,除非您手动更改。它还会自动结束响应,所以不需要再调用res.end()。这个方法适合发送一些简单的布尔值,比如一些判断结果或者开关状态。
  • res.json()方法会自动将布尔值转换为JSON字符串,并设置响应头的Content-Type为application/json。它也会自动结束响应,所以不需要再调用res.end()。这个方法适合发送一些复杂的布尔值,比如一些逻辑运算的结果或者条件判断的结果。
  • 前端接收布尔值类型的数据时,可以使用response.text()方法来解析数据为字符串,或者使用response.json()方法来解析数据为JSON对象。
// 后端使用res.send()发送一个布尔值
app.get('/send', (req, res) => {
  let bool = true;
  res.send(bool); // 自动设置响应头的Content-Type为text/html
});

// 前端使用fetch接收数据,并打印在控制台
fetch('/send')
  .then(response => response.text()) // 解析数据为字符串
  .then(data => console.log(data)); // 打印true

// 后端使用res.json()发送一个布尔值
app.get('/json', (req, res) => {
  let bool = false;
  res.json(bool); // 自动设置响应头的Content-Type为application/json
});

// 前端使用fetch接收数据,并打印在控制台
fetch('/json')
  .then(response => response.json()) // 解析数据为JSON对象
  .then(data => console.log(data)); // 打印false

buffer类型

如果您想发送Buffer类型的数据,您可以使用res.send()或res.end()方法。

  • res.send()方法会自动将Buffer转换为二进制数据,并设置响应头的Content-Type为application/octet-stream,除非您手动更改。它还会自动结束响应,所以不需要再调用res.end()。这个方法适合发送一些二进制数据,比如一些图片或者音频文件。
  • res.end()方法不会设置响应头的Content-Type,也不会转换数据的格式。它只是用来快速结束没有任何数据的响应,或者在使用res.write()多次发送数据后,结束响应。这个方法适合发送一些原始的Buffer数据,比如一些加密或者压缩的数据。
  • 前端接收Buffer类型的数据时,可以使用response.arrayBuffer()方法来解析数据为ArrayBuffer对象,或者使用response.blob()方法来解析数据为Blob对象。
// 后端使用res.send()发送一个Buffer
app.get('/send', (req, res) => {
  let buf = Buffer.from('Hello world'); // 创建一个Buffer对象
  res.send(buf); // 自动设置响应头的Content-Type为application/octet-stream
});

// 前端使用fetch接收数据,并打印在控制台
fetch('/send')
  .then(response => response.arrayBuffer()) // 解析数据为ArrayBuffer对象
  .then(data => console.log(data)); // 打印ArrayBuffer(11) [72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]

// 后端使用res.end()发送一个Buffer
app.get('/end', (req, res) => {
  let buf = Buffer.from('Hello world'); // 创建一个Buffer对象
  res.end(buf); // 不设置响应头的Content-Type
});

// 前端使用fetch接收数据,并打印在控制台
fetch('/end')
  .then(response => response.arrayBuffer()) // 解析数据为ArrayBuffer对象
  .then(data => console.log(data)); // 打印ArrayBuffer(11) [72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]

使用fetch来解析后端数据

当后端响应数据之后,我们使用fetch来获取数据,fetch提供了很多的方法来处理不同类型的数据,就像express中使用中间件来将request中的数据进行转化。

假设后端使用Express的res.send()方法发送了一个对象,如下:

// 后端代码
app.get('/send', (req, res) => {
  let obj = {name: 'Bing', age: 10};
  res.send(obj); // 自动设置响应头的Content-Type为application/json
});

那么前端使用fetch方法接收数据时,会得到一个Response对象,如下:

// 前端代码
fetch('/send')
  .then(response => {
    console.log(response); // 打印Response对象
    return response.json(); // 解析数据为JSON对象
  })
  .then(data => {
    console.log(data); // 打印{name: 'Bing', age: 10}
  });

Response对象的结构大致如下:

// Response对象的结构
{
  body: ReadableStream, // 响应的主体数据的流
  bodyUsed: false, // 响应的主体数据是否已经被使用
  headers: Headers, // 响应的头部信息的对象
  ok: true, // 响应的状态码是否在200-299之间
  redirected: false, // 响应是否经过重定向
  status: 200, // 响应的状态码
  statusText: "OK", // 响应的状态文本
  type: "basic", // 响应的类型,可能是basic, cors, error, opaque, opaqueredirect等
  url: "http://localhost:3000/send", // 响应的URL
  data: {name: 'Bing', age: 10} // 响应的主体数据,需要使用相应的方法来解析
}

前端获取到数据的方法有以下几种,以下方法的返回值都是Promise对象,并且Promise的值为将response对象的data进行转换后的结果:

  • response.json():将响应的主体数据解析为JSON对象,适用于响应头的Content-Type为application/json的情况。
  • response.text():将响应的主体数据解析为字符串,适用于响应头的Content-Type为text/html或者其他文本类型的情况。
  • response.blob():将响应的主体数据解析为Blob对象,适用于响应头的Content-Type为image/png或者其他二进制类型的情况。
  • response.arrayBuffer():将响应的主体数据解析为ArrayBuffer对象,适用于响应头的Content-Type为application/octet-stream或者其他二进制类型的情况。
  • response.formData():将响应的主体数据解析为FormData对象,适用于响应头的Content-Type为multipart/form-data的情况。