Vue圖片裁剪上傳元件

sufaith發表於2019-03-27

本元件基於vuejs框架, 使用ES6基本語法, css預編譯採用的scss, 圖片裁剪模組基於cropperjs,拍照時的圖片資訊獲取使用exif, 圖片上傳使用XMLHttpRequest

該元件已單獨部署上線, 線上地址: upload-img.sufaith.com/, 圖片最終是傳至我個人的七牛雲, 獲取七牛雲上傳憑證token的介面是我單獨做的一個nodejs服務, 可在PC或移動端開啟測試下效果.

涉及到的知識點整理如下:

vuejs 介紹 — Vue.js

scss Sass世界上最成熟、穩定和強大的CSS擴充套件語言 | Sass中文網

cropperjs github.com/fengyuanche…

exif github.com/exif-js/exi…

XMLHttpRequest XMLHttpRequest()


整體專案分成3個檔案:

1. uploadAvator.vue (父元件,用於選擇圖片,接收crop回撥,執行上傳)
2. crop.vue (裁剪元件, 用於裁剪,壓縮,回撥裁剪結果給uploadAvator.vue)
3. image.js (封裝了基本的base64轉換blob、獲取圖片url、xhr上傳、圖片壓縮等方法)複製程式碼


整體流程如下:

  1. input選擇圖片
  2. 呼叫cropperjs裁剪
  3. 修正方向, 壓縮
  4. 上傳


具體實現步驟:

一. 實現input選擇檔案

1. 定義一個隱形樣式的輸入框,用於選擇圖片檔案 (imgUrl初始化為預設圖片地址)

<template>
  <div class="upload-wrapper" :style="{backgroundImage: 'url(' + imgUrl + ')'}">
    <input @change="onChange" class="input" type="file" accept="image/jpg,image/jpeg,image/png,image/gif" multiple=""/>
  </div>
</template>複製程式碼


<style lang="scss" scoped>
.upload-wrapper {
  position: relative;
  width: 77px;
  height: 77px;
  background-size: cover;
  border: 0;
  border-radius: 50%;
  margin: 20px auto;
  .input {
    position: absolute;
    z-index: 1;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    -webkit-tap-highlight-color: rgba(0,0,0,0);
  }
}
</style>複製程式碼

2. 對input選擇圖片做一些優化

(1) 每次點選input選擇圖片時, 彈出選擇檔案的彈窗很慢,有些延遲

解決方案: 明確定義input的accept屬性對應的圖片型別

<input type="file" accept="image/jpg,image/jpeg,image/png,image/gif" multiple=""/>複製程式碼

(2) 在ios裝置下input若含有capture屬性, 則只能調起相簿,而安卓裝置下input若不含capture屬性,則只能調起相簿

解決方案: 判斷是否為ios裝置, 建立對應屬性的input

const UA = navigator.userAgent

const isIpad = /(iPad).*OS\s([\d_]+)/.test(UA)
const isIpod = /(iPod)(.*OS\s([\d_]+))?/.test(UA)
const isIphone = !isIpad && /(iPhone\sOS)\s([\d_]+)/.test(UA)

const isIos = isIpad || isIpod || isIphone
複製程式碼


<input v-if="isIos" @change="onChange" class="input" type="file" accept="image/jpg,image/jpeg,image/png,image/gif" multiple=""/>
<!-- 安卓裝置保留capture屬性 -->
<input v-else @change="onChange" class="input" type="file" accept="image/jpg,image/jpeg,image/png,image/gif" capture="camera" multiple=""/>
    複製程式碼

(3) 再次點選input選擇圖片時, 若選擇的圖片和上一次選擇的圖片相同時,則不會觸發onchange事件

解決方案: 在每次接收到onchange事件時先銷燬當前input, 再重新建立一個input, 此時可利用vue的v-if指令,輕鬆銷燬或重建

<input v-if="isIos && !destroyInput" @change="onChange" class="input" type="file" accept="image/jpg,image/jpeg,image/png,image/gif" multiple=""/>
<!-- 安卓裝置保留capture屬性 -->
<input v-if="!isIos && !destroyInput" @change="onChange" class="input" type="file" accept="image/jpg,image/jpeg,image/png,image/gif" capture="camera" multiple=""/>
    複製程式碼


data() {
    return {
      destroyInput: false, // 是否銷燬input元素, 解決在第二次和第一次選擇的檔案相同時不觸發onchange事件的問題
      isIos: isIos // 是否為ios裝置
    }
  },
複製程式碼
Vue圖片裁剪上傳元件

二. 呼叫cropperjs裁剪

1. 獲取選擇的圖片的url (用於裁剪)

Vue圖片裁剪上傳元件

2. 獲取拍照時的Orientation資訊,解決拍出來的照片旋轉問題

Vue圖片裁剪上傳元件

3.顯示裁剪元件並初始化

Vue圖片裁剪上傳元件
Vue圖片裁剪上傳元件

4. 取消裁剪和開始裁剪

Vue圖片裁剪上傳元件

三. 修正方向, 壓縮並將base64回撥給父元件

Vue圖片裁剪上傳元件
const image = {}

