圖片上傳那點事

来源:https://www.cnblogs.com/Army-Knife/archive/2019/12/12/12027740.html
-Advertisement-
Play Games

前端圖片上傳那些事兒 本文講的圖片上傳,主要是針對上傳頭像的。大家都知道,上傳頭像一般都會分成以下 4 個步驟: 選擇圖片 -> 預覽圖片 -> 裁剪圖片 -> 上傳圖片 接下來,就詳細的介紹每個步驟具體實現。 選擇圖片 選擇圖片有什麼好講的呢?不就一個 input[type=file] ,然後點擊 ...


前端圖片上傳那些事兒

本文講的圖片上傳,主要是針對上傳頭像的。大家都知道,上傳頭像一般都會分成以下 4 個步驟:

選擇圖片 -> 預覽圖片 -> 裁剪圖片 -> 上傳圖片

接下來,就詳細的介紹每個步驟具體實現。

選擇圖片

選擇圖片有什麼好講的呢?不就一個 input[type=file] ,然後點擊就可以了嗎?確實是這樣的,但是,我們想要做得更加的友好一些,比如需要過濾掉非圖片文件, 或只允許從攝像頭拍照獲取圖片等,還是需要進行一些簡單配置的。

下麵就先來看看最簡單的選擇圖片:

<input type="file" />

這時候,點擊這個 input , 在 iOS 手機的顯示如下:

其中的 “瀏覽” 選項,可以查看到非圖片類型的文件,這並不是我們想要的結果,畢竟我們只想要圖片類型。可以通過 accept 屬性來實現,如下:

<input type="file" accept="image/*">

這樣就可以過濾掉非圖片類型了。但是圖片的類型可能也太多了, 有些可能伺服器不支持,所以,如果想保守一些,只允許 jpg 和 png 類型,可以寫成這樣:

<input type="file" accept="image/jpg, image/jpeg, image/png">

或:

<input type="file" accept=".jpg, .jpeg, .png">

OK, 過濾非圖片的需求搞定了。但是有時候 ,產品還要求只能從攝像頭採集圖片,比如需要上傳證件照,防止從網上隨便找別人的證件上傳,那 capture 屬性就可以派上用場了:

<input type="file" accept="image/*" capture>

這時候,就不能從文件系統中選擇照片了,只能從攝像頭採集。到了這一步,可能覺得很完美了,但是還有個問題,可能有些變態產品要求預設打開前置攝像頭採集圖片,比如就是想要你的自拍照片。 capture 預設調用的是後置攝像頭。預設啟用前置攝像頭可以設置 capture="user" ,如下:

<input type="file" accept="image/*" capture="user">

好啦,關於選擇圖片的就講麽這麼多了,有個註意的地方是,可能有些配置在相容性上會有一些問題,所以需要在不同的機型上測試一下看看效果。

下麵再來談談預覽圖片的實現。

預覽圖片

在遠古時代,前端並沒有預覽圖片的方法。當時的做法時,用戶選擇圖片之後,立刻把圖片上傳到伺服器,然後伺服器返回遠程圖片的 url 給前端顯示。這種方法略顯麻煩,而且會浪費用戶的流量,因為用戶可能還沒有確定要上傳,你卻已經上傳了。幸好,遠古時代已經離我們遠去了,現代瀏覽器已經實現了前端預覽圖片的功能。常用的方法有兩個,分別是 URL.createObjectURL()  FileReader 。雖然他們目前均處在 w3c 規範中的 Working Draft 階段, 但是大多數的現代瀏覽器都已經良好的支持了。 下麵就介紹一下如何使用這兩個方法。

1. 使用 URL.createObjectURL 預覽

URL.createObjectURL() 靜態方法會創建一個 DOMString,其中包含一個表示參數中給出的對象的 URL。這個 URL 的生命周期和創建它的視窗中的 document 綁定。這個新的URL 對象表示指定的 File 對象或 Blob 對象。用法用下:

objectURL = URL.createObjectURL(object);

其中, object 參數指 用於創建 URL 的 File 對象、Blob 對象或者 MediaSource 對象。

