Vue的移動(dòng)端多圖上傳插件vue-easy-uploader

原文地址

前言

這段時(shí)間趕項(xiàng)目,需要用到多文件上傳,用Vue進(jìn)行前端項(xiàng)目開發(fā)轨功。在網(wǎng)上找了不少插件,都不是十分滿意悬包,有的使用起來繁瑣,有的不能適應(yīng)本項(xiàng)目馍乙。就打算自己折騰一下布近,寫一個(gè)Vue的上傳插件,一勞永逸丝格,以后可以直接使用吊输。

目前vue-easy-uploader已上傳到GitHubNPM,使用起來方便簡單铁追,不需要繁瑣的配置即可投入生產(chǎn)季蚂,不過需要后端配合,實(shí)現(xiàn)上傳接口琅束。

本項(xiàng)目GitHub地址: https://github.com/quanzaiyu/vue-easy-uploader

本項(xiàng)目NPM地址: https://www.npmjs.com/package/vue-easy-uploader

詳細(xì)的使用方法都在倉庫Readme中扭屁,就不贅述,這里談下本插件的設(shè)計(jì)開發(fā)思路涩禀。

插件介紹

vue-easy-uploader是一個(gè)多圖上傳插件料滥。主要特性包括:

  • 多文件上傳
  • 上傳圖片預(yù)覽
  • 上傳狀態(tài)監(jiān)測
  • 刪除指定圖片
  • 清空圖片
  • 重新上傳

后期版本迭代將不限于圖片,往通用文件上傳進(jìn)行改進(jìn)艾船。

先看看上傳插件使用時(shí)候的效果圖:

image

目錄結(jié)構(gòu)

image
index.js # 主入口文件
store.js # 狀態(tài)管理
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

這是插件的主入口文件葵腹,注冊了全局的上傳組件和狀態(tài)管理,使用時(shí)只需要在項(xiàng)目入口文件(一般是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

此文件為狀態(tài)管理配置文件屿岂,主要包含三個(gè)state:

img_upload_cache # 上傳文件緩存
img_paths # 上傳狀態(tài)践宴,包括 ready selected uploading finished
img_status # 上傳后的路徑反饋數(shù)組(數(shù)據(jù)結(jié)構(gòu)為Set集合)

針對每個(gè)state都有自己的mutation,用于改變state爷怀,規(guī)范上mutation都需要使用大寫字母加下劃線的形式阻肩,本人習(xí)慣使用小寫字母,不過都不是原則上的問題运授。

最重要的一個(gè)stateimg_status烤惊,用于監(jiān)視圖片上傳的狀態(tài)乔煞。包括以下幾個(gè)狀態(tài):

ready # 上傳開始前的準(zhǔn)備狀態(tài) 
selected # 已選擇上傳文件 
uploading # 開始上傳 
finished # 上傳完畢 

在組件中可以通過改變上傳狀態(tài)實(shí)現(xiàn)文件的上傳,同時(shí)也可以監(jiān)聽上傳狀態(tài)的變化而執(zhí)行回調(diào)柒室。如:

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方法更新了上傳狀態(tài),讓圖片開始執(zhí)行上傳操作雄右,使用watch進(jìn)行上傳狀態(tài)的監(jiān)視空骚,當(dāng)上傳完成(img_status狀態(tài)變?yōu)?code>finished),執(zhí)行回調(diào)函數(shù)submit不脯。

源文件如下:

// Created by quanzaiyu on 2017/10/25 0025.
var state = {
  img_upload_cache: [],
  img_paths: [],
  img_status: 'ready' // 上傳狀態(tài) 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

先看源代碼(為了節(jié)省空間府怯,未貼出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]
          }
          // 將圖片文件轉(zhuǎn)成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) // 存儲(chǔ)文件緩存
          this.$store.commit('set_img_status', 'selected') // 更新文件上傳狀態(tài)
        }
      }
    },
    // 上傳圖片
    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)
          // 存儲(chǔ)返回的地址
          let imgUrlPaths = new Set()   # ⑥
          datas.forEach(e => { // error === 0為成功狀態(tài)
            e.error === 0 && imgUrlPaths.add(e.url)
          })
          this.$store.commit('set_img_paths', imgUrlPaths) // 存儲(chǔ)返回的地址
          this.files = [] // 清空文件緩存
          this.index = 0 // 初始化序列號
          this.$store.commit('set_img_status', 'finished') // 更新文件上傳狀態(tài)
        } else {
          alert(`${xhr.status} 請求錯(cuò)誤!`)
        }
      }
    },
    // 移除圖片
    remove (index) {
      this.files.splice(index, 1)
      this.$store.commit('set_img_upload_cache', this.files) // 更新存儲(chǔ)文件緩存
    },
    // 清空圖片
    empty () {
      this.files.splice(0, this.files.length)
      this.$store.commit('set_img_upload_cache', this.files) // 更新存儲(chǔ)文件緩存
      this.$store.commit('set_img_paths', [])
    }
  },
  beforeCreate () {
    this.$store.commit('set_img_status', 'ready') // 更新文件上傳狀態(tài)
  },
  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') // 更新文件上傳狀態(tài)
      }
    }
  }
}
</script>

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

