H5拍照上傳填坑彙總

逸傑發表於2018-10-29

前言

最近工作一直在使用vue+vux做移動端專案,有一個拍照上傳照片的需求,發現vux裡並沒有實現,調研過非官方的vux-uploader後,感覺還不是很理想。

其實網上已經可以找到很多已經實現的成熟方案,但是在調研這個需求的時候,我發現在各種實現方案中也有一些puzzle的知識點,因此自己動手擼了一個輪子vux-uploader-component,並記錄一二。

需求

元件的互動功能要求如下:

  1. html5呼叫手機相機
  2. 渲染圖片為縮圖
  3. 前端壓縮圖片
  4. 預覽大圖
  5. 刪除當前圖片
  6. 自動上傳

部分關鍵技術點的實現方案

使用html media capture呼叫手機端的相機

<input type="file" accept="image/*"  capture />
複製程式碼

既然是在HTML5規範中,那最關心的問題肯定是相容性了

html-media-capture-caniuse

html-media-capture

可以看到,在大部分的主流平臺,相容性還可以接受,andriod2-4 都支援,只是在ios 6-10支援不太好。

感興趣的可以在自己的手機上測測相容性

html-media-capture-demo

html-media-capture demo

使用ULR.createObjectURL獲取圖片地址

blobURL = ULR.createObjectURL(object)
複製程式碼

object引數可以為FileBlobMediaSource

在這一塊可以衍生出好幾個問題:

  • 已知獲取圖片地址的方法有URL.createObjectURLFileReader.readAsDataURL,那應該使用哪個?為什麼?
  • IOS中拍照獲取的圖片會自動旋轉,為什麼?怎麼解決?
  • FileBlob是什麼關係?Blob URLData 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()來非同步提交二進位制檔案。

後期還可以通過Blobslice來擴充套件分片上傳功能。

知識點剖析

FileReader和URL.createObjectURL的區別

關於FileReaderURL.createObjectURL的用法就不詳細介紹了,感興趣的自行google。

我們現在只需要知道

  • 通過FileReader.readAsDataURL(file)可以獲取一段data:base64的字串

  • 通過URL.createObjectURL(blob)可以獲取當前檔案的一個記憶體URL

既然這兩個API都可以滿足我們獲取圖片地址的需求,那它們之間的區別在哪呢?

1、執行時機

  • createObjectURL是同步執行(立即的)
  • FileReader.readAsDataURL是非同步執行(過一段時間)

2、記憶體使用

  • createObjectURL返回一段帶hashurl,並且一直儲存在記憶體中,直到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

目前只有jpeg格式的圖片會有

orientation

: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,支援FileBlob兩種格式。

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的事件迴圈如下:

  1. 每個請求發起後先觸發loadstart,請求完成的flagfalse

  2. 在請求完成的flag設定為true之前,以50ms的間隔來輪詢觸發progress事件

  3. 當請求完成時,請求完成的flagtrue,根據請求完成的結果狀態,觸發abort,error,load,timeout其中之一。

  4. 請求完成後觸發loadend

至此,我們就很清楚的知道了,為什麼就算我們在本地上傳,並在progress的回撥裡console.log也只執行一次的原因了:本地上傳請求事件小於50ms

只要將network調成slow 3G,並換一張高清畫素的大圖片進行上傳,就可以看到progress事件會在上傳完成之前以50ms的間隔呼叫。

參考

最後

小小推廣一波基於weui風格實現的移動端vue圖片上傳元件vux-uploader-component

vux-uploader-component

歡迎掃碼體驗,Star, Issue, PR:)

相關文章