基於 vue-upload-component 封裝一個圖片上傳組件

需求分析

業務要求,須要一個圖片上傳控件,需知足html

  • 多圖上傳
  • 點擊預覽
  • 圖片前端壓縮
  • 支持初始化數據

相關功能及資源分析

  • 基本功能
    先到https://www.npmjs.com/search?q=vue+upload上搜索有關上傳的控件,沒有徹底知足需求的組件,過濾後找到 vue-upload-component 組件,功能基本都有,自定義也比較靈活,就以以此進行二次開發。
  • 預覽
    由於項目是基於 vant 作的,自己就提供了 ImagePreview 的預覽組件,使用起來也簡單,若是業務需求須要放大縮小,這個組件就不知足了。
  • 壓縮
    能夠經過 canvas 相關api來實現壓縮功能,還能夠用一些第三方庫來實現, 例如image-compressor.js
  • 數據
    由於表單頁面涉及編輯的狀況,上傳組件爲了展現優雅點,須要作點處理。首先就先要對數據格式和服務端進行約定,而後在處理剩下的

開發

需求和實現思路基本肯定,開始進入編碼,先搭建可運行可測試的環境前端

第一步,建立相關目錄vue

|- components
  |- ImageUpload
    |- ImageUpload.vue
    |- index.js


第二步,安裝依賴git

$ npm i image-compressor.js -S
$ npm i vue-upload-component -S

第三步,編寫核心主體代碼github

// index.js
import ImageUpload from './ImageUpload'
export default ImageUpload
// ImageUpload.vue 
<template>
  <div class="m-image-upload">
    <!--
      這裏分爲兩段遍歷,理由是:在編輯狀況下要默認爲組件添加默認數據,雖說組件提供了 `add` 方法,
        但在編輯狀態下,須要把 url 形式的圖片轉換成 File 以後才能夠添加進去,略微麻煩。
        因此分兩次遍歷,一次遍歷表單對象裏的圖片(直接用img標籤展現,新上傳的圖片能夠經過 blob 來賦值 src),第二次遍歷組件裏的 files
    -->
    <div
      class="file-item"
      v-for="(file, index) in value">
      <img
        :src="file.thumb || file.url"
        @click="preview(index)"
      />
      <van-icon
        name="clear"
        class="close"
        @click="remove(index, true)"/>   <!-- 把圖片從數組中刪除 -->
    </div>
    <div
      :class="{'file-item': true, 'active': file.active, 'error': !!file.error}"
      v-for="(file, index) in files">  <!-- 加幾個樣式來控制 `上傳中` 和 `上傳失敗` 的樣式-->
      <img
        v-if="file.blob"
        :src="file.blob"
      />
      <div class="uploading-shade">
        <p>{{ file.progress }} %</p>
        <p>正在上傳</p>
      </div>
      <div class="error-shade">
        <p>上傳失敗!</p>
      </div>
      <van-icon
        name="clear"
        class="close"
        @click="remove(index)"
      />
    </div>
    <file-upload
      ref="uploader"
      v-model="files"
      multiple
      :thread="10"
      extensions="jpg,gif,png,webp"
      post-action="http://localhost:3000/file/upload"
      @input-file="inputFile"
      @input-filter="inputFilter"
    >
      <van-icon name="photo"/>
    </file-upload>
  </div>
</template>

