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>
表单格式如下:
上传的数据为:
上述数据是一个多部分表单数据格式的字符串,它由以下几个部分组成:
-
------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的资料如下:
- nodeJs中间件Multer详解_使用express实现本地文件/图片上传到服务器指定目录_multer.array-CSDN博客
- multer/doc/README-zh-cn.md at master · expressjs/multer · GitHub
- multer - npm (npmjs.com)
使用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的情况。