對於我們的 input[type=file] 而言, input.files[0] 可以獲取到當前選中文件的 File 對象。示例代碼如下:

 <input id="inputFile" type="file" accept="image/*">
  <img src="" id="previewImage" alt="圖片預覽">
  <script>
    const $ = document.getElementById.bind(document);
    const $inputFile = $('inputFile');
    const $previewImage = $('previewImage');
    $inputFile.addEventListener('change', function() {
      const file = this.files[0];
      $previewImage.src = file ? URL.createObjectURL(file) : '';
    }, this);
  </script>

具體用法可以參考 MDN上的 URL.createObjectURL()

2. 使用 FileReader 預覽

FileReader 對象允許Web應用程式非同步讀取存儲在用戶電腦上的文件(或原始數據緩衝區)的內容,使用 File 或 Blob 對象指定要讀取的文件或數據。同理的,我們也可以通過 input.files[0] 獲取到當前選中的圖片的 File對象。

特別註意,FileReader 和 是非同步讀取文件或數據的!

下麵是使用 FileReader 預覽圖片的示例:

<input id="inputFile" type="file" accept="image/*">
  <img src="" id="previewImage" alt="圖片預覽">
  <script>
    const $ = document.getElementById.bind(document);
    const $inputFile = $('inputFile');
    const $previewImage = $('previewImage');
    $inputFile.addEventListener('change', function() {
      const file = this.files[0];
      const reader = new FileReader();
      reader.addEventListener('load', function() {
        $previewImage.src = reader.result;
      }, false);
      
      if(file) {
        reader.readAsDataURL(file);
      }
    }, false)
  </script>

會發現, FileReader 會相對複雜一些.

更多關於 FileReader 的用法 ,可以參考 MDN 文檔 FileReader

兩種方法的對比

我個人更加傾向於使用 URL.createObjectURL() 。主要原先它的 API 簡潔,同步讀取,並且他返回的是一個 URL ,比 FileReaer 返回的base64 更加精簡。相容性上,兩者都差不多,都是在 WD 的階段。性能上的對比, 在 chrome 上, 選擇了一張 2M 的圖片, URL.createObjectURL() 用時是 0 , 而 FileReader 用時 20ms 左右。 0 感覺不太合理,雖然這個方法立刻就會返回一個 URL ,但是我猜測實際上這個 URL 指定的內容還沒有生成好,應該是非同步生成的,然後才渲染出來的。所以並沒有很好的辦法來對比他們的性能。

如果想要學習更多關於圖片預覽,可以閱讀以下兩篇文章:

裁剪圖片

關於圖片的裁剪,很自然的會想到使用 canvas ,確實是要通過 canvas, 但是如果全部我們自己來實現,可能需要做比較多的工作,所以為了省力,我們可以站在巨人的肩膀上。比較優秀的圖片裁剪庫是 cropperjs , 該庫可以對圖片進行縮放、移動和旋轉。

cropperjs 的詳細配置這裡就不展開了 ,需要的可以自己去看文檔就好。下麵我們就以這個庫為基礎,實現一個裁剪人臉的例子:

