端H5文件上傳全解:解決iOS/安卓無(wú)法調(diào)用圖庫(kù)與拍照的問(wèn)題)
做移動(dòng)端 H5 這幾年我最常被問(wèn)到的一個(gè)標(biāo)簽就是input typefile。表面看它就是個(gè)普普通通的文件選擇框可真放到 iOS 和安卓手機(jī)上點(diǎn)下去沒(méi)反應(yīng)、只有圖庫(kù)沒(méi)有拍照、彈出來(lái)一個(gè)瀏覽器文件管理器、安卓 App 內(nèi)嵌 WebView 直接不配合……各種奇怪表現(xiàn)簡(jiǎn)直能把人逼瘋。這篇文章就專(zhuān)門(mén)圍繞“前端 html inputfile 在 ios/安卓 無(wú)法選擇圖庫(kù)/拍照”這個(gè)問(wèn)題展開(kāi)把背后的原因、不同系統(tǒng)上的差異、以及我實(shí)測(cè)過(guò)能落地的方案一股腦寫(xiě)清楚。如果你是做移動(dòng)端 H5、活動(dòng)頁(yè)、Hybrid 混合開(kāi)發(fā)的同學(xué)這篇對(duì)你應(yīng)該會(huì)有不小幫助。1. 移動(dòng)端 file input 的真實(shí)痛點(diǎn)與適用場(chǎng)景1.1 這不是你寫(xiě)錯(cuò)代碼是瀏覽器內(nèi)核的差異很多同學(xué)第一次遇到這個(gè)問(wèn)題時(shí)第一反應(yīng)是“我代碼寫(xiě)錯(cuò)了”。其實(shí)大部分時(shí)候我們的 HTML 和 JS 寫(xiě)法在 PC 瀏覽器上完全沒(méi)問(wèn)題比如 Chrome 桌面版點(diǎn)擊input typefile就能彈出文件管理器選擇圖片后也能正常預(yù)覽上傳??赏瑯拥拇a放到手機(jī)上從 iOS Safari 到安卓 Chrome再到微信內(nèi)置瀏覽器和各種 App 的 WebView表現(xiàn)完全不一樣。這里面的根源在于移動(dòng)端瀏覽器的實(shí)現(xiàn)不是一個(gè)標(biāo)準(zhǔn)。iOS 上無(wú)論 Safari 還是內(nèi)嵌 WKWebView用的都是 WebKit 內(nèi)核安卓上 Chrome 是 Blink 內(nèi)核但國(guó)內(nèi)大量 App 的 WebView 是基于 Chromium 改的再疊加各手機(jī)廠商 ROM 自帶的瀏覽器和文件選擇器最終同一個(gè)input typefile標(biāo)簽會(huì)走完全不同的選擇流程。尤其是accept屬性和capture屬性不同內(nèi)核的解析策略不同甚至同一個(gè)系統(tǒng)版本里微信瀏覽器和系統(tǒng)瀏覽器表現(xiàn)都不一樣。所以我們?cè)谔幚磉@個(gè)問(wèn)題時(shí)不能只盯著一份“萬(wàn)金油”寫(xiě)法而是要先搞清楚當(dāng)前運(yùn)行環(huán)境到底是原生瀏覽器還是 WebView再針對(duì)場(chǎng)景做兼容方案。1.2 常見(jiàn)業(yè)務(wù)場(chǎng)景和踩坑癥狀我遇到過(guò)的大多數(shù)情況集中在下面幾類(lèi)業(yè)務(wù)里用戶頭像上傳需要讓用戶選擇“拍照”或者“從相冊(cè)選擇一張”。證件照、身份證上傳多數(shù)情況只允許從相冊(cè)選擇防止用戶現(xiàn)場(chǎng)拍一張模糊的照片。H5 活動(dòng)頁(yè)上傳截圖/評(píng)論圖片需要簡(jiǎn)單快捷地從圖庫(kù)選圖最好不用跳轉(zhuǎn)。企業(yè)內(nèi)部 App 內(nèi)嵌 H5 表單上傳附件、照片等。在這些場(chǎng)景里典型的表現(xiàn)癥狀我認(rèn)為可以歸納成下面幾類(lèi)癥狀常見(jiàn)出現(xiàn)環(huán)境初步原因點(diǎn)擊按鈕完全沒(méi)反應(yīng)安卓 App 內(nèi)嵌 WebView原生端沒(méi)有實(shí)現(xiàn) file chooser 回調(diào)有反應(yīng)但只彈出文件管理器沒(méi)有圖庫(kù)/拍照選項(xiàng)安卓部分 ROM 瀏覽器、部分 WebViewaccept屬性未生效或被忽略只有圖庫(kù)/文件沒(méi)有拍照選項(xiàng)iOS Safari 部分版本缺少capture或系統(tǒng)版本限制只有拍照沒(méi)有圖庫(kù)選項(xiàng)代碼中加了capturecameracapture 把拍照行為固定死了第一次能選圖第二次點(diǎn)擊沒(méi)反應(yīng)iOS WKWebViewinput 節(jié)點(diǎn)被移除或非用戶手勢(shì)觸發(fā)點(diǎn)擊選完圖無(wú)法預(yù)覽所有移動(dòng)端直接用了input.value本地路徑上面這個(gè)表格里的每一個(gè)癥狀我都在項(xiàng)目里真實(shí)遇到過(guò)。下面兩節(jié)我會(huì)分別拆解 iOS 和安卓?jī)蓚€(gè)平臺(tái)上的詳細(xì)情況然后再給一套我目前比較推薦的完整寫(xiě)法。2. iOS 端經(jīng)典問(wèn)題與處理方案2.1 為什么 iOS 上會(huì)出現(xiàn)“點(diǎn)不動(dòng)”和“彈不對(duì)”iOS 上的 Safari 和 WKWebView 對(duì)用戶手勢(shì)的要求非常嚴(yán)格。如果你是在一個(gè)異步回調(diào)里調(diào)用input.click()比如先請(qǐng)求接口、拿到某個(gè)字段后再觸發(fā)文件選擇框Safari 會(huì)直接判定這不是“用戶主動(dòng)手勢(shì)”從而靜默忽略這次點(diǎn)擊。很多同學(xué)在安卓上覺(jué)得沒(méi)問(wèn)題就上線了結(jié)果 iOS 用戶反饋點(diǎn)了沒(méi)反應(yīng)就是這個(gè)原因。另外一個(gè)常見(jiàn)問(wèn)題是accept屬性寫(xiě)得太“死”。比如有些同學(xué)為了限制圖片格式寫(xiě)了accept.jpg,.jpeg,.png這種寫(xiě)法在 PC 上是有效的但到了 iOS 上系統(tǒng)可能不會(huì)彈“照片圖庫(kù)”那個(gè)原生選擇器而是給你彈出一個(gè)文件 App 的瀏覽頁(yè)面。用戶看到后完全不知道該怎么選相冊(cè)里的照片體驗(yàn)上就基本等于“廢了”。還有一類(lèi)問(wèn)題來(lái)自 CSS 隱藏方式。為了做自定義上傳按鈕很多前端習(xí)慣給 input 加display: none然后在按鈕點(diǎn)擊事件里調(diào)用.click()。這種方案在 PC 上很順手但在 iOS 的某些 WebView 里display: none的 input 即使被.click()也可能不彈任何選擇器。穩(wěn)妥的做法是使用position: absolute; opacity: 0; width: 1px; height: 1px;這種“視覺(jué)隱藏但仍在布局中”的方式。2.2 iOS 端推薦寫(xiě)法和細(xì)節(jié)先給出一段我目前在 iOS 上驗(yàn)證過(guò)比較穩(wěn)定的基礎(chǔ) HTMLlabel forimageInput classupload-btn選擇圖片/label input typefile idimageInput acceptimage/* styleposition:absolute;opacity:0;width:1px;height:1px; /這里我推薦用label關(guān)聯(lián) input而不是在按鈕上掛 JS 事件再調(diào)input.click()。原因很簡(jiǎn)單label 是瀏覽器的原生用戶手勢(shì)控件點(diǎn)擊 label 就相當(dāng)于點(diǎn)擊 input天然繞過(guò)了 iOS 對(duì)手勢(shì)的檢測(cè)限制省去很多不必要的麻煩。如果你確實(shí)需要在 JS 里觸發(fā)點(diǎn)擊比如某些自定義組件沒(méi)法用 label那至少要保證兩點(diǎn)點(diǎn)擊事件回調(diào)里同步調(diào)用input.click()不要包一層setTimeout更不能在axios請(qǐng)求返回之后再調(diào)用。如果 input 是動(dòng)態(tài)創(chuàng)建的必須先document.body.appendChild(input)再執(zhí)行.click()并且確保沒(méi)有提前把 input 從 DOM 上移除。另外如果你希望用戶能自己選擇“拍照”還是“從相冊(cè)選擇”不加capture屬性就行。iOS 上acceptimage/*的 input 點(diǎn)擊后系統(tǒng)會(huì)彈出操作列表一般包含“照片圖庫(kù)”“拍照”“選擇文件”等選項(xiàng)。這個(gè)是 iOS 系統(tǒng)自帶的行為前端不需要額外處理。但需要注意如果你在代碼里加了capturecameraiOS 會(huì)直接跳過(guò)圖庫(kù)強(qiáng)制打開(kāi)相機(jī)。有些業(yè)務(wù)場(chǎng)景覺(jué)得自己已經(jīng)加了“拍照”按鈕結(jié)果用戶點(diǎn)進(jìn)去卻找不到圖庫(kù)入口就是這個(gè)屬性的影響。2.3 iOS 隱私權(quán)限對(duì)相冊(cè)選擇的影響iOS 14 之后系統(tǒng)對(duì)相冊(cè)的權(quán)限策略做了調(diào)整彈窗文案變成了“允許訪問(wèn)所有照片”或“選擇照片”。如果用戶選擇了“選擇照片”但沒(méi)勾選任何照片或者之前在權(quán)限彈窗里點(diǎn)了拒絕會(huì)出現(xiàn)一種很詭異的現(xiàn)象input 點(diǎn)擊后相冊(cè)界面能打開(kāi)但里面全空白或者只有“最近項(xiàng)目”空的分類(lèi)。遇到這種情況前端能做的不多因?yàn)檫@是系統(tǒng)級(jí)權(quán)限限制。我能給的建議是在選擇圖片的彈層或空狀態(tài)頁(yè)面里加一段引導(dǎo)文案告訴用戶“如無(wú)法選擇照片請(qǐng)到系統(tǒng)設(shè)置-隱私-照片中允許本應(yīng)用訪問(wèn)”并且提供一個(gè)按鈕方便用戶跳轉(zhuǎn)設(shè)置頁(yè)。在 Safari 里可以通過(guò)window.open引導(dǎo)但大部分 WebView 無(wú)法直接跳轉(zhuǎn)系統(tǒng)設(shè)置只能做文字提示。3. Android 端的差異與適配3.1 同一個(gè)標(biāo)簽各家的表現(xiàn)完全不同Android 的情況比 iOS 要復(fù)雜得多。首先安卓上的瀏覽器內(nèi)核碎片化嚴(yán)重Chrome、系統(tǒng)自帶瀏覽器、微信 X5 內(nèi)核、各類(lèi) App 內(nèi)嵌 WebView隨便數(shù)一下就有四五種。再加上華為、小米、OPPO、vivo 這些廠商會(huì)對(duì) AOSP 的文件選擇器做定制導(dǎo)致“一個(gè)input typefile標(biāo)簽十個(gè)設(shè)備可能給出十種點(diǎn)擊結(jié)果”。我遇到的最典型問(wèn)題是安卓 App 內(nèi)嵌 WebView 里點(diǎn)擊 input 完全沒(méi)反應(yīng)。這個(gè)問(wèn)題的根源通常不在前端而在原生端Android WebView 默認(rèn)沒(méi)有實(shí)現(xiàn)文件選擇的回調(diào)。原生開(kāi)發(fā)者需要重寫(xiě)WebChromeClient里的onShowFileChooser方法然后調(diào)用系統(tǒng)的文件選擇器或相冊(cè)再把結(jié)果通過(guò)ValueCallback返回給 WebView。如果這一步?jīng)]做前端再怎么折騰 HTML 和 JS 都沒(méi)用。所以如果是混合開(kāi)發(fā)場(chǎng)景我建議前端同學(xué)拿到任務(wù)后先跟原生同學(xué)確認(rèn)一句“你們的 WebView 支持 input file 嗎”這個(gè)問(wèn)題越早確認(rèn)后面踩坑越少。3.2 安卓上的推薦方案在安卓原生瀏覽器或 Chrome 里input typefile acceptimage/*一般都能彈出一個(gè)系統(tǒng)選擇器里面至少有“相機(jī)”和“文件”選項(xiàng)。但在內(nèi)置 WebView 里有的選擇器會(huì)直接漏掉“相冊(cè)”入口用戶只能從文件管理器里翻圖片體驗(yàn)極其痛苦。為了應(yīng)對(duì)這種差異我現(xiàn)在比較推薦的方案是頁(yè)面里明確區(qū)分“拍照”和“從相冊(cè)選擇”兩個(gè)入口分別用兩個(gè) input 來(lái)實(shí)現(xiàn)。input typefile idcameraInput acceptimage/* captureenvironment styleposition:absolute;opacity:0;width:1px;height:1px; / input typefile idalbumInput acceptimage/* styleposition:absolute;opacity:0;width:1px;height:1px; /第一個(gè)入口用captureenvironment意思是優(yōu)先打開(kāi)后置攝像頭拍照第二個(gè)入口只寫(xiě)acceptimage/*讓系統(tǒng)彈出圖庫(kù)或文件選擇器。這樣即使某個(gè)瀏覽器不彈“相機(jī)/圖庫(kù)”二選一的系統(tǒng)彈窗用戶仍然可以通過(guò)頁(yè)面上獨(dú)立的“拍照”和“相冊(cè)”按鈕完成操作。這里有一個(gè)細(xì)節(jié)capture的值在不同安卓版本上解析有細(xì)微差異有的寫(xiě)capturecamera有的寫(xiě)captureenvironment。我測(cè)試下來(lái)environment在更多設(shè)備上能正確調(diào)起后置攝像頭。如果你的業(yè)務(wù)里希望用戶自拍可以換captureuser但支持率不如environment穩(wěn)定。3.3 安卓 7 的本地文件訪問(wèn)限制安卓系統(tǒng)從 7.0 開(kāi)始對(duì)file://協(xié)議的訪問(wèn)限制變得非常嚴(yán)格。以前前端拿到input.value里的本地路徑比如/storage/emulated/0/DCIM/xxx.jpg還能直接塞到img src里預(yù)覽。現(xiàn)在這樣寫(xiě)基本都會(huì)失敗顯示不出來(lái)。解決辦法是永遠(yuǎn)不要依賴(lài)input.value當(dāng)圖片地址而是改用input.files[0]這個(gè) File 對(duì)象然后通過(guò)兩種方式生成預(yù)覽const file event.target.files[0]; if (!file) return; // 方式一用 URL.createObjectURL 生成臨時(shí)鏈接 const tempUrl URL.createObjectURL(file); document.getElementById(preview).src tempUrl; // 方式二用 FileReader 讀取成 Data URL const reader new FileReader(); reader.onload function(e) { document.getElementById(preview).src e.target.result; }; reader.readAsDataURL(file);這兩種方式在 iOS 和安卓上都通用。第一種性能更好適合大圖第二種兼容性最穩(wěn)并且可以直接用于預(yù)覽。上傳時(shí)你仍然可以繼續(xù)用new FormData()追加 File 對(duì)象不需要把 base64 字符串傳給后端除非后端接口明確要求二進(jìn)制轉(zhuǎn)字符串。4. 完整可落地的兼容方案示例4.1 頁(yè)面結(jié)構(gòu)設(shè)計(jì)綜合 iOS 和安卓?jī)蛇叺目游医o出一套現(xiàn)在項(xiàng)目里常駐的完整方案。頁(yè)面結(jié)構(gòu)上用一個(gè)按鈕區(qū)域承載兩個(gè)隱藏的 input用戶看到的按鈕文案根據(jù)當(dāng)前環(huán)境來(lái)調(diào)整。div classupload-area button typebutton idbtnCamera拍照/button button typebutton idbtnAlbum從相冊(cè)選擇/button /div input typefile idcameraInput acceptimage/* captureenvironment / input typefile idalbumInput acceptimage/* /這里我沒(méi)有給 input 加style隱藏而是放在頁(yè)面里后續(xù)通過(guò)全局 CSS 隱藏。注意不要用display: none我用的是input[typefile] { position: absolute; left: -9999px; opacity: 0; width: 0; height: 0; }這個(gè)隱藏方式的好處是input 仍然在文檔流里不會(huì)被瀏覽器判定為不可交互元素同時(shí)它又是不可見(jiàn)的不影響頁(yè)面美觀。4.2 JS 邏輯與文件校驗(yàn)接下來(lái)是 JS 部分的完整邏輯包括事件監(jiān)聽(tīng)、文件類(lèi)型校驗(yàn)、圖片預(yù)覽、以及處理 iOS 不能重復(fù)選擇同一文件的坑。const btnCamera document.getElementById(btnCamera); const btnAlbum document.getElementById(btnAlbum); const cameraInput document.getElementById(cameraInput); const albumInput document.getElementById(albumInput); const previewImg document.getElementById(preview); function handleFileSelect(event) { const file event.target.files[0]; if (!file) return; // 校驗(yàn)文件類(lèi)型 if (!file.type.startsWith(image/)) { alert(請(qǐng)選擇圖片文件); return; } // 校驗(yàn)文件大小大于 10M 直接攔截 if (file.size 10 * 1024 * 1024) { alert(圖片不能超過(guò) 10M); return; } // 生成預(yù)覽 const tempUrl URL.createObjectURL(file); previewImg.src tempUrl; // 這里可以繼續(xù)做上傳操作 uploadFile(file); // 清空 input 的 value保證同一文件再次選擇時(shí)能觸發(fā) change event.target.value ; } function uploadFile(file) { const formData new FormData(); formData.append(file, file); // 用 fetch 或 axios 發(fā)送到后端... } btnCamera.addEventListener(click, function() { cameraInput.click(); }); btnAlbum.addEventListener(click, function() { albumInput.click(); }); cameraInput.addEventListener(change, handleFileSelect); albumInput.addEventListener(change, handleFileSelect);這段代碼里最關(guān)鍵的一行是最后的event.target.value 。iOS 上如果你不重置 value用戶第一次選擇圖片后下次再點(diǎn)同一個(gè) input 選擇同一張圖change 事件不會(huì)觸發(fā)因?yàn)闉g覽器認(rèn)為文件沒(méi)有變化。這個(gè)坑在真實(shí)項(xiàng)目里非常常見(jiàn)。4.3 與原生 App 配合的“兜底方案”如果你的 H5 頁(yè)面是被包在 App 的 WebView 里而且原生端確實(shí)沒(méi)有實(shí)現(xiàn)onShowFileChooser那前端再怎么兼容都是無(wú)用功。這種情況下我建議直接走 native bridge 方案也就是前端通過(guò)window.webkit.messageHandlersiOS或者AndroidBridge安卓去調(diào)用原生自己的相冊(cè)/拍照模塊然后由原生把圖片的本地路徑或 base64 回傳給前端。這部分代碼需要跟原生約定接口簡(jiǎn)單示意如下function chooseImageByNative() { if (window.webkit window.webkit.messageHandlers) { // iOS 端調(diào)用原生相冊(cè) window.webkit.messageHandlers.chooseImage.postMessage({}); } else if (window.AndroidBridge) { // 安卓端調(diào)用原生相冊(cè) window.AndroidBridge.chooseImage(); } }原生選擇完畢后再通過(guò)回調(diào)函數(shù)把圖片地址傳給前端比如window.onNativeImageSelected function(base64) { ... }。這種方案雖然要多跟原生同學(xué)對(duì)一輪接口但它是混合開(kāi)發(fā)里最穩(wěn)的路徑不會(huì)受 WebView 內(nèi)核差異影響。5. 常見(jiàn)問(wèn)題排查與避坑清單5.1 我踩過(guò)的高頻問(wèn)題實(shí)錄第一個(gè)高頻問(wèn)題是 iOS 第一次能選圖第二次點(diǎn)擊無(wú)響應(yīng)。最開(kāi)始我也以為是 input 被移除或者變量被釋放后來(lái)一步步排查才發(fā)現(xiàn)就是change事件觸發(fā)后文件選擇器自動(dòng)關(guān)閉但 input 的 value 沒(méi)有被重置導(dǎo)致用戶選擇同一張照片時(shí)瀏覽器認(rèn)為文件沒(méi)變不再觸發(fā)。解決辦法就是上面代碼里的event.target.value 。第二個(gè)高頻問(wèn)題出現(xiàn)在安卓 WebView 中。前端點(diǎn)擊按鈕調(diào)用input.click()后頁(yè)面完全沒(méi)反應(yīng)連系統(tǒng)文件選擇器都不彈。用 Chrome DevTools 遠(yuǎn)程調(diào)試也看不到報(bào)錯(cuò)后來(lái)確認(rèn)是原生沒(méi)實(shí)現(xiàn)文件回調(diào)。這個(gè)問(wèn)題不是前端能修的只能通過(guò) bridge 或讓原生補(bǔ)onShowFileChooser解決。第三個(gè)問(wèn)題是用戶選了照片預(yù)覽圖卻一片空白。一開(kāi)始我以為是路徑問(wèn)題后來(lái)發(fā)現(xiàn)是后端返回的圖片地址帶了跨域限制跟 input 本身沒(méi)關(guān)系。前端預(yù)覽可以先直接用URL.createObjectURL(file)不要依賴(lài)后端的臨時(shí)鏈接等上傳完成后再用后端返回的正式地址替換。5.2 快速排查步驟速查表真機(jī)調(diào)試這個(gè)環(huán)節(jié)很多同學(xué)容易忽略。其實(shí)大多數(shù)問(wèn)題通過(guò)控制臺(tái)都能快速定位?,F(xiàn)象排查步驟大概率原因點(diǎn)擊按鈕無(wú)反應(yīng)在點(diǎn)擊事件里立刻console.log確認(rèn)代碼是否執(zhí)行input 未在 DOM、用戶手勢(shì)限制、WebView 未實(shí)現(xiàn)回調(diào)能彈選擇器但選完沒(méi)反應(yīng)在change事件里打印event.target.files監(jiān)聽(tīng)器未綁定、value 未清空導(dǎo)致不觸發(fā)選擇圖片后預(yù)覽空白檢查預(yù)覽地址是否為 blob/dataURL/還是本地 file 路徑直接用了input.value或后端圖片跨域只有拍照沒(méi)有圖庫(kù)查看 input 是否加了capture屬性capture 強(qiáng)制了相機(jī)行為刪除即可安卓 WebView 點(diǎn)擊無(wú)反應(yīng)找原生同學(xué)確認(rèn)是否重寫(xiě)onShowFileChooser原生 WebChromeClient 缺少文件選擇實(shí)現(xiàn)iOS 上選擇圖片權(quán)限彈窗空白讓用戶檢查系統(tǒng)隱私設(shè)置里的照片權(quán)限系統(tǒng)權(quán)限拒絕/限制訪問(wèn)5.3 幾個(gè)獨(dú)家的避坑心得再分享幾個(gè)文檔上不容易看到的經(jīng)驗(yàn)。第一multiple屬性在部分 iOS 版本上表現(xiàn)非常不穩(wěn)定如果你讓用戶多選圖片上傳邏輯要額外處理數(shù)組我在低版本 iPhone 上遇到過(guò)選擇多張后只能回調(diào)第一張的情況。業(yè)務(wù)上非必要不建議用multiple真要用就給用戶多做幾次單張選擇。第二有些安卓 ROM 的選擇器會(huì)把圖片文件“過(guò)一遍壓縮”你拿到的 File 對(duì)象大小跟原圖可能不一樣這時(shí)前端不要重復(fù)壓縮否則可能導(dǎo)致圖片質(zhì)量不可控。第三如果你的頁(yè)面里有 canvas 對(duì)圖片做壓縮或旋轉(zhuǎn)處理一定注意 iOS 上拍照的圖片會(huì)帶 EXIF 方向信息直接用 canvas 畫(huà)上去可能出現(xiàn)旋轉(zhuǎn) 90 度的問(wèn)題最好用 exif-js 這類(lèi)庫(kù)先矯正方向再繪制。這些坑聽(tīng)起來(lái)都不大但真上了生產(chǎn)環(huán)境每一條都可能是用戶投訴的導(dǎo)火索。6. 圖片上傳前的前端優(yōu)化細(xì)節(jié)6.1 大圖壓縮策略移動(dòng)端拍照的圖片動(dòng)不動(dòng)就 5M、10M直接傳給后端不僅慢還容易因?yàn)槌瑫r(shí)導(dǎo)致失敗。前端在上傳前做一次壓縮是很有必要的。我常用的方案是 canvas 壓縮先把圖片繪制到 canvas 上控制最長(zhǎng)邊不超過(guò) 1600px然后導(dǎo)出成 jpeg 格式質(zhì)量參數(shù)設(shè)在 0.8 左右。這樣一張 8M 的照片往往能壓到 300K 以內(nèi)而且視覺(jué)損失幾乎看不出來(lái)。function compressImage(file, maxSize 1600, quality 0.8) { return new Promise((resolve, reject) { const reader new FileReader(); reader.onload function(e) { const img new Image(); img.onload function() { let width img.width; let height img.height; const scale Math.min(maxSize / width, maxSize / height, 1); width Math.round(width * scale); height Math.round(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); // 在 iOS 上需要處理 EXIF 方向這里先略過(guò)實(shí)際可引入 exif-js canvas.toBlob(function(blob) { resolve(blob); }, image/jpeg, quality); }; img.onerror reject; img.src e.target.result; }; reader.onerror reject; reader.readAsDataURL(file); }); }要注意的是canvas.toBlob在安卓和 iOS 的兼容性都還行但如果你要兼容很老的內(nèi)核可能需要用canvas.toDataURL再轉(zhuǎn) Blob。此外前端壓縮并不是萬(wàn)能的為了保證上傳圖片的最終可用性后端還是要有文件類(lèi)型、大小、尺寸的二次校驗(yàn)。6.2 多場(chǎng)景下按鈕文案與交互處理移動(dòng)端上傳功能交互文案也需要適配。比如同樣是“從相冊(cè)選擇”iOS 的相冊(cè)入口很直白安卓部分設(shè)備彈出來(lái)的選擇器里寫(xiě)的是“文檔”或“文件”用戶容易困惑。這種情況下前端可以在按鈕下方加一行小字提示“圖片上傳支持拍照或從相冊(cè)選擇”。如果是在安卓 WebView 里文字可以改成“請(qǐng)選擇圖片或拍攝照片”。另外在上傳過(guò)程中要給用戶明確反饋。我在項(xiàng)目里一般會(huì)做三態(tài)按鈕未選擇圖片時(shí)是“上傳圖片”選擇后變成“正在上傳”按鈕禁用并顯示 loading上傳成功后變成“上傳成功可點(diǎn)擊更換”。這個(gè)交互很簡(jiǎn)單但能極大減少用戶重復(fù)點(diǎn)擊導(dǎo)致的重復(fù)上傳。6.3 相機(jī)與圖庫(kù)雙入口的最終取舍回到標(biāo)題里最核心的問(wèn)題如何解決 iOS/安卓無(wú)法選擇圖庫(kù)/拍照。從我的實(shí)踐來(lái)看最省心的方式就是在頁(yè)面設(shè)計(jì)階段就放棄“一個(gè)按鈕搞定所有”的幻想。理想狀態(tài)是頁(yè)面上有醒目的“拍照”和“相冊(cè)”兩個(gè)入口。拍照入口的 input 帶captureenvironment保證能喚起相機(jī)。相冊(cè)入口的 input 只帶acceptimage/*保證系統(tǒng)能彈出圖庫(kù)。兩個(gè) input 都通過(guò)label或同步 JS 點(diǎn)擊來(lái)觸發(fā)。拿到文件后統(tǒng)一走壓縮、預(yù)覽、上傳、重置 value 的流程。這套方案我在微信 H5、普通瀏覽器、App 內(nèi)嵌 WebView 里都驗(yàn)證過(guò)不敢說(shuō)百分之百覆蓋所有機(jī)型但至少能解決 95% 以上的“無(wú)法選擇圖庫(kù)/拍照”問(wèn)題。剩下 5% 的極端情況基本就是沒(méi)有實(shí)現(xiàn)文件回調(diào)的原生 WebView那也不是前端單獨(dú)能解決的必須拉上客戶端同學(xué)配合。說(shuō)到最后還是想提醒一句移動(dòng)端這種看似簡(jiǎn)單的上傳功能其實(shí)牽扯了系統(tǒng)手勢(shì)限制、瀏覽器內(nèi)核差異、原生 WebView 配置、權(quán)限管理和前端狀態(tài)處理等好幾層?xùn)|西。遇到問(wèn)題別急著懷疑自己代碼先按環(huán)境把問(wèn)題定位清楚再對(duì)癥下藥。我自己現(xiàn)在做任何移動(dòng)端上傳需求第一件事就是先確認(rèn)運(yùn)行環(huán)境和原生能力第二件事就是準(zhǔn)備一個(gè)可以直接復(fù)用的雙入口方案。照著這個(gè)思路來(lái)你也能少走不少?gòu)澛贰?