惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园_首页
Security Archives - TechRepublic
Security Archives - TechRepublic
Application and Cybersecurity Blog
Application and Cybersecurity Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
C
CERT Recently Published Vulnerability Notes
S
Security @ Cisco Blogs
S
Security Affairs
D
Darknet – Hacking Tools, Hacker News & Cyber Security
L
Lohrmann on Cybersecurity
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Hacker News: Ask HN
Hacker News: Ask HN
Forbes - Security
Forbes - Security
H
Heimdal Security Blog
A
Arctic Wolf
NISL@THU
NISL@THU
P
Proofpoint News Feed
W
WeLiveSecurity
S
Schneier on Security
AI
AI
Schneier on Security
Schneier on Security
N
News and Events Feed by Topic
L
LINUX DO - 最新话题
Cisco Talos Blog
Cisco Talos Blog
AWS News Blog
AWS News Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
Know Your Adversary
Know Your Adversary
Scott Helme
Scott Helme
V
Vulnerabilities – Threatpost
Cyberwarzone
Cyberwarzone
I
Intezer
S
Securelist
Help Net Security
Help Net Security
Microsoft Security Blog
Microsoft Security Blog
量子位
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
小众软件
小众软件
Last Week in AI
Last Week in AI
Jina AI
Jina AI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
WordPress大学
WordPress大学
罗磊的独立博客
月光博客
月光博客
雷峰网
雷峰网
A
About on SuperTechFans
The GitHub Blog
The GitHub Blog
T
The Blog of Author Tim Ferriss
MongoDB | Blog
MongoDB | Blog
大猫的无限游戏
大猫的无限游戏
博客园 - 司徒正美
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角
漫谈前端进化史之从 Form 表单到文件上传 - 元视角
飞鸿踏雪 · 2018-09-05 · via 元视角

Hi,大家好,欢迎大家关注我的博客,我是 Payne,我的博客地址是https://qinyuanpei.github.io。今天这篇博客,我们来说说文件上传相关的内容。看到这里,大家一定觉得博主在技术上越来越没追求了吧,文件上传这种再简单不过的东西,真的值得博主你专门写篇博客吗?在介绍声明式 RESTful 客户端 WebApiClient 的这篇文章中,博主曾经提到,HTTP 协议中对文件上传的支持,主要是通过 multipart/form-data 来实现。因为这种方式是将文件视为一种特殊的键值对,所以对这种方式我本人不太喜欢。可作为标准的意义就是要忽略个人的情感因素,所以,在今天这篇文章中,博主更多的是想从 HTTP 协议(RFC2388)的角度来看待这个问题,即为什么它选择了 multipart/form-data 来实现上传,以及伴随着前端技术的发展它经历了哪些变化。

从 Form 表单说起

圣经上开篇就点明主旨,“起初神创造天地。地是空虚混沌。渊面黑暗”。一切的一切,都要从神创造天地开始,神说,要有光,这世上便有了光。那么,对于 HTTP 协议我们要从哪里开始说起呢。HTTP 的全称是超文本传输协议,所以,它设计的初衷是传输超文本类型的数据。什么是超文本类型的数据呢?从现代网页的组成,我们就可以知道,它不单单是指文本类信息,同时指图片、音频、视频等等一切可能的信息形式。可神奇的地方就在于,HTTP 协议是基于文本的协议,这意味着我们在网页中的信息交换,是借助某种文本类型的通信协议。顺着这个思路,最早我们在网页中交换信息的方式是什么呢?我认为是 Form 表单。想想看,我们在 Form 表单中输入信息,然后通过一个按钮将数据提交到服务器,服务器会对我们的请求做出响应。事实上,直到今天,我们的前端依然在采用这一机制。所不同的是,我们今天用各种组件替代了 Form 表单。

如果我们讲各种语言的"打印"理解为 Hello World,那么对前端而言最浅显的 Hello World 是什么呢?我个人以为是登录,想象一下,这是任何一个 Web 应用里都有的功能,我们输入用户名和密码以后,点击“登录”按钮就可以登录到系统。虽然,此时此刻的你我,都知道这是一个简单的 POST 请求,甚至对于用户名和密码这两个字段,我们有多种方法可以将其传递到服务器上。那么,各位是否知道,我们通过 Form 表单来登录时,这个过程中到底发生了什么呢?既然提到了登录,那么我们这里通过下面的例子来分析。

