前言
最近工作一直在使用vue+vux做移動端專案,有一個拍照上傳照片的需求,發現vux裡並沒有實現,調研過非官方的vux-uploader後,感覺還不是很理想。
其實網上已經可以找到很多已經實現的成熟方案,但是在調研這個需求的時候,我發現在各種實現方案中也有一些puzzle的知識點,因此自己動手擼了一個輪子vux-uploader-component,並記錄一二。
需求
元件的互動功能要求如下:
- html5呼叫手機相機
- 渲染圖片為縮圖
- 前端壓縮圖片
- 預覽大圖
- 刪除當前圖片
- 自動上傳
部分關鍵技術點的實現方案
使用html media capture呼叫手機端的相機
<input type="file" accept="image/*" capture />
複製程式碼
既然是在HTML5規範中,那最關心的問題肯定是相容性了
可以看到,在大部分的主流平臺,相容性還可以接受,andriod
2-4 都支援,只是在ios
6-10支援不太好。
感興趣的可以在自己的手機上測測相容性
使用ULR.createObjectURL
獲取圖片地址
blobURL = ULR.createObjectURL(object)
複製程式碼
object引數可以為
File
、Blob
、MediaSource
在這一塊可以衍生出好幾個問題:
- 已知獲取圖片地址的方法有
URL.createObjectURL
和FileReader.readAsDataURL
,那應該使用哪個?為什麼? - 在
IOS
中拍照獲取的圖片會自動旋轉,為什麼?怎麼解決? File
和Blob
是什麼關係?Blob URL
和Data URL
又有什麼區別?Data URL
怎麼轉換成Blob
?
使用canvas
來壓縮圖片
可以從兩個方面可以進行壓縮:
-
取一個最大寬度的限制對圖片的寬高尺寸進行等比例的縮小
canvas.width = Math.min(image.naturalWidth, option.maxWidth) const ratio = canvas.width / image.naturalWidth canvas.height = image.naturalHeight * ratio 複製程式碼
-
canvas.toDataURL
指定生成jpeg
或者webp
格式的圖片,可以指定0-1之間的encoderOptions
dataURL = canvas.toDataURL("image/jpeg", encoderOptions) 複製程式碼
最後生成的圖片大小 = 原圖大小 * ratio
* encoderOption
。
使用FormData
來上傳
const formData = new FormData()
formData.append('file', blob)
複製程式碼
這是XHR
Level2的產物,可以方便的以鍵值對的形式插入。
最大的優勢是可以通過XMLHttpRequest.send()
來非同步提交二進位制檔案。
後期還可以通過Blob
的slice
來擴充套件分片上傳功能。
知識點剖析
FileReader和URL.createObjectURL的區別
關於FileReader
和URL.createObjectURL
的用法就不詳細介紹了,感興趣的自行google。
我們現在只需要知道
-
通過
FileReader.readAsDataURL(file)
可以獲取一段data:base64
的字串 -
通過
URL.createObjectURL(blob)
可以獲取當前檔案的一個記憶體URL
既然這兩個API都可以滿足我們獲取圖片地址的需求,那它們之間的區別在哪呢?
1、執行時機
createObjectURL
是同步執行(立即的)FileReader.readAsDataURL
是非同步執行(過一段時間)
2、記憶體使用
createObjectURL
返回一段帶hash
的url
,並且一直儲存在記憶體中,直到document
觸發了unload
事件(例如:document close
)或者執行revokeObjectURL
來釋放。FileReader.readAsDataURL
則返回包含很多字元的base64
,並會比blob url
消耗更多記憶體,但是在不用的時候會自動從記憶體中清除(通過垃圾回收機制)
3、相容性
createObjectURL
支援從IE10往上的所有現代瀏覽器FileReader.readAsDataURL
同樣支援從IE10往上的所有現代瀏覽器
從上面答案不難看出,兩者的優劣勢
- 使用
createObjectURL
可以節省效能並更快速,只不過需要在不使用的情況下手動釋放記憶體 - 如果不太在意裝置效能問題,並想獲取圖片的
base64
,則推薦使用FileReader.readAsDataURL
參考
相機拍照的圖片會旋轉
從上面的createObjectURL
獲取到圖片的地址後,我們可以插入到頁面元素的background-image
屬性展示這個圖片,PC端模擬器展示沒有問題,但手機真機拍照得到的圖片會有逆時針的90°旋轉。
為什麼從相機拍照獲取的圖片會旋轉呢?
是因為從相機拍照獲取的圖片的EXIF(Exchangeable image file format)會預設設定一個orientation tag
:point_up_2:上圖就是orientation tag
與圖片旋轉角度的對應關係
如何解決這個問題呢?
1、獲取圖片的orientation
- 需要考慮相容的話,建議使用Exif.js
- 如果不希望有外部依賴,而且對相容性要求不是那麼高的話,可以利用
DataView
來獲取,詳情見stackoverflow高贊回答
2、根據圖片的orientation
做對應的旋轉
switch (orientation) {
case 2:
// horizontal flip
ctx.translate(width, 0);
ctx.scale(-1, 1);
break;
case 3:
// 180 rotate left
ctx.translate(width, height);
ctx.rotate(Math.PI);
break;
case 4:
// vertical flip
ctx.translate(0, height);
ctx.scale(1, -1);
break;
case 5:
// vertical flip + 90 rotate right
ctx.rotate(0.5 * Math.PI);
ctx.scale(1, -1);
break;
case 6:
// 90 rotate right
ctx.rotate(0.5 * Math.PI);
ctx.translate(0, -height);
break;
case 7:
// horizontal flip + 90 rotate right
ctx.rotate(0.5 * Math.PI);
ctx.translate(width, -height);
ctx.scale(-1, 1);
break;
case 8:
// 90 rotate left
ctx.rotate(-0.5 * Math.PI);
ctx.translate(-width, 0);
break;
複製程式碼
參考
File和Blob的關係?Blob Url和DataURL的區別?DataURL如何轉成Blob?
File和Blob的關係
從input onchange
中返回的圖片物件其實就是一個File
物件。
而Blob
物件是一個用來包裝二進位制檔案的容器,File
繼承於Blob
。
FileReader
是用來讀取記憶體中的檔案的API,支援File
和Blob
兩種格式。
Blob Url和Data URLs的區別
Blob Url
只能在瀏覽器中通過URL.createObjectURL(blob)
建立,當不使用的時候,需要URL.revokeObjectURL(blobURL)
來進行釋放。
可以簡單理解為對應瀏覽器記憶體檔案中的軟連結。該連結只能存在於瀏覽器單一例項或對應會話中(例如:頁面的生命週期)
blobURL = URL.createObjectURL(blob)
// blob:http://localhost:8000/xxxxxxxx
複製程式碼
Data URLs
可以獲取檔案的base64
。
data:[<mediatype>][;base64],<data>
複製程式碼
mediatype
是個 MIME 型別的字串,例如 "image/jpeg
" 表示 JPEG 影像檔案。如果被省略,則預設值為text/plain;charset=US-ASCII
可以通過FileReader.readAsDataURL
獲取
const reader = new FileReader();
reader.addEventListener("load", e => {
const dataURL = e.target.result;
})
reader.readAsDataURL(blob);
複製程式碼
DataURL如何轉成Blob?
function dataURItoBlob(dataURI) {
// convert base64 to raw binary data held in a string
// doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this
var byteString = atob(dataURI.split(',')[1]);
// separate out the mime component
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]
// write the bytes of the string to an ArrayBuffer
var ab = new ArrayBuffer(byteString.length);
// create a view into the buffer
var ia = new Uint8Array(ab);
// set the bytes of the buffer to the correct values
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
// write the ArrayBuffer to a blob, and you're done
var blob = new Blob([ab], {type: mimeString});
return blob;
}
複製程式碼
參考
上傳進度條從一開始上傳就是100%,為什麼?
眾所周知,目前監聽上傳檔案進度的主流方式是使用XHR的onprogress
事件來實現,但是為什麼在我本地除錯上傳的時候,onprogress
只被呼叫了一次呢?
在XHR2中有一個事件物件ProgressEvent
,以下幾種監聽事件都可以獲取到這個物件:
事件名稱 | 觸發時機 |
---|---|
loadstart | 請求發起 |
progress | 傳遞資料 |
abort | 請求被中止(例如,通過abort() 方法來觸發) |
error | 請求失敗 |
load | 請求成功完成後 |
timeout | 在指定時間內,請求超時時觸發 |
loadend | 請求完成後(不論請求成功還是失敗) |
ProgressEvent
的事件迴圈如下:
-
每個請求發起後先觸發
loadstart
,請求完成的flag
為false
-
在請求完成的
flag
設定為true
之前,以50ms的間隔來輪詢觸發progress
事件 -
當請求完成時,請求完成的
flag
為true
,根據請求完成的結果狀態,觸發abort
,error
,load
,timeout
其中之一。 -
請求完成後觸發
loadend
至此,我們就很清楚的知道了,為什麼就算我們在本地上傳,並在progress
的回撥裡console.log
也只執行一次的原因了:本地上傳請求事件小於50ms
只要將network調成slow 3G,並換一張高清畫素的大圖片進行上傳,就可以看到progress
事件會在上傳完成之前以50ms的間隔呼叫。
參考
最後
小小推廣一波基於weui風格實現的移動端vue圖片上傳元件vux-uploader-component
歡迎掃碼體驗,Star, Issue, PR:)