以上代碼中有一些注釋序號刻诊,是此插件設(shè)計(jì)的主要思路防楷,其他代碼都比較容易理解,分別說下

  • ① 選擇文件后執(zhí)行则涯,img_status狀態(tài)變?yōu)?code>selected复局。
  • ② 將帶上傳的圖片文件轉(zhuǎn)化為Base64格式,用于縮略圖顯示粟判。
  • ③ 創(chuàng)建一個(gè)表單對象亿昏,用于存儲(chǔ)待上傳的文件。
  • ④ 注意這里的name屬性值档礁,暫時(shí)寫死角钩,后面設(shè)計(jì)打算從組件中指定name屬性,如果是多文件的話呻澜,name屬性的數(shù)組序號從0開始遞增递礼。
  • ⑤ 未依賴任何Ajax請求插件,使用原生的XMLHttpRequest對象創(chuàng)建請求羹幸。
  • ⑥ 存儲(chǔ)上傳成功后服務(wù)器返回的上傳路徑脊髓。
  • ⑦ 檢測上傳狀態(tài),當(dāng)在使用此插件時(shí)將img_status的狀態(tài)設(shè)置為uploading時(shí)執(zhí)行上傳操作栅受。

使用

參考本項(xiàng)目的GItHubNPM将硝。

注意

使用此插件時(shí),需要與后端約定返回的數(shù)據(jù)格式屏镊,如下:

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

預(yù)覽如下:

image

返回的是一個(gè)上傳后的路徑數(shù)組依疼,包括errorurl字段,每個(gè)文件有自己的上傳狀態(tài)而芥,當(dāng)error為0的時(shí)候?yàn)樯蟼鞒晒μ喂幔⒎祷厣蟼骱蟮穆窂?code>url

改進(jìn)

后續(xù)版本打算進(jìn)行如下改進(jìn)

  1. 把表單的name屬性名稱通過組件傳遞。
  2. 自定義上傳成功后服務(wù)器響應(yīng)的數(shù)據(jù)格式蔚出,比如自定義error的名稱和其值所表示的狀態(tài)弟翘。
  3. 支持其他類型文件的上傳虫腋,可以在組件中自行制定上傳的文件類型,及其預(yù)覽方式稀余。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末悦冀,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子睛琳,更是在濱河造成了極大的恐慌盒蟆,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件师骗,死亡現(xiàn)場離奇詭異历等,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)辟癌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進(jìn)店門寒屯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人黍少,你說我怎么就攤上這事寡夹。” “怎么了厂置?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵菩掏,是天一觀的道長。 經(jīng)常有香客問我昵济,道長智绸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任访忿,我火速辦了婚禮瞧栗,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘醉顽。我一直安慰自己沼溜,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布游添。 她就那樣靜靜地躺著系草,像睡著了一般。 火紅的嫁衣襯著肌膚如雪唆涝。 梳的紋絲不亂的頭發(fā)上找都,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天,我揣著相機(jī)與錄音廊酣,去河邊找鬼能耻。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的晓猛。 我是一名探鬼主播饿幅,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼戒职!你這毒婦竟也來了栗恩?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤洪燥,失蹤者是張志新(化名)和其女友劉穎磕秤,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體捧韵,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡市咆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了再来。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蒙兰。...
    茶點(diǎn)故事閱讀 40,115評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖其弊,靈堂內(nèi)的尸體忽然破棺而出癞己,到底是詐尸還是另有隱情膀斋,我是刑警寧澤梭伐,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站仰担,受9級特大地震影響糊识,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜摔蓝,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一赂苗、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧贮尉,春花似錦拌滋、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至魏铅,卻和暖如春昌犹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背览芳。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工斜姥, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓铸敏,卻偏偏與公主長得像缚忧,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子杈笔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評論 2 355