<script>
  /**
   * 圖片上傳控件
   * 使用方法:
      import ImageUpload from '@/components/ImageUpload'
      ...
      components: {
        ImageUpload
      },
      ...
      <image-upload :value.sync="pics"/>
   */

  import uploader from 'vue-upload-component'
  import ImageCompressor from 'image-compressor.js';
  import { ImagePreview } from 'vant';

  export default {
    name: 'ImageUpload',
    props: {
      value: Array // 經過`.sync`來添加更新值的語法題,經過 this.$emit('update:value', this.value) 來更新
    },
    data() {
      return {
        files: [] // 存放在組件的file對象
      }
    },
    components: {
      'file-upload': uploader
    },
    methods: {
      // 當 add, update, remove File 這些事件的時候會觸發
      inputFile(newFile, oldFile) {
        // 上傳完成
        if (newFile && oldFile && !newFile.active && oldFile.active) {
          // 得到相應數據
          if (newFile.xhr && newFile.xhr.status === 200) {
            newFile.response.data.thumb = newFile.thumb // 把縮略圖轉移
            this.value.push(newFile.response.data) // 把 uploader 裏的文件賦值給 value
            this.$refs.uploader.remove(newFile) // 刪除當前文件對象
            this.$emit('update:value', this.value) // 更新值
          }
        }

        // 自動上傳
        if (Boolean(newFile) !== Boolean(oldFile) || oldFile.error !== newFile.error) {
          if (!this.$refs.uploader.active) {
            this.$refs.uploader.active = true
          }
        }
      },
      // 文件過濾,能夠經過 prevent 來阻止上傳
      inputFilter(newFile, oldFile, prevent) {
        if (newFile && (!oldFile || newFile.file !== oldFile.file)) {
          // 自動壓縮
          if (newFile.file && newFile.type.substr(0, 6) === 'image/') { // && this.autoCompress > 0 && this.autoCompress < newFile.size(小於必定尺寸就不壓縮)
            newFile.error = 'compressing'
            // 壓縮圖片
            const imageCompressor = new ImageCompressor(null, {
              quality: .5,
              convertSize: Infinity,
              maxWidth: 1000,
            })
            imageCompressor.compress(newFile.file).then((file) => {
              // 建立 blob 字段 用於圖片預覽
              newFile.blob = ''
              let URL = window.URL || window.webkitURL
              if (URL && URL.createObjectURL) {
                newFile.blob = URL.createObjectURL(file)
              }
              // 縮略圖
              newFile.thumb = ''
              if (newFile.blob && newFile.type.substr(0, 6) === 'image/') {
                newFile.thumb = newFile.blob
              }
              // 更新 file
              this.$refs.uploader.update(newFile, {error: '', file, size: file.size, type: file.type})
            }).catch((err) => {
              this.$refs.uploader.update(newFile, {error: err.message || 'compress'})
            })
          }
        }
      },
      remove(index, isValue) {
        if (isValue) {
          this.value.splice(index, 1)
          this.$emit('update:value', this.value)
        } else {
          this.$refs.uploader.remove(this.files[index])
        }
      },
      preview(index) {
        ImagePreview({
          images: this.value.map(item => (item.thumb || item.url)),
          startPosition: index
        });
      }
    }
  }
</script>

圖片壓縮也能夠本身來實現,主要是理清各類文件格式的轉換web

compress(imgFile) {
  let _this = this
  return new Promise((resolve, reject) => {
    let reader = new FileReader()
    reader.onload = e => {
      let img = new Image()
      img.src = e.target.result
      img.onload = () => {
        let canvas = document.createElement('canvas')
        let ctx = canvas.getContext('2d')
        canvas.width = img.width
        canvas.height = img.height
        // 鋪底色
        ctx.fillStyle = '#fff'
        ctx.fillRect(0, 0, canvas.width, canvas.height)
        ctx.drawImage(img, 0, 0, img.width, img.height)

        // 進行壓縮
        let ndata = canvas.toDataURL('image/jpeg', 0.3)
        resolve(_this.dataURLtoFile(ndata, imgFile.name))
      }
    }
    reader.onerror = e => reject(e)
    reader.readAsDataURL(imgFile)
  })
}
// base64 轉 Blob
dataURLtoBlob(dataurl) {
  let arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n)
  while (n--) {
    u8arr[n] = bstr.charCodeAt(n)
  }
  return new Blob([u8arr], {type: mime})
},
// base64 轉 File
dataURLtoFile(dataurl, filename) {
  let arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n)
  while (n--) {
    u8arr[n] = bstr.charCodeAt(n)
  }
  return new File([u8arr], filename, {type: mime})
}

最終效果


參考資料

vue-upload-component 文檔npm

相關文章
相關標籤/搜索