如你所见,这是一个相当“简陋”的 Web 页面。对一名后端开发人员而言,精致的 Web 页面就是一段被套在华丽外壳里的代码(不知道这样会不会被前端网红们打死)。所以,排除了样式相关的 CSS,可以让我们更加专注于核心原理。同样地,我们编写了一个简单的 Web API,来处理前端发送来的 HTTP 请求,这不是本文的重点,我们假设它存在且可以工作就好。

HTML结构/界面 HTML结构/界面

这里已经说过,比起炫酷的 Web 页面和后端接口,我们这里更关心的是,登录时到底发生了什么。所以,大家都猜对了,通过 Chrome 自带的开发人员工具,我们可以捕捉到点击“登录”按钮时发出的 HTTP 请求,我们一起来看看它的报文内容是什么吧,相信大家都会有一种恍然大悟的感觉,让我们拭目以待吧! encrypt为x-www-form-urlencode时的请求报文 encrypt为x-www-form-urlencode时的请求报文

通过这个报文内容,我们可以发现,“登录”实际上是一个 POST 请求,这是因为我们在 HTML 结构中声明了,Form 表单用什么样的方式去提交数据。而实际上呢,Form 表单默认的行为是 GET。我们同样会注意到报文中的 Content-Type 为 application/x-www-form-urlencode,它的特点是采用类似 key1=value1&key2=value2……的形式来提交数据,并且每一个 value 都会被编码。这样,我们就不得不提到 Form 表单的 encrypt 属性,它有三种基本取值:text/plain、application/x-www-form-urlencode 和 multipart/form-data。其中,text/plain 这种不必再说,因为它传递的是纯文本数据。而对于 multipart/form-data 来说,它的特点是采用一系列的 boundary 来分割不同的值,如果我们将示例中 Form 表单的 encrypt 属性设为 multipart/form-data,就会得到下面的报文内容,可以注意到,它和我们预期是一致的。 encrypt为multipart/form-data时的请求报文 encrypt为multipart/form-data时的请求报文

或许大家会说,现在我们用 AJAX 来请求 RESTful 风格的 API 时,不都是用 JSON 作为数据交换的格式吗?对于这一点,或许我们可以理解为,Form 表单是封装了有限的 3 种 Content-Type 的 XHR 对象,所以,Form 表单足以让我们一窥 AJAX 最初的样子。虽然,我们今天已经不再主张使用 jQuery,但是熟悉 jQuery 的朋友一定知道这一点,即 jQuery 中默认的 Content-Type 示例上是 application/x-www-form-urlencoded。所以,即使我们今天有了全新的 Fetch API,可它依然脱离不了 HTTP 协议的范畴。可或许正因为如此,HTTP 中的文件上传多少像是某种妥协的产物。

神奇的 Input 控件

OK,在本文的第一节,我们使用的是最简单的 Input 控件,即它的 type 属性为“text”。事实上,Input 控件是一个神奇的控件,因为不同的 type 会有不同的作用。例如,type 为 password 对应密码域;type 为 checkbox 对应复选;type 为 radio 对应单选域;type 为 button 对应按钮域等等……有很多朋友可能会问,你说的这个和这篇文章有什么关系吗?我想说的是,当然有关系而且关系密切,因为我们下面要提到的这种 Input 控件,和本文想要说明的 HTTP 上传,在本质上有着千丝万缕的联系。具体是什么样的联系呢?我们来一起看下面的这个例子。

HTTP_Upload_06 HTTP_Upload_06

HTTP_Upload_05 HTTP_Upload_05

通过这个例子,我们很容易发现的一点是,当我们采用 type 为 file 的 Input 控件上传一个文件时,它会采用 multipart/form-data 来传递数据,报文中使用了和第二个示例类似的结构,即第一部分负责描述文件信息,譬如文件的名称、扩展名类型等等;第二部分表示文件数据流,可以理解为二进制形式的内容。既然它采用 multipart/form-data 来传递数据,那么这是否意味着,我们可以在这个结构中携带更多的信息呢?譬如,有时候我们需要将文件和用户提交的信息关联起来,这个时候就需要将这些信息一切提交到服务器端,如果我们将其拆分为两个 API 来实现,那么就需要去花精力维护这个关联的 id 啦。答案自然是可以的,只要把文件视为一种特殊的键值对即可。

