POST 请求用法详解
以 Flutter Dio 为例,系统讲解 POST 请求中 body、query、headers、path、form 和 multipart 的使用方式。
在 Flutter 项目中使用 Dio 发起 POST 请求时,数据并不只有一种携带方式。常见位置包括:请求体 data、URL 查询参数 queryParameters、请求头 headers、路径参数 path、表单 body,以及用于文件上传的 multipart/form-data。
本文以 Dio 为例,说明这些 POST 请求的写法、客户端实际发出去的 HTTP 大概长什么样,以及服务端一般会从哪里取到这些数据。
基础结构
Dio 的 POST 请求通常长这样:
final dio = Dio(BaseOptions(
baseUrl: 'https://example.com',
));
final response = await dio.post(
'/api/users',
data: {},
queryParameters: {},
options: Options(
headers: {},
),
);
几个关键参数的含义是:
| Dio 参数 | HTTP 位置 | 服务端常见读取方式 |
|---|---|---|
data | 请求体 body | req.body |
queryParameters | URL 后面的 ?a=1&b=2 | req.query |
options.headers | 请求头 headers | req.headers |
| path 字符串 | URL 路径 | req.params |
FormData | multipart body | req.body / req.file |
简单记忆:
data -> body
queryParameters -> URL query
Options.headers -> headers
FormData -> multipart 文件上传
路径字符串插值 -> path params
1. JSON Body
现代 API 最常见的 POST 方式,是把业务数据放在 JSON body 里。
final response = await dio.post(
'/api/users',
data: {
'name': 'Alice',
'age': 18,
'email': 'alice@example.com',
},
options: Options(
contentType: Headers.jsonContentType,
),
);
客户端实际发出去大概是:
POST /api/users HTTP/1.1
Host: example.com
Content-Type: application/json
{
"name": "Alice",
"age": 18,
"email": "alice@example.com"
}
服务端通常收到:
req.body
// {
// name: "Alice",
// age: 18,
// email: "alice@example.com"
// }
适合放在 JSON body 的内容包括:
- 登录、注册信息
- 创建订单参数
- 提交表单数据
- 嵌套对象
- 数组列表
- 复杂筛选条件
例如创建订单:
await dio.post(
'/api/orders',
data: {
'userId': 123,
'items': [
{'productId': 1, 'quantity': 2},
{'productId': 2, 'quantity': 1},
],
'address': {
'city': 'Shanghai',
'street': 'Road 1',
},
},
);
对应 HTTP body:
{
"userId": 123,
"items": [
{ "productId": 1, "quantity": 2 },
{ "productId": 2, "quantity": 1 }
],
"address": {
"city": "Shanghai",
"street": "Road 1"
}
}
2. URL Query 参数
POST 请求也可以在 URL 后面携带 query 参数。Dio 中使用 queryParameters。
await dio.post(
'/api/search',
queryParameters: {
'page': 1,
'pageSize': 20,
'sort': 'createdAt',
},
data: {
'keyword': 'phone',
'brand': 'Apple',
},
);
客户端实际请求大概是:
POST /api/search?page=1&pageSize=20&sort=createdAt HTTP/1.1
Host: example.com
Content-Type: application/json
{
"keyword": "phone",
"brand": "Apple"
}
服务端通常会分开收到:
req.query
// {
// page: "1",
// pageSize: "20",
// sort: "createdAt"
// }
req.body
// {
// keyword: "phone",
// brand: "Apple"
// }
注意,query 参数在很多服务端框架里会先被解析成字符串。例如 page=1 通常先是字符串 "1",不是数字 1。
query 参数适合放:
- 分页参数:
page、pageSize - 排序参数:
sort、order - 简单筛选开关
- 来源标记:
source=app - 调试或行为控制参数
不适合放:
- 密码
- token
- 手机号、身份证等敏感信息
- 大段文本
- 复杂 JSON
原因是 query 会出现在 URL 中,可能被浏览器历史、网关日志、服务端日志记录下来,而且 URL 长度也有限制。
3. Form Urlencoded Body
有些老接口、OAuth 接口或传统表单接口要求 application/x-www-form-urlencoded。
Dio 写法:
await dio.post(
'/login',
data: {
'username': 'alice',
'password': '123456',
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
);
客户端实际发出去:
POST /login HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
username=alice&password=123456
服务端收到:
req.body
// {
// username: "alice",
// password: "123456"
// }
它看起来很像 URL query,但位置不同:
POST /login?username=alice&password=123456
这是 query 参数。
POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username=alice&password=123456
这是 body 参数。
关键区别在 Dio 的 contentType:
Options(
contentType: Headers.formUrlEncodedContentType,
)
常见使用场景:
- OAuth token 接口
- 传统 Java、PHP 后端表单接口
- 兼容 HTML form 风格的接口
4. Multipart FormData 上传文件
上传文件时通常使用 FormData 和 MultipartFile。
final formData = FormData.fromMap({
'username': 'alice',
'avatar': await MultipartFile.fromFile(
file.path,
filename: 'avatar.png',
),
});
final response = await dio.post(
'/api/upload',
data: formData,
);
客户端实际发出去大概是:
POST /api/upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----dio-boundary
------dio-boundary
Content-Disposition: form-data; name="username"
alice
------dio-boundary
Content-Disposition: form-data; name="avatar"; filename="avatar.png"
Content-Type: image/png
<文件二进制内容>
------dio-boundary--
服务端通常收到:
req.body
// {
// username: "alice"
// }
req.file
// {
// fieldname: "avatar",
// originalname: "avatar.png",
// mimetype: "image/png",
// size: 12345
// }
多个文件可以这样写:
final formData = FormData();
formData.fields.add(const MapEntry('title', 'photos'));
formData.files.add(MapEntry(
'files',
await MultipartFile.fromFile('/path/a.png', filename: 'a.png'),
));
formData.files.add(MapEntry(
'files',
await MultipartFile.fromFile('/path/b.png', filename: 'b.png'),
));
await dio.post('/api/photos', data: formData);
使用 FormData 时,Dio 会自动处理 Content-Type 和 boundary。一般不要手动写死 multipart 的 boundary。
5. Headers 请求头
Header 通常用于携带请求元信息,而不是业务主体数据。
await dio.post(
'/api/orders',
data: {
'productId': 1001,
'quantity': 2,
},
options: Options(
headers: {
'Authorization': 'Bearer your_token',
'X-Request-Id': 'req-001',
'X-Client-Version': '1.0.0',
},
),
);
客户端实际请求:
POST /api/orders HTTP/1.1
Host: example.com
Authorization: Bearer your_token
X-Request-Id: req-001
X-Client-Version: 1.0.0
Content-Type: application/json
{
"productId": 1001,
"quantity": 2
}
服务端读取:
req.headers.authorization
// "Bearer your_token"
req.headers['x-request-id']
// "req-001"
req.body
// {
// productId: 1001,
// quantity: 2
// }
如果 token 每个请求都要带,不建议每次都手写 Options,可以放在 Dio 初始化中:
final dio = Dio(BaseOptions(
baseUrl: 'https://example.com',
headers: {
'Authorization': 'Bearer your_token',
},
));
更常见的是通过拦截器动态添加:
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
options.headers['Authorization'] = 'Bearer your_token';
handler.next(options);
},
),
);
header 适合放:
AuthorizationContent-TypeAccept- 客户端版本
- 语言
- 设备信息
- 请求 ID
- 签名
不适合放订单详情、用户资料、商品列表等业务主体数据。
6. Path 参数
Path 参数是 URL 路径本身的一部分,通常用于表示资源 ID。
final userId = 123;
await dio.post(
'/api/users/$userId/orders',
data: {
'productId': 1001,
'quantity': 2,
},
);
客户端实际请求:
POST /api/users/123/orders HTTP/1.1
Host: example.com
Content-Type: application/json
{
"productId": 1001,
"quantity": 2
}
服务端通常收到:
req.params
// {
// userId: "123"
// }
req.body
// {
// productId: 1001,
// quantity: 2
// }
常见例子:
await dio.post('/api/videos/$videoId/like');
await dio.post(
'/api/users/$userId/avatar',
data: formData,
);
await dio.post(
'/api/orders/$orderId/cancel',
data: {
'reason': 'user_cancel',
},
);
path 适合表示明确资源:
- 用户 ID
- 订单 ID
- 视频 ID
- 某个资源下的子操作
不适合放复杂筛选条件、长文本、敏感信息。
7. Cookie
Flutter App 不像浏览器那样天然维护 Cookie。你可以手动放在 header 里,也可以接入 cookie 管理库。
手动携带 Cookie:
await dio.post(
'/api/profile',
data: {
'nickname': 'Alice',
},
options: Options(
headers: {
'Cookie': 'sessionId=abc123; theme=dark',
},
),
);
客户端实际请求:
POST /api/profile HTTP/1.1
Host: example.com
Cookie: sessionId=abc123; theme=dark
Content-Type: application/json
{
"nickname": "Alice"
}
服务端读取:
req.cookies
// {
// sessionId: "abc123",
// theme: "dark"
// }
req.body
// {
// nickname: "Alice"
// }
在移动 App 中,更常见的做法是使用:
Authorization: Bearer token
Cookie 更常见于浏览器 Web 应用。
8. 混合使用
实际项目中经常会同时使用 path、query、body 和 header。
final userId = 123;
final response = await dio.post(
'/api/users/$userId/orders',
queryParameters: {
'source': 'app',
'debug': false,
},
data: {
'productId': 1001,
'quantity': 2,
'couponCode': 'NEW_USER',
},
options: Options(
headers: {
'Authorization': 'Bearer token',
'X-Request-Id': 'req-001',
},
),
);
客户端实际请求:
POST /api/users/123/orders?source=app&debug=false HTTP/1.1
Host: example.com
Authorization: Bearer token
X-Request-Id: req-001
Content-Type: application/json
{
"productId": 1001,
"quantity": 2,
"couponCode": "NEW_USER"
}
服务端对应读取:
req.params
// { userId: "123" }
req.query
// { source: "app", debug: "false" }
req.headers.authorization
// "Bearer token"
req.body
// {
// productId: 1001,
// quantity: 2,
// couponCode: "NEW_USER"
// }
9. 实际开发中的选择规则
推荐按下面的规则选择:
| 场景 | 推荐方式 | Dio 写法 |
|---|---|---|
| 主要业务数据 | JSON body | data: {...} |
| 分页、排序、简单开关 | URL query | queryParameters: {...} |
| 资源 ID | path | '/users/$userId' |
| token、客户端版本、请求 ID | header | Options(headers: {...}) |
| 文件上传 | multipart | FormData + MultipartFile |
| 传统表单接口 | urlencoded body | contentType: Headers.formUrlEncodedContentType |
| 浏览器式会话 | Cookie | headers: {'Cookie': ...} |
10. 常见误区
POST 不代表数据只能放 body
POST 请求可以同时有 query、body 和 header。例如:
await dio.post(
'/api/search',
queryParameters: {'page': 1},
data: {'keyword': 'Flutter'},
);
实际是:
POST /api/search?page=1 HTTP/1.1
{"keyword":"Flutter"}
query 和 form body 长得像,但位置不同
query 在 URL 上:
/api/login?username=alice&password=123456
form body 在请求体中:
username=alice&password=123456
Dio 中分别对应:
queryParameters: {'username': 'alice'}
和:
data: {'username': 'alice'},
options: Options(contentType: Headers.formUrlEncodedContentType),
敏感信息不要放 URL
即使是 HTTPS,URL path 和 query 也更容易出现在日志、埋点、代理记录和错误报告里。密码、token、身份证号等敏感信息不要放 query 或 path。
FormData 不等于普通 JSON
FormData 是 multipart 格式,适合文件上传。普通业务对象优先用 JSON body,不需要为了传对象就用 FormData。
总结
Dio 的 POST 请求可以这样理解:
await dio.post(
'/api/users/$userId/orders', // path 参数
queryParameters: {}, // URL query
data: {}, // body
options: Options(
headers: {}, // header
),
);
最常见的组合是:
await dio.post(
'/api/orders',
data: {
'productId': 1001,
'quantity': 2,
},
options: Options(
headers: {
'Authorization': 'Bearer token',
},
),
);
判断数据该放哪里时,优先问自己三个问题:
- 这是业务主体数据吗?是的话放
data。 - 这是分页、排序、筛选开关吗?通常放
queryParameters。 - 这是认证、设备、版本、追踪 ID 吗?通常放
headers。
如果涉及文件上传,再使用 FormData 和 MultipartFile。