<input id="inputFile" type="file" accept="image/*">
  <img class="preview-image" id="previewImage" src="" alt="">
  <!-- cropper裁剪框 -->
  <div class="cropper" id="cropper">
    <div class="inner">
      <div class="face-container">
        <img class="cropper-image" id="cropperImage">
      </div>
      <div class="tips">請將面部區域置於人臉框架內</div>
      <div class="toolbar">
        <div class="btn" id="confirm">確認</div>
      </div>
    </div>
  </div>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.6/cropper.min.css">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.6/cropper.min.js"></script>
 
 <style>
  .preview-image,
  .cropper-image {
    max-width: 100%;
  }

  .cropper {
    display: none;
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    background: #ccc;
    font-size: 0.27rem;
    text-align: center;
  }

  .inner {
      height: 100%;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
    }

    .face-container {
      position: relative;
      width: 320px;
      height: 320px;
      margin: 50px  auto;
    }

    .cropper-modal {
      background: url('https://ok.166.net/gameyw-misc/opd/squash/20191028/152551-m37snfsyu1.png') center no-repeat;
      background-size: 100% 100%;
      opacity: 1;
    }

    .cropper-bg {
      background: none;
    }

    .cropper-view-box {
      opacity: 0;
    }

    .tips {
      font-size: 16px;
    }

    .toolbar {
      display: flex;
      justify-content: center;
      margin: 50px 0;
    }

    .btn {
      width: 150px;
      line-height: 40px;
      font-size: 20px;
      text-align: center;
      color: #fff;
      background: #007fff;
    }
  </style>

  <script>
  const $ = document.getElementById.bind(document);
  const $cropper = $('cropper');
  const $inputFile = $('inputFile');
  const $previewImage = $('previewImage');
  const $cropperImage = $('cropperImage');
  const $confirmBtn = $('confirm')
  let cropperInstance = null;

  // 選擇圖片後,顯示圖片裁剪框
  $inputFile.addEventListener('change', function() {
    const file = this.files[0];
    if(!file) return;
    $cropperImage.src = URL.createObjectURL(file);
    showCropper();
  }, false);

  // 點擊確認按鈕,將裁剪好的圖片放到 img 標簽顯示。
  $confirmBtn.addEventListener('click', function() {
    const url = cropperInstance.getCroppedCanvas().toDataURL("image/jpeg", 1.0);
    $cropper.style.display = 'none';
    $previewImage.src = url;
  }, false);


  function showCropper() {
    $cropper.style.display = 'block';
    cropperInstance && cropperInstance.destroy();
    cropperInstance = new Cropper($cropperImage, {
      viewMode: 1,
      aspectRatio: 1,
      autoCropArea: 1,
      dragMode: 'move',
      guides: false,
      highlight: false,
      cropBoxMovable: false,
      cropBoxResizable: false
    });
  }
  </script>

 

效果圖如下:

上傳

前面的操作已經完成了圖片上傳前的準備,包括選擇圖片、預覽圖片、編輯圖片等,那接下來就可以上傳圖片了。上面的例子中,使用了 cropperInstance.getCroppedCanvas() 方法來獲取到對應的 canvas 對象 。有了 canvas 對象就好辦了,因為 canvas.toBlob() 方法可以取得相應的 Blob 對象,然後,我們就可以把這個 Blob 對象添加到 FromData 進行無刷新的提交了。大概的代碼如下:

function uploadFile() {
    cropperInstance.getCroppedCanvas().toBlob(function(blob) {
      const formData = new FormData();
      formData.append('avatar', blob);
      fetch('xxxx', {
        method: 'POST',
        body: formData
      });
    });
  }

這段代碼並不能真正執行,因為我們還沒有對應的後端伺服器。如果想要嘗試上傳圖片的朋友,可以參考一下這篇文章 寫給新手前端的各種文件上傳攻略,從小圖片到大文件斷點續傳,由於篇幅原因,這裡就不展開啦。

後記

關於圖片上傳的介紹,差不多不到些結束了。但是之前在 iPhone 和 小米 手機上,遇到一個奇怪的問題: 就是我使用前置攝像頭自拍出來的照片,選擇之後 ,會自逆時針旋轉 90 度,比如像下圖:

拍照的時候明明就是正著拍的,為什麼預覽就會變成橫著了呢?當時第一次遇到這個問題的時候,也覺得好奇怪。後來查了一下,得知這是因為拍照時,相機都會記錄拍照的角度信息,可能 iPhone 前置攝像頭記錄的角度信息和其他的有點不一樣,而 iPhone 自己的相冊在瀏覽照片時,自動糾正了角度 ,而瀏覽器卻沒有糾正,所以才會出現這個旋轉。

為瞭解決這個問題,需要使用 EXIF 這個庫來處理。

我剛剛試了一下,發現我的 iPhone 現在竟然不會有這個問題了,大概是半年前,當時在做一個需求時,自拍的圖片會發生這種旋轉,有可能是 iOS 系統升級後, 已經修複了這個問題。而現在身邊又沒有小米手機, 所以也不好復現。還好,當時我保存了一張會自動旋轉的圖片。大家可以到這裡下載:

https://ok.166.net/gameyw-misc/opd/squash/20191028/170829-f5t38i0d9k.png