HTTP 与文件上传

好了,说了这多么内容,是时候来说说 HTTP 与文件上传啦!现在大家都知道了,HTTP 上传实际上是在 multipart/form-data 基础上扩展而来的。早期人们在制定 HTTP 协议的时候,并没有想到用它来作为文件上传的协议,因为事实上 TPC/IP 或者 FTP 都可以提供更好的上传支持。当我们回顾 Form 表单中关于 HTTP 的部分,我们就会发现,HTTP 中具备上传文件可能性的方式只有两种,即 multipart/form-data 和 x-www-form-urlencode。这里为什么不考虑 text/plain 呢?尽管从理论上来讲,它可以作为文件上传的一种方式,此时,它相当于把整个文件的内容全部放在请求体(body)中。从实用性角度来讲,text/pain 在实际应用中并不多见,因为采用纯文本意味着客户端与服务端必须按照某种规则去解析报文。而从功能性角度来讲,把整个文件的内容全部放在请求体中,则会造成文件信息的不完整,因为此时文件名等信息是没有办法传输到服务器端的,所以,这样综合下来再看的话,HTTP 协议本身留给我们的选择的空间并不大,我们能够选择的就只有 multipart/form-data 和 x-www-form-urlencode 这两种啦,下面着重来分析下这种数据加密方式。

HTTP_Upload_07 HTTP_Upload_07

对于 Content-Type 为 multipart/form-data 而言,首先,它会在请求头部的 Content-Type 字段中,声明当前的内容类型为 multipart/form-data,并指定一个名为 boundary 的随机字符串,它的含义是说,从现在开始,请求中的每一个“字段”都会用这个名为 boundary 的随机字符串进行分割。而对于每一个“字段”而言,它可以拥有部分子头部字段,一个最为常见的头部字段是 Content-Disposition,其取值为 form-data。除此之外,每一个“字段”可以在**Content-Disposition: form-data;**后追加若干个字段,譬如 name、filename 以及用以指定文件类型的 Content-Type(假如这个“字段”是一个文件的话)。HTTP 协议中还规定这里可以支持扩展字段。我们通过 type 为 file 的 Input 控件进行上传时,默认的 name 为 multipartfile,当服务器端接受到类似的字段时,就会根据报文对文件进行拼接,所以,对于 HTTP 上传来说,它可以支持多个文件并发上传,但并不直接支持断点续传。注意这里我说的是,不直接支持断点续传,实际上它可以通过请求头部中的 Range 字段来实现,当然这已经超出了这篇文章的范畴。

HTTP_Upload_08 HTTP_Upload_08

对于 Content-Type 为 x-www-form-urlencode 而言,它会将请求中的每一个字段以 key1=value1&key2=value2……的形式串联起来,并对每一个 value 进行编程,这种传值方式我们一般称为 QueryString,而更为一般的场景是,我们在通过 GET 方式请求数据的时候,QueryString 是唯一的传参方式,不同之处是 GET 请求的参数是附加在 URL 上,而 POST 请求的参数是附加在 body 里。如果我们用这种方式来上传文件会怎么样呢?答案是,当我们试图将一个文件以 x-www-form-urlencode 方式进行传输时,文件流会被彻底忽略,它实际传输的是对应文件的名称。所以,从这个角度来讲,它不能用于文件的上传。事实上,它是被设计用来传输非二进制数据的,那么可能有人要问啦,那我如果有 JSON 来传输文件可不可以呢?理论上应该没有问题,曾经我们在一个项目中用 JSON 描述图片,当然这是经过 Base64 编码以后的图片。回过头来看 text/plain,我们把 JSON 字符串直接放到 body 里可不可以呢?当然没有问题,因为问题全部转移到服务器端。所以,官方建议用它来作为调试的一种选择。

