先记住这个答案
表单默认多用 application/x-www-form-urlencoded 编码普通字段;有文件上传时,用 multipart/form-data 保留文件内容并为每个字段划定边界。text/plain 虽也是可选值,但不适合常规接口。enctype 规定原生表单提交的编码,脚本自己构造请求时应按实际请求体设置。
- 普通表单默认使用 URL 编码
- 文件上传常用
multipart/form-data - 浏览器负责生成 multipart 边界
FormData请求别手写不完整类型头
文件为什么不能按普通字段处理
普通 URL 编码适合名称和值这样的文本对,文件则需要文件名、类型与二进制内容。multipart/form-data 通过多个分段承载这些信息,浏览器把每个成功控件写入相应部分,并在请求头中声明用于分隔的 boundary。服务端按相同边界解析。
<form action="/upload" method="post" enctype="multipart/form-data">
<label for="photo">照片</label>
<input id="photo" name="photo" type="file">
<button type="submit">上传</button>
</form>name 是服务端识别文件字段的关键。若控件被禁用或根本未选择文件,具体数据行为要按成功控件规则处理;切换编码不会使禁用字段重新出现。
脚本发送 FormData 时别覆盖边界
用 new FormData(form) 再交给 fetch 作为请求体,浏览器会构建 multipart 数据和对应的 Content-Type 边界。手动写 Content-Type: multipart/form-data 却不带匹配的 boundary,服务端可能无法解析。
如果接口明确接受 JSON,脚本应把需要的字段构造成 JSON 并设置相应类型,不要以为表单上写了 enctype 就能改变脚本请求。原生 GET 提交与 POST 提交的编码行为也不同,上传文件一般使用 POST。
在 Network 面板里看一次上传请求会更容易理解:每个字段有自己的边界和头部,文件部分会带文件名及媒体类型,而不是把二进制硬塞进 URL 编码字符串。若表单用了 method="get",enctype 对请求体没有作用,文件上传应走 post。前端即使限制了文件扩展名,服务器仍需重新检查实际内容、大小和权限。若用 fetch 发送 FormData,让浏览器生成带 boundary 的 Content-Type;只手写 multipart/form-data 会缺少分隔符参数。
用 new FormData(form) 再交给 fetch 发送时,请求体由浏览器组装;如果改用 URLSearchParams,文件对象不会以文件字节的形式上传。抓包时可查看请求头里的 boundary 是否和请求体分隔符对应,排查“后端收不到文件”尤其有用。大文件上传还会遇到进度、取消与失败重试等问题,那是网络请求层面的设计,不能只靠 enctype 这个属性解决。
容易答错的地方
- 把
multipart/form-data当成安全校验 - 它只定义传输格式,不校验文件内容、大小或扩展名;服务端仍需检查。
- 手写不含 boundary 的 multipart 请求头
- 浏览器构造
FormData请求时应让它同时生成请求体和边界参数。
面试官还会怎么问?
普通文本表单默认是什么编码?
通常是 application/x-www-form-urlencoded,字段名和值按表单编码规则进入请求。
text/plain 适合生产上传吗?
一般不适合,它更多用于简单调试,缺少标准文件分段结构。
fetch 发送 FormData 要设置 Content-Type 吗?
通常不要手动设置,让浏览器按生成的分段边界配套填写。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。