image.compress = function(img, Orientation) {
  // 圖片壓縮
  // alert('圖片的朝向' + Orientation)
  let canvas = document.createElement('canvas')
  let ctx = canvas.getContext('2d')
  // 瓦片canvas
  let tCanvas = document.createElement('canvas')
  let tctx = tCanvas.getContext('2d')
  let initSize = img.src.length
  let width = img.width
  let height = img.height

  // 如果圖片大於四百萬畫素,計算壓縮比並將大小壓至400萬以下
  let ratio
  if ((ratio = width * height / 4000000) > 1) {
    console.log('大於400萬畫素')
    ratio = Math.sqrt(ratio)
    width /= ratio
    height /= ratio
  } else {
    ratio = 1
  }
  canvas.width = width
  canvas.height = height
  // 鋪底色
  ctx.fillStyle = '#fff'
  ctx.fillRect(0, 0, canvas.width, canvas.height)
  // 如果圖片畫素大於100萬則使用瓦片繪製
  let count
  if ((count = width * height / 1000000) > 1) {
    count = ~~(Math.sqrt(count) + 1) // 計算要分成多少塊瓦片
    // 計算每塊瓦片的寬和高
    let nw = ~~(width / count)
    let nh = ~~(height / count)
    tCanvas.width = nw
    tCanvas.height = nh
    for (let i = 0; i < count; i++) {
      for (let j = 0; j < count; j++) {
        tctx.drawImage(img, i * nw * ratio, j * nh * ratio, nw * ratio, nh * ratio, 0, 0, nw, nh)
        ctx.drawImage(tCanvas, i * nw, j * nh, nw, nh)
      }
    }
  } else {
    ctx.drawImage(img, 0, 0, width, height)
  }
  // 修復ios上傳圖片的時候 被旋轉的問題
  if (Orientation && Orientation !== '' && Orientation !== 1) {
    switch (Orientation) {
      case 6: // 需要順時針(向左)90度旋轉
        image.rotateImg(img, 'left', canvas)
        break
      case 8: // 需要逆時針(向右)90度旋轉
        image.rotateImg(img, 'right', canvas)
        break
      case 3: // 需要180度旋轉
        image.rotateImg(img, 'right', canvas) // 轉兩次
        image.rotateImg(img, 'right', canvas)
        break
    }
  }
  // 設定jpegs圖片的質量
  let ndata = canvas.toDataURL('image/jpeg', 1)
  console.log(`壓縮前:${initSize}`)
  console.log(`壓縮後:${ndata.length}`)
  console.log(`壓縮率:${~~(100 * (initSize - ndata.length) / initSize)}%`)
  tCanvas.width = tCanvas.height = canvas.width = canvas.height = 0
  return ndata
}

image.rotateImg = function(img, direction, canvas) {
  // 圖片旋轉
  // 最小與最大旋轉方向,圖片旋轉4次後回到原方向
  const minStep = 0
  const maxStep = 3
  if (img == null) return
  // img的高度和寬度不能在img元素隱藏後獲取,否則會出錯
  let height = img.height
  let width = img.width
  let step = 2
  if (step == null) {
    step = minStep
  }
  if (direction === 'right') {
    step++
    // 旋轉到原位置,即超過最大值
    step > maxStep && (step = minStep)
  } else {
    step--
    step < minStep && (step = maxStep)
  }
  // 旋轉角度以弧度值為引數
  let degree = step * 90 * Math.PI / 180
  let ctx = canvas.getContext('2d')
  switch (step) {
    case 0:
      canvas.width = width
      canvas.height = height
      ctx.drawImage(img, 0, 0)
      break
    case 1:
      canvas.width = height
      canvas.height = width
      ctx.rotate(degree)
      ctx.drawImage(img, 0, -height)
      break
    case 2:
      canvas.width = width
      canvas.height = height
      ctx.rotate(degree)
      ctx.drawImage(img, -width, -height)
      break
    case 3:
      canvas.width = height
      canvas.height = width
      ctx.rotate(degree)
      ctx.drawImage(img, -width, 0)
      break
  }
}
export default image
複製程式碼

四. 上傳圖片

1. base64轉換為檔案

Vue圖片裁剪上傳元件

2. XMLHttpRequest上傳

Vue圖片裁剪上傳元件

3. 定義上傳狀態的樣式,包括上傳進度和上傳失敗的標識

Vue圖片裁剪上傳元件

4.父元件接收到裁剪元件的回撥的base64後,執行上傳

<template>
<div>
  <div class="upload-wrapper" :class="{'upload-status-bg': showStatusWrapper}" :style="{backgroundImage: 'url(' + imgUrl + ')'}">
    <input v-if="isIos && !destroyInput" @change="onChange" class="input" type="file" accept="image/jpg,image/jpeg,image/png,image/gif" multiple=""/>
    <!-- 安卓裝置保留capture屬性 -->
    <input v-if="!isIos && !destroyInput" @change="onChange" class="input" type="file" accept="image/jpg,image/jpeg,image/png,image/gif" capture="camera" multiple=""/>
    <!-- 上傳狀態 -->
    <div v-if="showStatusWrapper" class="upload-status-wrapper">
      <i class="fail" v-if="showStatusFail">!</i>
      <i v-else>{{procent}}%</i>
    </div>
  </div>
  <crop ref="cropWrapper" v-show="showCrop" @hide="showCrop=false" @finish="setUpload"></crop>
</div>
</template>
複製程式碼
Vue圖片裁剪上傳元件
Vue圖片裁剪上傳元件


相關文章