现在,我们可以来总结下 Form 表单和 HTTP 协议间的关系啦!首先,Form 表单可以提交非二进制数据和二进制数据。非二进制数据,比如一般表单中提交的各种文本信息,用户名、密码这一类等等。二进制数据,主要指各种不同类型的文件等等。对于非二进制数据,可以通过 x-www-form-urlencode 或者 multipart/form-data 两种编码方式来提交。对于二进制数据,只能通过 multipart/form-data 这种方式来提交。所以,当我们需要混合提交二进制数据和非二进制数据的时候,我们就只有 multipart/form-data 这一种选择啦!更一般的结论是,只要我们的 Form 表单里有一个 type 为 file 的 Input 控件,对应 POST 请求的 Content-Type 就会变为 multipart/form-data。我不喜欢这种方式的原因之一,就是构造它的 HTTP 报文非常难受。如果用 HttpClient,痛苦会降低很多;而如果用 HttpWebRequest,我会感到绝望。当然,你此时已明白了这个原理,相信 Postman 可以帮到你的忙。

Form 表单消失以后

熟悉前端演变历程的朋友,应该对我下面要说的历史表示怀念。在很久很久以前,我们的网页三剑客分别是 Dreamwave、Fireworks 和 Flash。那个时候我们用 Dreamwave 制作的网页充斥着大量的 Form 表单,通过 JS 实现对数据的校验,就像这篇文章里描述的一样,我们做几个 type 为 submit 的按钮,就可以把数据提交到服务器端。按理说,这样子很没完美啊,我们可以提交用户输入的信息,可以上传用户选择的文件,何乐而不为呢?为什么大家要用 Div + CSS 淘汰 Form 表单呢?我认为主要有两点,传统的基于表格的布局无法满足现代 Web 程序的布局要求,RESTful 风格 Web API 的出现让开发者希望前后端交互可控。换言之,开发者希望通过 FormData 这样的对象,精细地控制整个请求的细节,而不是交给一个由浏览器发出的 POST 请求。所以,我们看到了前端文件上传的新思路。

首先,最常见的方式,是通过监听 Input 控件的 onchange 方法,通过 files 属性即可获得当前用户选择的文件。我们知道,在大多数情况下,前端是无法和本地文件系统进行交互的。因此通过这种方式获得文件路径,实际上是一个指向本地数据的 blob,前端将文件相关的 type 和 size 组织到一个 FormData 对象的实际中,即可完成对文件的上传。其次,可以利用 HTML5 中的“拖拽”和“粘贴”,其核心依然是监听相关的事件,然后从中获取 File 对象或者 blob 对象的实例,一旦获得了这些实例,就可以将其添加到 FormData 中。到了这一步,接下来的就和第一种方法完全一样啦!最后,是类似百度出品的 WebUploader 这类 HTML5 和 Flash 混合的插件,主打兼容性,不过随着大家对 IE8 以下版本兼容问题的逐步放弃,这类产品的使用场景会越来越少,我们大概知道就可以啦!归根到底一句话,Form 表单和 FormData 对象,其实是可以相互转化的,Form 表单里每一个 Input 控件的 name,其实就是 FormData 里的 key 啦,到了这一步,我想 HTTP 上传就没有什么好神秘了的吧!

本文小结

本文从 Form 表单说起,首先探讨了 Form 表单和 HTTP 之间的关系,即 Form 表单在提交数据的时候,背后的本质其实是一条 HTTP 请求,相对应地,Form 表单默认的请求方式是 GET,在第一个示例中,我们分别展示了使用 x-www-form-urlencode 和 multipart/form-data 时请求报文实际的内容。接下来,我们提到了 HTML 中的 Input 控件,它可以通过指定不同的 type 达到不同的效果,作为第一个示例的延伸,我们尝试通过 Form 表单上传文件并重点关注其报文的结构。接下来,我们从协议的角度分析了为什么要选用 multipart/form-data 来上传文件以及它的原理是什么。最后,我们从前端常见的文件上传方式入手,简要分析了 Form 表单和 FormData 对象间的内在联系,即 Form 表单和 FormData 对象,其实是可以相互转化的,Form 表单里每一个 Input 控件的 name,其实就是 FormData 里的 key。好啦,又是一个难以入眠的夜晚,这篇博客先写到这里,大家晚安!