這圖片下載後,用電腦的圖片查看器打開是正常的,但是,在瀏覽器中,選擇這個圖片後,使用 URL.createObjectURL()  FileReader 來預覽就會發生旋轉。甚至直接 img 標簽引入也會逆時針旋轉了 90 度,比如:

<img src="https://ok.166.net/gameyw-misc/opd/squash/20191028/170829-f5t38i0d9k.png">

效果如下:

下麵就以這張圖片為例,介紹一下如何使用 EXIF 來檢測圖片角度。關於 EXIF 的詳細用法大家可以到 github 的主頁上查看 https://github.com/exif-js/exif-js

<img id="exifImage" src="https://ok.166.net/gameyw-misc/opd/squash/20191028/170829-f5t38i0d9k.png" alt="">

  <script src="https://cdnjs.cloudflare.com/ajax/libs/exif-js/2.3.0/exif.js"></script>
  <script>
      const $exifImage = document.getElementById('exifImage');
      $exifImage.onload = function() {
        EXIF.getData($exifImage, function() {
          let allMetaData = EXIF.getAllTags(this);
          console.log(allMetaData.Orientation); // 6
        });
      };
  </script>

上面代碼的輸出 allMetaData.Orientation 的結果為 6 , 那 6 到底是什麼意思呢? 可以參考這個篇文章 http://sylvana.net/jpegcrop/exif_orientation.html 裡面有個表格:

如果這個表格看不太懂,再參考一下這篇文章 JPEG Orientation,里有個圖:

可以看出,攝像頭信息是逆時針旋轉了 90 度。那要怎麼糾正呢?就順時針旋轉 90 度抵消掉這個角度就好。

事實上, CropperJS 也會檢測圖片的 EXIF 信息,並且會自動糾正角度的,詳情參考 https://github.com/fengyuanchen/cropperjs#checkorientation
這裡也提到了,但只支持讀取 jpg 圖片的 EXIF 信息,而我們這張圖片是 PNG 所以並不支持。

有個 CSS 屬性叫做 image-orientation , 它有個值叫做 from-image , 就是使用圖片的 EXIF 數據來旋轉的。可惜,目前 chrome 不支持該屬性。有興趣的可以瞭解一下。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 案例:協議按鈕倒計時和禁用 <textarea name="texta" id="" cols="30" rows="10"> 這個世界就是這麼瘋狂,你不同意,我就讓你註冊,秦始皇,打錢 </textarea> <input type="button" value="請仔細閱讀協議(5)" id=" ...
  • 之前學的定時器:setInterval和清除定時器 clearInterval(定時器id); //常用的,反覆的執行 window.setInterval(function () { alert("哈哈"); }, 1000); window.clearInterval(定時器id);//清理定時 ...
  • angular package 1、xlsx npm install xlsx --save 2、file-saver npm install file-saver --save npm install @types/file-saver --save 3、實現導出多個sheet的數據 export ...
  • 在項目的時候一直都是在使用谷歌瀏覽器在調試,後來在現場部署到伺服器上的時候,客戶使用的是IE瀏覽器,版本是11 在測試的過程中,出現幾個問題,雖然是幾個問題,但是問題的原因就是AJAX第一次響應,第二次就不在響應 例如:下拉框的數據第一次載入會通過AJAX調用後端方法,但是第二次之後就不在調用後端 ...
  • part5 課程介紹 另一個定時器 第一個定時器的小案例 練習 封裝動畫函數 勻速的動畫函數,過渡到 >緩動的動畫函數 簡單的輪播圖 左右焦點的輪播圖 無縫連接的輪播圖 輪播圖 重點 三大系列中的第一個系列:offset系列 重點 封裝緩動動畫函數 筋斗雲,固定導航欄 升級4到5個版本 手風琴 旋轉 ...
  • xmind文件如圖所示, 最終生成的數據結構如圖: 2,選擇導出為excel文件,導出的excel文件打開如圖 3,安裝node讀取excel模塊 cnpm i node-xlsx --save 4,使用node-xlsx模塊讀取excel文件,註意文件名不能為中文, 使用示例 var xlsx2j ...
  • 我們在使用git初始化一個項目時,尤其是通過git submodule update --init --remote初始化子模塊時,可能會遇到下麵這個錯誤: 這是由於當你通過HTTPS訪問Git遠程倉庫的時候,如果伺服器上的SSL證書未經過第三方機構認證,git就會報錯。原因是因為未知的沒有簽署過的 ...
  • 案例:美女時鐘 思路: 打開頁面就有圖片按每秒1張的順序輪換,用到了日期對象,獲取小時和秒。 封裝到一個命名函數後,為了使頁面打卡就有圖片的輪換,先調用下f1,再設置setInterval <!DOCTYPE html> <html lang="en"> <head> <meta charset=" ...
