Vue的移動端多圖上傳插件vue-easy-uploader

原文地址前端

前言

這段時間趕項目,須要用到多文件上傳,用Vue進行前端項目開發。在網上找了很多插件,都不是十分滿意,有的使用起來繁瑣,有的不能適應本項目。就打算本身折騰一下,寫一個Vue的上傳插件,一勞永逸,之後能夠直接使用。vue

目前vue-easy-uploader已上傳到GitHubNPM,使用起來方便簡單,不須要繁瑣的配置便可投入生產,不過須要後端配合,實現上傳接口。git

本項目GitHub地址: https://github.com/quanzaiyu/vue-easy-uploadergithub

本項目NPM地址: https://www.npmjs.com/package/vue-easy-uploadervuex

詳細的使用方法都在倉庫Readme中,就不贅述,這裏談下本插件的設計開發思路。npm

插件介紹

vue-easy-uploader是一個多圖上傳插件。主要特性包括:後端

  • 多文件上傳
  • 上傳圖片預覽
  • 上傳狀態監測
  • 刪除指定圖片
  • 清空圖片
  • 從新上傳

後期版本迭代將不限於圖片,往通用文件上傳進行改進。api

先看看上傳插件使用時候的效果圖:數組

目錄結構

index.js # 主入口文件
store.js # 狀態管理
uploader.vue # 上傳組件

文件解析

index.js

import uploader from './uploader'
import store from './store'

let plugin = {}

plugin.install = function (_Vue, _store) {
  _Vue.component('uploader', uploader)
  _store.registerModule('imgstore', store)
}

export default plugin

這是插件的主入口文件,註冊了全局的上傳組件和狀態管理,使用時只須要在項目入口文件(通常是main.js)中加入如下代碼便可引入此插件:緩存

import Vue from 'vue'
import Vuex from 'vuex'
import uploader from 'vue-easy-uploader'
 
let store = new Vuex.Store({})
Vue.use(uploader, store)

store.js

此文件爲狀態管理配置文件,主要包含三個state:

img_upload_cache # 上傳文件緩存
img_paths # 上傳狀態,包括 ready selected uploading finished
img_status # 上傳後的路徑反饋數組(數據結構爲Set集合)

針對每一個state都有本身的mutation,用於改變state,規範上mutation都須要使用大寫字母加下劃線的形式,本人習慣使用小寫字母,不過都不是原則上的問題。

最重要的一個stateimg_status,用於監視圖片上傳的狀態。包括如下幾個狀態:

ready # 上傳開始前的準備狀態 
selected # 已選擇上傳文件 
uploading # 開始上傳 
finished # 上傳完畢

在組件中能夠經過改變上傳狀態實現文件的上傳,同時也能夠監聽上傳狀態的變化而執行回調。如:

methods: {
  upload () {
    this.$store.commit('set_img_status', 'uploading')
  },
  submit () {
    // some code
  }
}
computed: {
  ...mapState({
    imgStatus: state => state.imgstore.img_status
  })
},
watch: {
  imgStatus () {
    if (this.imgStatus === 'finished') {
      this.submit()
    }
  }
}

上述代碼中,使用upload方法更新了上傳狀態,讓圖片開始執行上傳操做,使用watch進行上傳狀態的監視,當上傳完成(img_status狀態變爲finished),執行回調函數submit

源文件以下:

// Created by quanzaiyu on 2017/10/25 0025.
var state = {
  img_upload_cache: [],
  img_paths: [],
  img_status: 'ready' // 上傳狀態 ready selected uploading finished
}

const actions = {}

const getters = {}

const mutations = {
  set_img_upload_cache (state, arg) {
    state.img_upload_cache = arg
  },
  set_img_paths (state, arg) {
    state.img_paths = arg
  },
  set_img_status (state, arg) {
    state.img_status = arg
  }
}

export default {
  state,
  mutations,
  actions,
  getters
}

uploader.vue

先看源代碼(爲了節省空間,未貼出style部分的代碼):

<template>
  <div class="imgUploader">
    <div class="file-list">
      <section
        v-for="(file, index) in imgStore" :key="index"
        class="file-item draggable-item"
      >
        <img :src="file.src" alt="" ondragstart="return false;">
        <span class="file-remove" @click="remove(index)">+</span>
      </section>
      <section class="file-item" v-if="imgStatus !== 'finished'">
        <div class="add">
          <span>+</span>
          <input type="file" pictype='30010003' multiple
            data-role="none" accept="image/*"
            @change="selectImgs"
            ref="file"
          >
        </div>
      </section>
    </div>
    <div class="uploadBtn">
      <section>
        <span v-if="imgStore.length > 0" class="empty"
          @click="empty">
            {{imgStatus === 'finished' ? '從新上傳' : '清空'}}
        </span>
      </section>
    </div>
  </div>
</template>

