前言
這段時(shí)間趕項(xiàng)目,需要用到多文件上傳,用Vue進(jìn)行前端項(xiàng)目開發(fā)轨功。在網(wǎng)上找了不少插件,都不是十分滿意悬包,有的使用起來繁瑣,有的不能適應(yīng)本項(xiàng)目馍乙。就打算自己折騰一下布近,寫一個(gè)Vue的上傳插件,一勞永逸丝格,以后可以直接使用吊输。
目前vue-easy-uploader
已上傳到GitHub
和NPM
,使用起來方便簡單铁追,不需要繁瑣的配置即可投入生產(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í)候的效果圖:
目錄結(jié)構(gòu)
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è)state
是img_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í)行上傳操作栅受。
使用
注意
使用此插件時(shí),需要與后端約定返回的數(shù)據(jù)格式屏镊,如下:
[{"error":0,"url":"\/uploads\/api\/201711\/25\/fde412bd83d3ec5d6a49769bd0c143cd.jpg"},{"error":0,"url":"\/uploads\/api\/201711\/25\/c6fd51f0388c63a0b6d350331c945fb1.jpg"}]
預(yù)覽如下:
返回的是一個(gè)上傳后的路徑數(shù)組依疼,包括error
和url
字段,每個(gè)文件有自己的上傳狀態(tài)而芥,當(dāng)error
為0的時(shí)候?yàn)樯蟼鞒晒μ喂幔⒎祷厣蟼骱蟮穆窂?code>url
改進(jìn)
后續(xù)版本打算進(jìn)行如下改進(jìn)
- 把表單的
name
屬性名稱通過組件傳遞。 - 自定義上傳成功后服務(wù)器響應(yīng)的數(shù)據(jù)格式蔚出,比如自定義
error
的名稱和其值所表示的狀態(tài)弟翘。 - 支持其他類型文件的上傳虫腋,可以在組件中自行制定上傳的文件類型,及其預(yù)覽方式稀余。