ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

图片上传前预览实战:FileReader与ObjectURL原理与避坑

图片上传前预览实战:FileReader与ObjectURL原理与避坑 先问一个最基本的问题你们产品里的图片上传是不是用户在电脑里选完图片界面上还是一片空白非得等文件传完、服务器返回url才能看到图片效果如果是那我强烈建议你马上把这个体验改掉。用 JS 实现图片预览上传前预览这个需求算是我做过的项目里反复出现频率最高的实战技能点之一用户注册换头像、商品主图上传、富文本编辑器插图、实名认证证件拍照……几乎每个带文件上传的模块都会碰到它。做得好产品会显得非常顺畅做得糙用户就会反复测试上传、抱怨为什么选完图片没反应。这篇博客我不会跟你讲太多花哨的东西就来一遍完整的实战拆解原理、代码、避坑经验全都给你摊开讲。无论你是刚入行的前端新人还是被临时拉去改需求的全干工程师都应该用得上。我计划从零开始把单图预览、多图预览、压缩优化、拖拽上传、编辑回显这些场景一次性讲透每一步都会给出可以直接用的代码和踩坑提示。最后我还会把这些年实际遇到的问题整理成一个排查速查表遇到问题时可以直接翻到后面找解决方案。1. 需求分析与功能拆解1.1 上传前预览到底解决了什么问题先想清楚一个道理为什么大多数产品都要求上传前预览最直接的原因是反馈闭环。用户选择一个文件后如果界面上什么都不发生他其实没法确认自己有没有选对文件——是选成了桌面截图还是真正的头像是完全不知道的。等传到服务器再回显速度快的时候还好速度慢的时候就变成了一场折磨。上传前预览解决的恰恰是这个问题文件还在本地浏览器通过自己的能力读取并展示它让用户立刻获得视觉反馈。这不只是体验层面的优化在实际业务里它还能承担更多职责。举个例子我做后台系统的时候经常遇到需要上传一个几百KB的 logo 图如果不做前置预览用户上传了一张 10MB 的照片等传到服务器做压缩回显整个过程既慢又浪费流量。而做了预览功能之后前端可以在本地就发现这张图太大、格式不对直接把问题拦在门口。还有一个容易被忽略的价值预览是很多高级功能的基础。比如图片裁剪、加水印、旋转校正都需要先把图片在本地展示出来再在 Canvas 上做操作。所以上传前预览不是孤立的它往往是整个图片处理链路的第一个环节。1.2 两条主流技术路线FileReader 与 ObjectURL实现上传前预览其实有两条技术路线网上资料很多但经常把两者混着讲容易把人搞晕。我先给结论再详细展开。技术方案核心原理优点缺点适用场景FileReader.readAsDataURL把图片文件读取为 base64 字符串直接赋给 img.src兼容性好几乎全浏览器可用编码后的字符串就是数据本身不依赖临时对象base64 字符串体积比原文件大约 33%大图时内存压力大需要等文件完全读入内存单张图片、兼容性要求极高的旧系统URL.createObjectURL在浏览器内存中生成一个临时 URL指向待上传文件速度快不进行编码转换原文件多大内存开销就是多大性能极佳生成的 URL 需要手动释放否则会占用内存IE 下的兼容性处理稍麻烦多图预览、大图预览、频繁切换的场景如果只让我推荐一种我会首推URL.createObjectURL理由很简单性能好、代码量少。但是这不代表FileReader一无是处。因为readAsDataURL会把文件转成 base64 字符串后续如果要把这张图传给后端base64 的格式其实是可以直接当字符串塞给接口的。所以说场景不同、选择不同我在实际项目中就用默认用 ObjectURL需要提前拿 base64 数据时才走 FileReader这套策略。1.3 梳理清楚代码组织思路在动手写代码之前我习惯先画一下流程明确我们需要哪些输入、哪些输出、哪些中间状态。输入用户在input typefile里选择的文件或拖拽进来的文件核心校验文件是否存在、文件类型是否为图片或是否在允许的白名单内、文件大小是否超标预览输出图片本地 URL用于填充到img标签或 Canvas 中中间状态选文件前的占位状态、预览生成中的 loading 状态、校验失败的错误提示清理逻辑切换图片或组件销毁时释放掉 ObjectURL避免内存泄漏这个流程理清楚之后写代码就是顺理成章的事了。很多新手写这个功能可能也就十行八行代码但把状态边界都想清楚的人写出来的东西负责得多。2. 核心 API 原理从 File 到可视化2.1 认识浏览器里的 File 对象很多人写了很久的前端对File对象依然是一知半解。我先把这个东西讲清楚。当你通过input typefile选择了一张图片后浏览器会为你创建一个File对象它本质上是Blob的子类带有文件的基本信息。const fileInput document.getElementById(fileInput); fileInput.addEventListener(change, function (event) { const files event.target.files; // 这是一个 FileList 对象 const file files[0]; // 单个 File 对象 console.log(file.name); // 文件名例如 avatar.png console.log(file.size); // 文件大小单位字节 console.log(file.type); // MIME 类型例如 image/png console.log(file.lastModified); // 最后修改时间戳 });这里有几个关键点。第一event.target.files是只读的FileList不能用push等方法修改要操作它得先转成真正的数组。第二File.type是浏览器根据文件格式推断的 MIME 类型不是根据扩展名猜的所以用它做类型校验比较可靠但并不是所有浏览器都识别得完全一致。第三input的accept属性只是给文件选择器一个建议过滤用户依然可以通过所有文件选项绕过它所以前端必须再做一次真实校验。2.2 FileReader把文件变成可展示的字符串FileReader是 HTML5 时代的产物它的功能就是异步读取文件内容。做图片预览时我们主要用的是readAsDataURL方法。function previewWithFileReader(file) { const reader new FileReader(); // 读取完成后触发 onload reader.onload function (loadEvent) { const dataUrl loadEvent.target.result; // 类似 data:image/png;base64,iVBOR... document.getElementById(previewImg).src dataUrl; }; // 读取失败会触发 onerror reader.onerror function () { alert(文件读取失败请重试); }; reader.readAsDataURL(file); }整个读取过程是异步的所以不用担心阻塞页面。但要注意FileReader每次只能处理一个文件读取任务如果你并发读多张图建议每张图单独创建FileReader实例反复调用同一个实例会抛异常。另外dataUrl的本质是 base64 编码你如果打印出来看会发现它是一个很长的字符串所以读大图时内存会非常紧张。FileReader还有兄弟方法如readAsText、readAsArrayBuffer但做图片预览用readAsDataURL最直观。2.3 ObjectURL浏览器给你开了一扇临时门URL.createObjectURL是另一个思路浏览器不读取文件内容而是给这个文件生成一个临时的内存地址这个地址形如blob:http://localhost:3000/xxxx-xxxx-xxxx。图片标签可以直接用它作为src。function previewWithObjectURL(file) { const objectURL URL.createObjectURL(file); document.getElementById(previewImg).src objectURL; // 注意使用完需要释放 // URL.revokeObjectURL(objectURL); }生活化类比一下FileReader相当于你把一本书的全部内容抄到一张纸上然后拿这张纸给人看URL.createObjectURL则相当于你在图书馆给这本书贴了一个索引号别人拿着这个索引号就能直接翻到这本书。当然是索引号更快更省力。正因如此ObjectURL在执行效率上的优势非常明显但是坑也很明显它不会自动清理。你每调用一次createObjectURL浏览器内存里就挂着一个引用页面关闭时没问题但如果页面一直开着反复预览多张图片内存就会只涨不落最终把页面拖垮。所以正确的习惯是URL 使用完毕后一定要调用revokeObjectURL把它销毁。2.4 兼容性老浏览器怎么办现在的浏览器环境已经非常友好Chrome、Edge、Firefox、Safari 对于FileReader和ObjectURL的支持都已经很完备。真正需要留意的反而是两类场景一类是 IE10/IE11。这两个老家伙虽然支持FileReader但URL.createObjectURL需要加前缀window.URL || window.webkitURL。我在老项目中一直用的是兼容写法const objectURLSupport typeof window.URL ! undefined typeof window.URL.createObjectURL function; const fileReaderSupport typeof window.FileReader ! undefined;如果两者都不支持基本是 IE9 及更早那抱歉这个功能做不了建议引导用户升级浏览器或直接采用普通上传把预览交给服务器返回。另一类是移动端 WebView 的差异。部分安卓机的内置浏览器对图片 MIME 类型识别并不总是准比如 iOS Safari 拍出来的照片File.type返回的可能是image/jpeg也可能是image/heic后者在某些浏览器里展示不出来。这种时候我会额外判断如果FileReader读出来之后img.src没有正常加载就触发onerror事件提示用户换格式。3. 从零到一单图上传前预览完整实现3.1 HTML 骨架与样式设计先搭一个最基础的单图预览页面。我不建议直接用默认的input[typefile]原生效用体验一般我习惯把它隐藏掉用自定义的按钮或图片区域触发选择。div classpreview-uploader div classpreview-box idpreviewBox img idpreviewImg alt预览图片 / div classpreview-placeholder idpreviewPlaceholder点击选择图片/div /div input typefile idfileInput acceptimage/* hidden / p classpreview-tip idpreviewTip/p /div样式这里我不贴太多核心思路是previewBox作为容器设置固定宽高和圆角边框img默认隐藏当有预览时展示并设置object-fit: cover或containpreviewPlaceholder在无图时显示previewTip用于展示校验错误或加载状态。注意点击previewBox时要触发fileInput.click()这样整个盒子都变得可点击交互面积更大体验更好。3.2 核心逻辑监听 change 事件与文件校验核心代码并不复杂但一定要把状态判断做完整。下面我给出一个可以直接运行的版本。const fileInput document.getElementById(fileInput); const previewBox document.getElementById(previewBox); const previewImg document.getElementById(previewImg); const previewPlaceholder document.getElementById(previewPlaceholder); const previewTip document.getElementById(previewTip); let currentObjectURL null; previewBox.addEventListener(click, function () { if (!fileInput.disabled) { fileInput.click(); } }); function resetPreview() { if (currentObjectURL) { URL.revokeObjectURL(currentObjectURL); currentObjectURL null; } previewImg.removeAttribute(src); previewImg.style.display none; previewPlaceholder.style.display block; previewTip.textContent ; } fileInput.addEventListener(change, function (event) { const file event.target.files[0]; if (!file) { return; } // 1. 类型校验 if (!file.type.startsWith(image/)) { previewTip.textContent 请选择有效的图片文件jpg/png/webp 等; fileInput.value ; return; } // 2. 大小校验这里假设限制为 5MB const maxSize 5 * 1024 * 1024; if (file.size maxSize) { previewTip.textContent 图片大小不能超过 5MB; fileInput.value ; return; } // 3. 通过校验后生成预览 if (currentObjectURL) { URL.revokeObjectURL(currentObjectURL); } currentObjectURL URL.createObjectURL(file); previewImg.src currentObjectURL; previewImg.onload function () { previewImg.style.display block; previewPlaceholder.style.display none; previewTip.textContent ; }; previewImg.onerror function () { resetPreview(); previewTip.textContent 图片预览失败文件可能已损坏或格式不受支持; }; });这里有几个细节我可以展开讲。第一为什么校验失败后要执行fileInput.value 因为如果用户第一次选了一个非法的文件校验失败后下一次再选同一个文件浏览器会认为input的值没有变化change事件就不触发了。清空value可以强制下一次选择同样文件时也能正常触发。第二为什么要先revokeObjectURL再重新赋值因为previewImg.src每次引用一个 blob URL如果不撤销旧引用就会造成内存泄漏。你在一个页面上反复换头像十几次旧 URL 全部存在内存里加起来就是不小的开销。第三previewImg.onload很重要。有些人直接把img.src赋值之后就不管了如果图片本身不合法浏览器加载不出来界面就一直停留在旧状态。有了 onload/onerror 两个回调界面的反馈才完整。3.3 用户体验细节加载提示与错误提示这里我说说实战中的体验优化。预览生成实际上是很快的但本地图片也可能很大比如手机拍的照片动辄几 10MB生成 blob URL 虽然快但img加载并渲染出来也需要几十毫秒到几百毫秒。如果用户按住不放图片还没出来就会有到底选没选上的疑虑。我习惯的做法是在生成预览前把previewTip文案改成正在加载图片...等 onload 触发后再清空同时可以给预览容器加一个类名比如加个半透明的遮罩或者 loading 特效。另一个细节是如果预览失败应该保留错误提示并自动重置 input这样用户不用自己重新找文件。当然所有提示文案都要做成用户能看懂的话而不是技术术语。之前我看过有同事把格式不正确写成了MIME 类型校验失败用户看了当场懵掉。宁可直白一点图片格式不支持请换 JPG 或 PNG 文件。4. 升级玩法多图预览、图片压缩与拖拽上传4.1 多图预览把单点逻辑变成列表逻辑现实的业务需求里单图预览往往只是开胃菜更多的其实是多图上传比如商品图集、功能截图、证件照批量上传等等。多图和多单图的区别本质上就是把一个 File 对应一个预览位置变成一组 File 对应一组预览位置。这个场景下最合理的做法是用数组维护所有待上传文件同时维护一个Map或者数组保存每个文件对应的预览 URL。数据结构清楚了代码自然顺。下面是核心思路。const uploadContainer document.getElementById(uploadContainer); const addButton document.getElementById(addButton); const fileInput document.getElementById(fileInput); const previewList []; // 保存 { file, url } 的对象数组 // 添加图片时触发 fileInput.addEventListener(change, function (event) { const files Array.from(event.target.files); // 转为数组 files.forEach(file { const url URL.createObjectURL(file); previewList.push({ file, url }); renderPreviewItem(file, url); }); fileInput.value ; }); function renderPreviewItem(file, url) { const item document.createElement(div); item.className preview-item; const img document.createElement(img); img.src url; img.style.width 100%; img.style.height 100%; img.style.objectFit cover; const deleteBtn document.createElement(button); deleteBtn.textContent 删除; deleteBtn.className delete-btn; deleteBtn.addEventListener(click, function () { const index previewList.findIndex(p p.url url); if (index ! -1) { URL.revokeObjectURL(previewList[index].url); previewList.splice(index, 1); } item.remove(); }); item.appendChild(img); item.appendChild(deleteBtn); uploadContainer.appendChild(item); }这套代码里两个细节值得注意。第一event.target.files本身就是类数组但不可直接forEach所以要先Array.from转换。第二每个删除按钮绑定的事件里既要从previewList移除数据也要revokeObjectURL释放内存然后从 DOM 中移除节点。三步都别漏否则会出现界面删掉了但全选数据里还有的诡异问题。4.2 图片压缩为上传环节减负做过真实项目的人都知道用户拿着手机随手拍的照片动辄 3-5MB有的甚至十几MB。这样的文件直接传到服务器速度慢不说服务器那边还要再做压缩处理。前端如果能先压缩一下再上传体验会好非常多。前端压缩图片的核心原理很简单把图片绘制到 Canvas 上再用canvas.toBlob()导出成新的压缩图片文件。function compressImage(file, maxWidth 1280, quality 0.8) { return new Promise((resolve, reject) { const img new Image(); const objectURL URL.createObjectURL(file); img.onload function () { // 计算缩放尺寸等比缩放 const scale Math.min(1, maxWidth / img.width); const width Math.round(img.width * scale); const height Math.round(img.height * scale); const canvas document.createElement(canvas); canvas.width width; canvas.height height; const ctx canvas.getContext(2d); ctx.drawImage(img, 0, 0, width, height); canvas.toBlob(function (blob) { URL.revokeObjectURL(objectURL); if (blob) { resolve(blob); } else { reject(new Error(canvas 导出失败)); } }, image/jpeg, quality); }; img.onerror function () { URL.revokeObjectURL(objectURL); reject(new Error(图片加载失败)); }; img.src objectURL; }); }这段代码的流程是先将文件转成Img等onload后获取原始宽高然后按maxWidth等比缩放绘制到 Canvas 中最后导出为 JPEG 格式。压缩质量和体积是成反比的quality 0.8是我常用的一种平衡值对多数照片来说画质损失基本感知不到但体积能压缩掉至少一半。用的时候就更灵活了可以预览时用原图给大家看真正上传时才执行压缩。或者更粗暴一点——预览生成之后直接就地压缩连预览都是缩略图。但我的建议是预览展示尽量用高质量原图压缩只发生在最终上传给服务器前因为用户可能还要在预览里观察图片细节。4.3 拖拽上传交互增强的神来之笔很多后台系统都喜欢做拖拽上传图片的交互。这个功能同样需要用到底层的文件获取能力但事件来源从input.change变成了drop。const dropZone document.getElementById(dropZone); dropZone.addEventListener(dragover, function (event) { event.preventDefault(); // 必须阻止默认行为否则不会触发 drop dropZone.classList.add(dragging); }); dropZone.addEventListener(dragleave, function () { dropZone.classList.remove(dragging); }); dropZone.addEventListener(drop, function (event) { event.preventDefault(); dropZone.classList.remove(dragging); const files Array.from(event.dataTransfer.files); const imageFiles files.filter(f f.type.startsWith(image/)); if (imageFiles.length 0) { alert(拖入的文件中包含非图片文件); return; } imageFiles.forEach(file { // 复用之前的添加预览逻辑 addPreviewItem(file); }); });这里有几个实战中容易踩的坑。第一dragover和drop事件都需要preventDefault()否则浏览器默认行为会尝试打开拖入的文件页面整个跳走。第二dragleave和dragover是高频触发事件如果你在回调里做复杂的 DOM 操作记得做节流处理否则容易卡顿。第三拖入的dataTransfer.files里可能混着文件夹你如果不想支持文件夹最好也加上类型过滤。5. 进阶场景编辑回显与移动端适配5.1 编辑场景下如何回显已有图片预览功能不只是选择新图片才会用到。在编辑页比如用户要改头像界面上一般需要先把已有的头像展示出来然后旁边放一个重新选择按钮。这个已有图片回显有两种情况。如果已有图片是服务器返回的 URL比如https://example.com/avatar.jpg那很简单直接把 URL 塞给img.src就行不需要任何 JS 技巧。麻烦的情况是表单最终提交时要带上新的图片文件但用户可能不换头像这时你需要把已有的 URL 图片转换成File对象和其他待上传文件一起拼在 FormData 里。我以前遇到这个需求时走的方案是用fetch下载图片转成Blob再包成File。async function urlToFile(url, fileName, mimeType) { const response await fetch(url); const blob await response.blob(); return new File([blob], fileName, { type: mimeType }); }不过这个方法是不是适用取决于你的服务器是否允许跨域请求下载该图片。如果不允许跨域请求就直接失败。所以我的建议是后端在设计接口时如果明确存在编辑回显重新上传的需求最好在返回数据里同时给一个fileId前端提交时如果用户没换图片就只提交fileId换了才传新的File。这样既省流量又不用绕弯子。5.2 移动端拍照照片的方向问题移动端拍照是图片上传的高频场景但这里藏着一个坑很多手机拍出来的照片会带有 EXIF 方向信息。比如你把手机竖着拍一张照片实际图片像素里可能已经是横躺的只是 EXIF 里有旋转90度的标记而img标签在移动端很多浏览器会根据这个标记自动纠正方向但一旦你把图片绘制到 Canvas 再做压缩方向信息可能就丢了换出来的照片会被躺平。解决方向问题一般有几个方案。轻量级方案是引入exif-js库读取 EXIF 方向然后手动在 Canvas 绘制的时候做旋转处理。重型方案是用loadImage这类库自动处理所有转换。如果你不想引入额外依赖也有一些简单技巧比如先把图片放img中让浏览器自行处理再把处理后的图片绘制到 Canvas。但说实话最干净的做法是后端在生成图片时剥掉 EXIF 信息统一标准方向。前端不是不能做只是处理 EXIF 的代码量不小而且容易出边界情况。我个人经验是如果产品对图片方向有严格要求前段先尽量修正后端再加一道保险。5.3 在 Vue 和 React 中使用预览功能现代前端已经很难绕开框架很多人会问这个功能的原生写法到了 Vue/React 里是不是就不适用了其实核心逻辑完全一样只是数据绑定和 DOM 操作的姿势变了。拿 React 举例核心逻辑是用useState保存预览 URLuseEffect里处理对象生命周期。function AvatarUploader() { const [previewUrl, setPreviewUrl] useState(null); const [file, setFile] useState(null); const inputRef useRef(null); const handleChange (event) { const selectedFile event.target.files[0]; if (!selectedFile) return; setFile(selectedFile); const url URL.createObjectURL(selectedFile); setPreviewUrl(url); }; // 清理时释放旧 URL useEffect(() { return () { if (previewUrl) { URL.revokeObjectURL(previewUrl); } }; }, [previewUrl]); return ( div {previewUrl img src{previewUrl} alt预览 /} input ref{inputRef} typefile acceptimage/* onChange{handleChange} / /div ); }这里有个 React 特有的点useEffect的清理函数会在下次 effect 执行前调用所以每次更换预览时旧 URL 都会被释放掉。这等于把之前手动revokeObjectURL的逻辑交给了框架的声明周期非常舒服。Vue 里也类似在watch或onBeforeUnmount里做清理。另一个框架场景下的细节是很多组件库封装了上传组件比如 Element Plus 的el-upload或 antd 的Upload它们内部已经做了预览能力但如果你需要在预览区域做自定义压缩等操作最终还是要回到原生FileReader/ObjectURL的掌握上。所以别觉得组件库帮我做了就不需要学了基础功在关键时候是非常值钱的。6. 常见问题与排查技巧实录这一节我把这些年实际遇到的、群里问过的高频问题整理成一份速查表每一个都是真实踩过的坑。现象原因解决方案点击按钮无反应文件选择框没弹出input 被隐藏且点击事件绑定在父元素上但父元素没有触发 fileInput.click()确认点击事件内部真的有fileInput.click()调用选择图片后预览区域没有变化没有在 change 回调里读取 files或者 files 长度为 0断点检查 event.target.files确认 input 不是 disabled总是提示请选择有效图片file.type为空或识别错误用扩展名二次校验做兜底或对file.type 的情况单独处理同一张图片反复选择第二次不触发input 的 value 未被清空每次预览前清空fileInput.value页面长时间使用后卡顿ObjectURL 未被 revoke在替换和卸载时及时revokeObjectURL大图预览很慢原图太大img 直接加载大文件开启 canvas 压缩并生成缩略图再展示预览的图片方向不对移动端 EXIF 方向被 Canvas 剥掉读取 EXIF 信息并在绘制时纠正方向拖拽图片进入时页面直接打开图片忘记阻止 dragover/drop 默认行为两个事件都调用event.preventDefault()删除预览项后再次全选仍包含旧图只删了 DOM 节点没有删数组数据同步维护数据结构连同 URL 一起 release6.1 Chrome 更新后 File 类型识别变化前端这个领域浏览器更新后都会有各种小惊喜。我记得有一段时间 Chrome 对某些 PNG 文件识别file.type返回的是image/png没问题但对无扩展名的文件可能返回一个空字符串。这就是为什么我在校验时可能不会只用file.type.startsWith(image/)而是会加一个兜底判断function isImage(file) { return file.type.startsWith(image/) || /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(file.name); }这个兜底有自己的问题扩展名可以伪造文件内容未必是图片。但作为前端体验层面的校验已经够用了。真要完全确定只能把文件读一部分出来看 magic bytes前端做这个有点杀鸡用牛刀。6.2 大文件预览与内存释放的完整示例很多读者看完上面的表格可能还是不知道到底怎么释放才是完整的我给一个更完整的伪代码流程function addFileToPreview(file) { const url URL.createObjectURL(file); // 存到当前组件/页面的数组中 previewRegistry.push({ file, url }); return url; } // 移除图片时 function removeFromPreview(id) { const item previewRegistry.find(i i.id id); if (item) { URL.revokeObjectURL(item.url); // 从数组中删除并删除对应的 DOM 节点 previewRegistry previewRegistry.filter(i i.id ! id); } } // 页面卸载或组件销毁时 function destroyAllPreviews() { previewRegistry.forEach(item { URL.revokeObjectURL(item.url); }); previewRegistry []; }我总结的释放原则是创建一条 URL在它被替换或不再显示时立刻回收。别依赖页面刷新来兜底在 SPA 应用里页面根本不刷新内存泄漏就是这样一天一天堆出来的。6.3 针对不支持 ObjectURL 的浏览器降级最后说说降级方案。如果检测到浏览器不支持URL.createObjectURL代码可以退回FileReader.readAsDataURL。这两种方案对外暴露的接口完全一致都是给我一个能用于 img.src 的字符串所以切换起来非常自然。function getFilePreviewURL(file) { return new Promise((resolve, reject) { if (typeof window.URL ! undefined URL.createObjectURL) { resolve(URL.createObjectURL(file)); } else if (typeof window.FileReader ! undefined) { const reader new FileReader(); reader.onload () resolve(reader.result); reader.onerror () reject(reader.error); reader.readAsDataURL(file); } else { reject(new Error(当前浏览器不支持本地预览请升级浏览器)); } }); }用 Promise 包装一下调用方就只管拿到 URL 去填img.src至于内部用的什么方案它不需要关心。这个封装思路在项目里很好用尤其是当你的代码要兼容旧浏览器的时候。结尾一点个人经验写到最后我想说几句实在话。图片上传前预览这个功能代码量大不大不大。核心技术难点有没有也没有想象中那么高深。但就是这样一个功能我在不同的项目里前前后后写了不下二十次每一次都能碰到新的细节问题原因就是它和文件、内存、异步事件、浏览器兼容、用户体验全部都有关联。如果你只是照着网上的教程抄三个方法就算交差了那以后业务一复杂坑还是会回来的。我个人最大的体会是两个。第一个预览功能一定要和待上传文件列表的数据结构分开维护预览归预览上传归上传两者在用户换图时由中间层做同步不要用看一眼预览然后再去 DOM 里找文件的模糊思路。第二个打开控制台的 Performance/内存面板模拟反复选图、删除、再选的操作如果你能看到内存占用曲线在缓慢增长那就是你的revokeObjectURL没写对地方千万别忽视这种暂时看不出毛病的隐患。还有一个小技巧如果你想让预览出来的图片看起来更柔和、更符合业务风格可以用 CSS 的filter做轻微亮度、饱和度的调整但千万别在预览里动手改文件本身的像素那应该交给 Canvas 去处理毕竟用户看到的就是他最终上传的你在预览里加了滤镜到了服务器上是原图反而会引发为什么我上传后变样了的投诉。这篇文章给出的所有代码块都可以直接复制到你的 HTML 页面里试跑。建议你从单图版开始练手跑通了再改多图版最后加上压缩和拖拽。等你把这些场景都趟过一遍往后哪怕要接uniapp或者其他框架的图片选择功能心里也会有底得多。
返回列表