<script>
import { mapState } from 'vuex'
export default {
  props: ['url'],
  data () {
    return {
      files: [], // 文件緩存
      index: 0 // 序列號
    }
  },
  computed: {
    ...mapState({
      imgStore: state => state.imgstore.img_upload_cache,
      imgPaths: state => state.imgstore.img_paths,
      imgStatus: state => state.imgstore.img_status
    })
  },
  methods: {
    // 選擇圖片
    selectImgs () { # ①
      let fileList = this.$refs.file.files
      for (let i = 0; i < fileList.length; i++) {
        // 文件過濾
        if (fileList[i].name.match(/.jpg|.gif|.png|.bmp/i)) { 
          let item = {
            key: this.index++,
            name: fileList[i].name,
            size: fileList[i].size,
            file: fileList[i]
          }
          // 將圖片文件轉成BASE64格式
          let reader = new FileReader()  # ②
          reader.onload = (e) => {
            this.$set(item, 'src', e.target.result)
          }
          reader.readAsDataURL(fileList[i])
          this.files.push(item)
          this.$store.commit('set_img_upload_cache', this.files) // 存儲文件緩存
          this.$store.commit('set_img_status', 'selected') // 更新文件上傳狀態
        }
      }
    },
    // 上傳圖片
    submit () {
      let formData = new FormData() # ③
      this.imgStore.forEach((item, index) => {
        item.name = 'imgFiles[' + index + ']' # ④
        formData.append(item.name, item.file)
      })
      formData.forEach((v, k) => console.log(k, ' => ', v))
      // 新建請求
      const xhr = new XMLHttpRequest()  # ⑤
      xhr.open('POST', this.url, true)
      xhr.send(formData)
      xhr.onload = () => {
        if (xhr.status === 200 || xhr.status === 304) {
          let datas = JSON.parse(xhr.responseText)
          console.log('response: ', datas)
          // 存儲返回的地址
          let imgUrlPaths = new Set()   # ⑥
          datas.forEach(e => { // error === 0爲成功狀態
            e.error === 0 && imgUrlPaths.add(e.url)
          })
          this.$store.commit('set_img_paths', imgUrlPaths) // 存儲返回的地址
          this.files = [] // 清空文件緩存
          this.index = 0 // 初始化序列號
          this.$store.commit('set_img_status', 'finished') // 更新文件上傳狀態
        } else {
          alert(`${xhr.status} 請求錯誤!`)
        }
      }
    },
    // 移除圖片
    remove (index) {
      this.files.splice(index, 1)
      this.$store.commit('set_img_upload_cache', this.files) // 更新存儲文件緩存
    },
    // 清空圖片
    empty () {
      this.files.splice(0, this.files.length)
      this.$store.commit('set_img_upload_cache', this.files) // 更新存儲文件緩存
      this.$store.commit('set_img_paths', [])
    }
  },
  beforeCreate () {
    this.$store.commit('set_img_status', 'ready') // 更新文件上傳狀態
  },
  destroyed () {
    this.$store.commit('set_img_upload_cache', [])
    this.$store.commit('set_img_paths', [])
  },
  watch: {
    imgStatus () {
      if (this.imgStatus === 'uploading') {
        this.submit()   # ⑦
      }
    },
    imgStore () {
      if (this.imgStore.length <= 0) {
        this.$store.commit('set_img_status', 'ready') // 更新文件上傳狀態
      }
    }
  }
}
</script>

<style lang="less" scoped>
...
</style>

以上代碼中有一些註釋序號,是此插件設計的主要思路,其餘代碼都比較容易理解,分別說下

  • ① 選擇文件後執行,img_status狀態變爲selected
  • ② 將帶上傳的圖片文件轉化爲Base64格式,用於縮略圖顯示。
  • ③ 建立一個表單對象,用於存儲待上傳的文件。
  • ④ 注意這裏的name屬性值,暫時寫死,後面設計打算從組件中指定name屬性,若是是多文件的話,name屬性的數組序號從0開始遞增。
  • ⑤ 未依賴任何Ajax請求插件,使用原生的XMLHttpRequest對象建立請求。
  • ⑥ 存儲上傳成功後服務器返回的上傳路徑。
  • ⑦ 檢測上傳狀態,當在使用此插件時將img_status的狀態設置爲uploading時執行上傳操做。

使用

參考本項目的GItHubNPM

注意

使用此插件時,須要與後端約定返回的數據格式,以下:

[{"error":0,"url":"\/uploads\/api\/201711\/25\/fde412bd83d3ec5d6a49769bd0c143cd.jpg"},{"error":0,"url":"\/uploads\/api\/201711\/25\/c6fd51f0388c63a0b6d350331c945fb1.jpg"}]

預覽以下:

返回的是一個上傳後的路徑數組,包括errorurl字段,每一個文件有本身的上傳狀態,當error爲0的時候爲上傳成功,並返回上傳後的路徑url

改進

後續版本打算進行以下改進

  1. 把表單的name屬性名稱經過組件傳遞。
  2. 自定義上傳成功後服務器響應的數據格式,好比自定義error的名稱和其值所表示的狀態。
  3. 支持其餘類型文件的上傳,能夠在組件中自行制定上傳的文件類型,及其預覽方式。
相關文章
相關標籤/搜索