一周排行
    -Advertisement-
    Play Games
  • 概述:本文代碼示例演示瞭如何在WPF中使用LiveCharts庫創建動態條形圖。通過創建數據模型、ViewModel和在XAML中使用`CartesianChart`控制項,你可以輕鬆實現圖表的數據綁定和動態更新。我將通過清晰的步驟指南包括詳細的中文註釋,幫助你快速理解並應用這一功能。 先上效果: 在 ...
  • openGauss(GaussDB ) openGauss是一款全面友好開放,攜手伙伴共同打造的企業級開源關係型資料庫。openGauss採用木蘭寬鬆許可證v2發行,提供面向多核架構的極致性能、全鏈路的業務、數據安全、基於AI的調優和高效運維的能力。openGauss深度融合華為在資料庫領域多年的研 ...
  • openGauss(GaussDB ) openGauss是一款全面友好開放,攜手伙伴共同打造的企業級開源關係型資料庫。openGauss採用木蘭寬鬆許可證v2發行,提供面向多核架構的極致性能、全鏈路的業務、數據安全、基於AI的調優和高效運維的能力。openGauss深度融合華為在資料庫領域多年的研 ...
  • 概述:本示例演示了在WPF應用程式中實現多語言支持的詳細步驟。通過資源字典和數據綁定,以及使用語言管理器類,應用程式能夠在運行時動態切換語言。這種方法使得多語言支持更加靈活,便於維護,同時提供清晰的代碼結構。 在WPF中實現多語言的一種常見方法是使用資源字典和數據綁定。以下是一個詳細的步驟和示例源代 ...
  • 描述(做一個簡單的記錄): 事件(event)的本質是一個委托;(聲明一個事件: public event TestDelegate eventTest;) 委托(delegate)可以理解為一個符合某種簽名的方法類型;比如:TestDelegate委托的返回數據類型為string,參數為 int和 ...
  • 1、AOT適合場景 Aot適合工具類型的項目使用,優點禁止反編 ,第一次啟動快,業務型項目或者反射多的項目不適合用AOT AOT更新記錄: 實實在在經過實踐的AOT ORM 5.1.4.117 +支持AOT 5.1.4.123 +支持CodeFirst和非同步方法 5.1.4.129-preview1 ...
  • 總說周知,UWP 是運行在沙盒裡面的,所有許可權都有嚴格限制,和沙盒外交互也需要特殊的通道,所以從根本杜絕了 UWP 毒瘤的存在。但是實際上 UWP 只是一個應用模型,本身是沒有什麼許可權管理的,許可權管理全靠 App Container 沙盒控制,如果我們脫離了這個沙盒,UWP 就會放飛自我了。那麼有沒... ...
  • 目錄條款17:讓介面容易被正確使用,不易被誤用(Make interfaces easy to use correctly and hard to use incorrectly)限制類型和值規定能做和不能做的事提供行為一致的介面條款19:設計class猶如設計type(Treat class de ...
  • title: 從零開始:Django項目的創建與配置指南 date: 2024/5/2 18:29:33 updated: 2024/5/2 18:29:33 categories: 後端開發 tags: Django WebDev Python ORM Security Deployment Op ...
  • 1、BOM對象 BOM:Broswer object model,即瀏覽器提供我們開發者在javascript用於操作瀏覽器的對象。 1.1、window對象 視窗方法 // BOM Browser object model 瀏覽器對象模型 // js中最大的一個對象.整個瀏覽器視窗出現的所有東西都 ...