前端图片上传的三种方式:base64、File与FormData原理详解
前端做图片上传绕来绕去就这三种路子直接传base64字符串、把base64转成File再传、用form表单原样传文件。再加上element-ui这类组件库给你封装好的上传组件很多人就彻底绕晕了。前几天还有同事问我到底哪种方式是对的为什么有的后端只收base64有的后端只认file字段还有的非要你用form表单。这个问题确实没法一句话回答因为上面每一种方案都真实存在于生产环境中它们只是在不同接口、不同场景下各自合适而已。这篇文章我把这几种方式从头到尾讲透包括base64和File/Blob的本质区别、转换原理、element-ui整个上传链路的改造方法以及我实际踩过的坑。适合刚接触前端上传的新人也适合后端转前端或者一直用组件库但没搞懂底层原理的同学。1. 三种上传姿势到底差在哪先把原理摊开1.1 一张图片在浏览器里到底以什么形态存在先说结论你在网页上选中的一张图片在浏览器内存里并不是你眼睛看到的那个图片而是一个二进制数据块。根据你怎么处理它它可以变成三种不同的形态。第一种是File对象。当你用input typefile选择了文件拿到的event.target.files[0]就是一个File对象。File对象继承自Blob它自带name、size、type这些属性比如type可能是image/jpeg、image/png。这个对象本质上是二进制数据的封装。第二种是base64字符串。所谓base64是一种用64个可打印字符来表示二进制数据的编码方式。浏览器里通过FileReader.readAsDataURL()可以把File对象转换成一个以data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAASABIAAD...开头的大字符串。注意看热搜词里那一长串data:image/jpg;base64,/9j/...就是一张jpeg图片被base64编码之后的完整样子。这串字符可以直接放在img标签的src里显示也可以通过JSON传给后端。第三种是FormData。new FormData()创建出来的对象模拟的是html里form表单提交的数据格式。你可以用append方法把File对象塞进去浏览器会在发送请求时自动把它编码成multipart/form-data格式。搞清楚这三种形态后面所有问题就顺了。1.2 三种方式对应三次不一样的请求这三种形态提交到后端HTTP请求是完全不一样的。上传方式Content-Type后端拿到的东西适用场景base64字符串放JSONapplication/json一个普通字符串字段需要自己解码后端接口设计成接收图片字符串base64转File后放FormDatamultipart/form-data一个文件字段和普通上传一样后端只认文件但前端拿到的是base64原始File直接放FormDatamultipart/form-data一个文件字段常规文件上传最推荐这里容易踩的第一个坑就在Content-Type上。如果你用axios直接提交一个JSON对象里面有个字段是base64字符串那请求头就是application/json。后端如果用RequestParam(file) MultipartFile file来接收它会发现请求体里根本没有file这个文件字段直接报400。反过来也一样你把File塞进FormData却手动设置了Content-Type: application/json那浏览器就不会帮你生成multipart边界后端同样收不到文件。1.3 选型不是哪个最好而是哪个最合适我个人的选型逻辑很简单按优先级排后端接口如果明确要base64比如某些AI识别接口、证件照接口那就是纯base64方式。后端接口只支持multipart/form-data文件上传但你的图片经过了canvas裁剪、压缩、或者从某个编辑器插件里拿到的是base64那就转File再传。没有特殊要求直接拿input typefile选中的File对象放FormData传这是最常规、性能最好、后端处理最方便的方式。element-ui的el-upload组件并没有改变这三种方式它只是在组件层面帮你封装了文件选择、进度、成功失败回调这些交互逻辑底层传输方式你依然可以自己控制。所以接下来我把每一层的细节都拆开讲。2. base64直接上传最省事但也最容易踩坑的地方2.1 base64字符串的结构和编码原理base64编码的核心规则是把每3个字节24位拆成4组每组6位再对应到64个可打印字符。所以base64编码后的体积大约是原始数据的4/3倍也就是膨胀约33%。一张5MB的图片转成base64字符串大约有6.7MB如果再算上data:image/jpeg;base64,这截前缀只会更长。很多后端会把base64直接存进数据库字段类型如果还是VARCHAR(255)那存一张稍微清晰点的图片就超了。这是后端设计问题但前端在动手前最好先问问后端打算把这个base64存成什么类型如果是文本字段那意味着每次请求都要传一个超大字符串数据库也会被撑爆。data:image/jpeg;base64,这截前缀不是可有可无的装饰。data:表示data URI协议image/jpeg表示MIME类型base64表示编码方式。浏览器看到这个完整的字符串才能正确把后面的编码还原成图片。后端如果拿到的base64字符串末尾没有等号补位、中间混了换行符解码也会出错。2.2 前端拿到base64的完整链路原因是FileReader的多种read方法分工不同readAsDataURL输出data URI字符串readAsArrayBuffer输出二进制缓冲readAsText按文本读取用于图片场景通常会选readAsDataURL。readAsDataURL读出来的结果直接就能用。但请注意这个方法是异步的结果要在onload回调里拿别想在reader.result上同步读到值。2.3 base64上传前通常要做的压缩处理直接拿原始大图转base64上传在PC端可能还能忍移动端就非常痛苦。现在的手机拍一张照片动不动5-10MB转成base64后请求体积直接翻倍弱网环境下很容易超时后端接包也吃力。所以我基本都会在转base64之前先对图片做一次canvas压缩。核心思路是把图片画到canvas上再用canvas.toDataURL(image/jpeg, quality)输出压缩后的base64。这里有个细节很多人会忽略canvas在浏览器里的最大尺寸是有限制的。iOS Safari上canvas宽高超过4096就可能白屏或画不出来所以压缩前先判断一下图片尺寸超过阈值就先把图片等比缩小到安全范围内再画。压缩质量参数quality一般取0.6到0.8头像类图片0.5就差不多了具体看你们对清晰度的要求。2.4 直接上传base64的风险清单这部分是纯经验总结都是我在实际项目中踩过或者帮别人排查过的体积膨胀base64比原始二进制大约大33%这是编码规则决定的没法优化只能靠压缩前置来缓解。请求体超长很多网关、Nginx默认对请求体大小有限制传一个7MB的base64字符串可能直接被返回413 Request Entity Too Large。日志刷屏如果后端把请求参数打日志一整个base64字符串全打进日志文件排查问题的时候日志文件会被撑大好几倍而且根本没法看。内存占用读取大图到base64浏览器内存会同时存在原始Blob、ArrayBuffer和字符串多份数据移动端低端机容易卡顿甚至闪退。JSON解析慢一个6MB的JSON字符串序列化和反序列化都不是零成本接口响应时间会被拖慢。所以我的结论是除非后端接口设计上就必须收base64否则不要主动选择直接传base64。如果实在绕不开那一定要在客户端做压缩并且和后端确认好请求体大小限制。3. base64转File为什么转怎么转才不踩雷3.1 什么情况下你会需要把base64转成File最典型的场景是你用某个图片裁剪组件或富文本编辑器它给你返回的是base64字符串但你的后端上传接口只认MultipartFile或者multipart/form-data格式。这时候你不能跟后端吵只能老老实实把base64转成File对象再放FormData里。还有一种情况是canvas.toDataURL()输出的是base64但你们项目一直用的是统一的FormData上传接口为了不破坏现有上传链路需要先把这个base64变成File。3.2 标准转换代码原因是atob是浏览器内置的base64解码函数但不能直接处理二进制字符串。atob返回的是一个字符串其中每个字符的charCodeAt就是原始的字节值必须用一个Uint8Array来逐字节保存再传给Blob或File。如果不走Uint8Array直接把atob结果丢给Blob编码可能会被当成UTF-8处理导致图片数据损坏上传后图片打不开。3.3 File和Blob的关系以及一个兼容性注意点File继承自Blob所以能传Blob的地方基本都能传FileFile只是多了name和lastModified两个属性。new File()这个构造函数在较新的浏览器里都支持但如果你要兼容很老的环境可以退一步用Blob构造然后手动给Blob对象补一个name属性因为实际上FormData只关心这个对象能不能被当作文件序列化。3.4 转完之后怎么上传转成File之后恢复成一个常规文件上传就行。这里有个高频坑使用axios上传文件时Content-Type必须交给浏览器自己生成因为multipart/form-data的boundary是随机生成的你手动指定的Content-Type里不包含这个boundary后端解析一定会失败。正确做法是const formData new FormData(); formData.append(file, fileObject); formData.append(bizType, avatar); const response await axios.post(/api/upload, formData, { // 不要手动设置 Content-Type让浏览器自己带 boundary });4. form表单上传与FormData老技术真的不老4.1 原生form表单的最简形态在AJAX还没普及的年代文件上传就是靠form表单直接提交。这个写法到现在依然有效HTML里enctype有三个可选值很多人不知道它们到底干嘛的enctype值作用适用场景application/x-www-form-urlencoded默认值表单数据被编码为键值对普通文本表单multipart/form-data每个字段单独编码支持二进制文件文件上传、含文件的表单text/plain纯文本传输目前很少用特殊调试场景如果表单里包含文件输入框但漏写了enctypemultipart/form-data浏览器会把文件名当成普通字符串提交后端拿不到文件内容。这种低级的坑在真实项目里真的出现过不少次。原生form提交最大的问题只有一个提交成功后页面会整个跳转到后端返回的地址。这在现在的前后端分离架构里基本不可接受所以日常开发我们基本不用纯form提交而是用JS创建一个FormData对象来模拟form提交。4.2 用FormData模拟表单上传FormData为什么比直接传JSON更适合文件上传是因为浏览器的XMLHttpRequest或fetch在序列化FormData时会自动按multipart/form-data格式处理文件字段和普通字段可以混合在一起const formData new FormData(); formData.append(file, fileObject); formData.append(fileName, fileObject.name); formData.append(description, 这是一张示例图片);这个特性非常重要你可以在同一个请求里既传文件又传业务参数后端用RequestPart(file)和RequestParam(description)分别接收就行不用把参数拼到URL里。4.3 FormData的一些隐藏细节第一append同一个字段名可以传多个文件后端可以用数组方式接收比如files字段接收多文件。第二FormData允许追加Blob对象所以不是File也能塞进去只要你给Blob指定一个filename。第三FormData在开发工具Network面板里显示为一个multipart/form-data的请求展开后能直接预览文件和参数排查上传问题非常方便。另外还有一个细节如果你想要上传进度用axios要在onUploadProgress回调里取progressEvent.loaded / progressEvent.total算百分比。fetch自身的Request对象不支持上传进度回调需要借助XMLHttpRequest或者axios这类库这一点在写原生上传逻辑时要提前想清楚。5. element-ui上传组件实战改造从demo到生产可用的距离5.1 el-upload的核心属性和触发链路element-ui的el-upload应该是国内用得最多的上传组件了但很多人只会填一个action地址遇到需求变了就不知道怎么改。先看它典型的demoaction就是上传接口地址name是文件字段名before-upload在文件选择的校验之后、实际上传开始之前触发on-success和on-error对应上传结果回调。这几个属性之间的关系是一条完整的触发链用户选文件 -before-upload可拦截 - 选择是否走action/http-request- 上传中触发on-progress- 成功或失败触发回调。5.2 压缩、类型检查、大小限制的接入点before-upload是整个组件里最灵活的一个钩子。它返回false会阻止上传返回Promise会在Promise resolve之后继续上传。这个特性让我们可以把类型检查大小校验压缩全部塞进去。注意这中间有个坑before-upload的返回值在被reject或者返回false时element-ui会停止上传但不会默认提示任何信息你需要自己在this.$message.error()里把错误原因告诉用户。另外压缩后的文件类型可能变了比如原图是png但你用toDataURL(image/jpeg)输出那file.type也变了后端如果校验文件后缀和MIME类型要注意联调确认。5.3 用http-request自定义上传绕开action统一走业务接口很多项目的上传接口不是简单的POST一个文件就能了事而是要带动态token、自定义请求头、还要签名字段。action属性写死的URL和静态headers满足不了动态需求这时候就要用http-request接管整个上传动作。http-request会收到一个包含file、onSuccess、onError、onProgress的对象。你需要自己调用上传逻辑并在合适时机调用onSuccess或onErrorelement-ui才会感知上传结果并触发on-success或on-error。这种做法的好处是所有上传逻辑收敛到一处不管上游是FormData还是base64最终提交都可以走统一封装的上传服务组件内部无需改动。5.4 图片回显base64、URL和objectURL上传之后展示图片其实也是一个容易被忽视的环节。常见回显方式有两种第一种是后端返回一个图片URLel-image或者img直接显示URL即可。第二种是上传前需要本地预览这时候可以拿到File对象后用URL.createObjectURL(file)生成一个临时URL也可以继续用FileReader转base64放进src。URL.createObjectURL的性能比FileReader好生成的blob:开头的URL是同步的但要注意使用完后释放否则会一直占着内存。base64方式虽然重但它有一个独特优势可以直接塞进img标签跨页面传递也很方便所以很多纯前端生成海报、分享卡的场景还是会选择base64。热搜里出现的vxeui图片渲染base64和kkfileview对比本质上也是在讨论同一个问题表格里渲染图片、在线预览文件时组件需要你提供的是URL还是base64。我的经验是表格组件渲染大量base64图片会明显卡顿能用URL绝对不要用base64只有在小规模预览时才用base64这是性能和便利性之间的取舍。6. 生产环境实战完整链路拆解与常见报错排查6.1 一条生产可用的完整上传链路把上面几节的内容串成一条完整链路大概是这样的用户通过el-upload选择图片拿到原始File对象。before-upload里先做类型检查只允许jpg/png/webp、大小检查超过2MB则提示。对超过阈值的图片用canvas压缩缩小尺寸、降低质量输出压缩后的File对象。把File对象放进FormData带上业务参数用axios走统一的业务上传接口。后端返回图片URL前端把URL回填到表单或者直接回显到页面上。如果有裁剪需求裁剪组件输出base64那就在裁剪组件拿到base64之后调用转换方法转成File再走第4步。这个过程听起来不复杂但每一环都有对应的细节和坑落地的代码量比想象中要多不少。6.2 常见报错的排查思路我整理了一个排查表这些报错都是真实出现过的基本覆盖了90%的上传问题报错现象根本原因解决方案请求返回413 Payload Too Largebody太大超过服务端限制前端压缩后端调大client_max_body_size返回400提示缺少file文件字段FormData字段名和后端接口不一致检查append(file)和后端RequestParam名称上传成功但后端拿到的文件是空手动设置了Content-Type导致boundary缺失不要手动设置Content-Type交给浏览器on-success触发了但页面没显示图片后端返回结构不是预期格式回显逻辑取错了字段和后端确认返回JSON结构打印response调试图片上传后被旋转了90度手机照片带EXIF方向信息canvas画图时没处理用exif-js或image-orientation处理后重绘大图压缩后变成全黑或白屏canvas尺寸超过浏览器上限先等比缩小到安全尺寸再绘制压缩6.3 根据自己的项目选一个最不折腾的组合文章写到这核心内容已经cover完了最后再根据我这几年的实际体会给一个组合建议也算是我给自己总结的默认配置只要能拿原始File就直接用FormData上传不转base64。中间任何环节出现了base64值立即转File不要让base64在项目里到处传递。组件层能用http-request接管上传逻辑就接管别依赖写死的action项目里业务接口改动太频繁了。图片预览优先用URL无论是后端返回URL还是本地URL.createObjectURLbase64只作为临时值存在内存里或者特殊情况使用。上传这个功能看着不起眼但它是几乎所有业务系统的刚需。把base64、File、FormData三者的本质弄明白再看element-ui这类组件的封装思路会清晰很多。以后再遇到换一个上传组件或者后端要改接口格式的需求你只要在上传链路里找到对应那一段改掉对应那一层就完全不会被卡住了。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →