用戶圖片上傳思路:

1.點擊上傳,通過一個input type="file"選擇你要上傳的圖片
2.點擊確定,馬上上傳
3.發(fā)送一個post請求給服務(wù)器
4.得到一個響應(yīng) url(以:http://cdn.lifa.com/1.png)為例,然后把這個url放到頁面中一個隱藏的input中,作為這個input的value
5.預(yù)覽
6.保存(去你上面存的那個隱藏的input中去取url,把這個url存到數(shù)據(jù)庫中)
功能

api設(shè)計
<lf-upload accept="image/*" action="http://wanglifa1995.com/upload" name="avatar"
:fileList.sync="fileList"
>
<button>上傳</button>
<div>只能上傳300kb以內(nèi)的png、jpeg文件</div>
</lf-upload>
accept: 支持傳入的文件類型
action: 上傳到的哪個網(wǎng)址
name: 上傳的文件名稱
fileList: 文件上傳成功后的url數(shù)組集合
如何做到瀏覽器把文件傳到你的服務(wù)器
- form表單必須設(shè)置action對應(yīng)你服務(wù)器的路徑,必須設(shè)置method="post" enctype="multipart/form-data"
- 必須指定文件的name
- 自己寫一個server
1). 首先運行npm init -y
2). 安裝express multer和cors
3). 使用express響應(yīng)一個頁面
- index.js
const express = require('express')
const app = express()
app.get('/',(req,res)=>{
res.send('hello')
})
app.listen(3000)
這樣當(dāng)我們打開localhost:3000的時候頁面就會顯示hello
4). 如何實現(xiàn)把用戶上傳的圖片保存下來
- index.js
//把用戶傳來的文件存到我服務(wù)器的yyy目錄下,沒有這個目錄它會自動創(chuàng)建
+ const upload = multer({dest: 'yyy/'})
//下面的single('xxx')里的xxx與你傳來的文件名要一致
app.post('/upload',upload.single('xxx'),(req,res)=>{
console.log(req.file)
res.send('hello')
})
- 前臺頁面代碼
<form action="http://127.0.0.1:3000/upload" method="post" enctype="multipart/form-data">
<input type="file" name="xxx">
<input type="submit">
</form>
運行node控制臺打印出

我們可以通過req.file.filename獲取到上傳成功后的文件名
上面的做法我們無法拿到這個url,因為form表單一旦提交頁面就刷新了,所以我們要通過阻止表單提交的默認(rèn)行為,然后通過ajax提交
let form = document.querySelector('#form')
form.addEventListener('submit',(e)=>{
e.preventDefault()//阻止默認(rèn)行為
let formData = new FormData
let fileInput = document.querySelector('input[name="xxx"]')
//xxx你要添加的文件名,fileInput你要上傳文件的input
formData.append('xxx',fileInput.files[0])
var xhr = new XMLHttpRequest()
xhr.open('POST',form.getAttribute('action'))
//成功后打印出響應(yīng)內(nèi)容
xhr.onload = function(){
console.log(xhr.response)
}
xhr.send(formData)
})
運行上面的代碼會報一個錯誤,因為他不允許你跨域

所以我們需要在node里設(shè)置一個允許跨域的響應(yīng)頭
app.post('/upload',upload.single('xxx'),(req,res)=>{
+ res.set('Access-Control-Allow-Origin','*')
res.send(req.file.filename)
})
實現(xiàn)上傳成功的文件在前臺頁面中顯示(下載你上傳的文件)
我們在ajax請求成功后,給img設(shè)置一個src,路徑是根目錄下的preview里也就是
xhr.onload = function(){
img.src = `http://127.0.0.1:3000/preview/${xhr.response}`
}
在我們的node里我們通過設(shè)置preview這個路徑來下載你上傳的圖片從而在前臺頁面展示
//這里面的:key就是用戶上傳后文件的文件名
app.get('/preview/:key',(req,res)=>{
//通過req.params.key獲取:key
res.sendFile(`yyy/${req.params.key}`,{
root: __dirname, //根目錄是當(dāng)前目錄
headers: {
'Content-Type': 'image/jpeg'
}
},(error)=>{
console.log(error)
})
})
使用cors替代Access-Control-Allow-Origin
在所有需要跨域的域名路徑里添加一個cors就可以
- index.js
const express = require('express')
const multer = require('multer')
const cors = require('cors')
//把用戶傳來的文件存到我服務(wù)器的uploads目錄下,沒有這個目錄它會自動創(chuàng)建
const upload = multer({dest: 'uploads/'})
const app = express()
//options和post都得加cors()
app.options('/upload', cors())
//cors()替代了上面的res.set('Access-Control-Allow-Origin','*')
app.post('/upload', cors(), upload.single('file'),(req,res)=>{
res.send(req.file.filename)
})
app.get('/preview/:key', cors(), (req,res)=>{
res.sendFile(`uploads/${req.params.key}`,{
root: __dirname,
headers: {
'Content-Type': 'image/jpeg'
}
},(error)=>{
console.log(error)
})
})
app.listen(3000)
前臺頁面代碼
<form id="form" action="http://127.0.0.1:3000/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit">
</form>
<img src="" id="img" alt="">
let form = document.querySelector('#form')
console.log(form)
form.addEventListener('submit',(e)=>{
e.preventDefault()
let formData = new FormData
let fileInput = document.querySelector('input[name="file"]')
formData.append('file',fileInput.files[0])
var xhr = new XMLHttpRequest()
xhr.open('POST',form.getAttribute('action'))
xhr.onload = function(){
img.src = `http://127.0.0.1:3000/preview/${xhr.response}`
}
xhr.send(formData)
})
5). 使用heroku當(dāng)做服務(wù)器
因為我們沒法保證我們的server一直在自己的服務(wù)器上開著,所以需要將我們的node代碼上傳到heroku
這里要注意:因為heroku里的端口號是隨機給的,不一定是3000,所以我們的端口號不能寫死,要通過環(huán)境獲取端口號
- index.js
let port = process.env.PORT || 3000
app.listen(port)
然后給package.json中添加一個start命令
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
+ "start": "node index.js"
},

使用heroku必須注意兩點
1.script里必須配置start
2.必須配置環(huán)境端口號
創(chuàng)建upload
思路:當(dāng)我們引入這個組件的時候,用戶自己寫入一個按鈕,點擊彈出選擇文件窗口,我們可以通過slot,把用戶的按鈕放到插槽里,然后點擊按鈕,在它的下面的兄弟元素下創(chuàng)建一個input標(biāo)簽,然后默認(rèn)點擊它,之后監(jiān)聽input的chage事件,拿到對應(yīng)的文件名和相應(yīng)的相應(yīng),發(fā)送ajax請求
- upload.vue
<template>
<div class="lifa-upload">
<div @click="onClickUpload">
<slot></slot>
</div>
<div ref="tmp" style="width: 0;height:0;overflow: hidden;"></div>
</div>
</template>
<script>
export default {
name: "LiFaUpload",
props: {
name: {
type: String,
required: true
},
action: {
type: String,
required: true
},
method: {
type: String,
default: 'post'
}
},
methods: {
onClickUpload(){
let input = document.createElement('input')
input.type= 'file'
this.$refs.tmp.appendChild(input)
input.addEventListener('change',()=>{
let file = input.files[0]
input.remove()
let formData = new FormData()
formData.append(this.name, file)
let xhr = new XMLHttpRequest()
xhr.open(this.method, this.action)
xhr.onload = function () {
console.log(xhr.response);
}
xhr.send(formData)
})
input.click()
}
}
}
</script>
<style scoped>
</style>
初步實現(xiàn)upload
后端給前端的接口返回的必須是JSON格式的字符串,原因是http協(xié)議只支持字符串形式,后端通過JSON.stringify將對象轉(zhuǎn)換為字符串這叫做序列化,前端拿到這個JSON格式的字符串,通過JSON.parse將字符串轉(zhuǎn)成對象,這叫做反序列化
- index.js
app.post('/upload', cors(), upload.single('file'),(req,res)=>{
let fileAttr = req.file
let object = {id:fileAttr.filename}
res.send(JSON.stringify(object))
})
- upload.vue
xhr.onload = ()=> {
let {id, name, type, size} = JSON.parse(xhr.response)
let url = `http://127.0.0.1:3000/preview/${id}`
}
上面的代碼的問題我們的upload組件必須得接受一個JSON格式的字符串,然后對它反序列化,但我們沒法保證用戶用的是JSON格式,他有可能不用JSON格式,所以我們不能在onload里寫上面兩句代碼,要讓用戶去寫,然后通過props接受傳進(jìn)來的這個parseResponse的函數(shù)
<lf-upload accept="image/*" action="http://127.0.0.1:3000/upload" name="file"
:fileList.sync="fileList" :parse-response="parseResponse"
>
</lf-upload>
methods: {
parseResponse(response){
let {id} = JSON.parse(response)
let url = `http://127.0.0.1:3000/preview/${id}`
return url
}
}
- upload.vue
props: {
parseResponse: {
type: Function,
required: true
}
}
xhr.onload = ()=> {
this.url = this.parseResponse(xhr.response)
}
對代碼進(jìn)行重構(gòu)
data(){
return {
url: 'about:blank'
}
},
methods: {
onClickUpload(){
let input = this.createInput()
input.addEventListener('change',()=>{
let file = input.files[0]
input.remove()
this.updateFile(file)
})
input.click()
},
createInput(){
let input = document.createElement('input')
input.type= 'file'
this.$refs.tmp.appendChild(input)
return input
},
updateFile(file){
let formData = new FormData()
formData.append(this.name, file)
this.doUploadFile(formData,(response)=>{
let url = this.parseResponse(response)
this.url = url
})
},
doUploadFile(formData,success){
let xhr = new XMLHttpRequest()
xhr.open(this.method, this.action)
xhr.onload = ()=>{
success(xhr.response)
}
xhr.send(formData)
}
}
使用一個fileList對每次上傳的文件信息進(jìn)行存儲
<ol>
<li v-for="file in fileList" :key="file.name">
<img :src="file.url" :alt="file.name" width="80" height="80">
{{file.name}}
</li>
</ol>
fileList: {
type: Array,
default: ()=>[]
},
methods: {
updateFile(file){
let formData = new FormData()
formData.append(this.name, file)
let {name,size,type}=file
this.doUploadFile(formData,(response)=>{
let url = this.parseResponse(response)
this.url = url
this.$emit('update:fileList',[...this.fileList,{name,size,type,url}])
})
},
}
上面的代碼,因為有可能你每次上傳的圖片的name都是一樣的,但是我們綁定的key必須得是唯一值,所以當(dāng)你上傳同一張圖片就會報錯,解決辦法:
- 強制規(guī)定每一個上傳的文件都必須返回一個唯一的id
- 每次判斷fileList數(shù)組里的每一項里是否有當(dāng)前name,有的話就在現(xiàn)在的name后面加一個(1)
this.doUploadFile(formData,(response)=>{
let url = this.parseResponse(response)
this.url = url
+ while(this.fileList.filter(n=>n.name === name).length > 0){
let division = name.lastIndexOf('.')
let start = name.substring(0,division)
let end = name.substring(division)
start+= '(1)'
name = start+end
}
this.$emit('update:fileList',[...this.fileList,{name,size,type,url}])
})
效果如下:

實現(xiàn)刪除功能
<li v-for="(file,index) in fileList" :key="file.name">
<img :src="file.url" :alt="file.name" width="80" height="80">
{{file.name}}
<span @click="onRemoveFile(index)">x</span>
</li>
onRemoveFile(index){
let copy = JSON.parse(JSON.stringify(this.fileList))
let confirm = window.confirm('你確定要刪除嗎?')
if(confirm){
copy.splice(index,1)
this.$emit('update:fileList',copy)
}
}
顯示上傳中
思路:定義兩個鉤子函數(shù)一個是上傳成功后(afterUploadFile)觸發(fā),一個是上傳時(beforeUploadFile)觸發(fā),在beforeUPloadFIle里給fileList中添加一個status屬性為uploading,然后成功后我們先通過唯一的name在fileList中查找name等于我們現(xiàn)在的name的一項,之后對它進(jìn)行深拷貝然后給這一項添加一個url和status改為success,之后拿到這一項的索引,在對fileList深拷貝后刪除這一項改為修改后的(這里因為要name唯一所以我們需要把修改name的操作放在updateFile最開始的地方)
- upload.vue
<li v-for="(file,index) in fileList" :key="file.name">
<template v-if="file.status === 'uploading'">
菊花
</template>
<img :src="file.url" :alt="file.name" width="80" height="80">
{{file.name}}
<span @click="onRemoveFile(index)">x</span>
</li>
methods: {
updateFile(rawFile){
let {name,size,type}=rawFile
let newName = this.generateName(name)
this.beforeUpdateFile(rawFile,newName)
let formData = new FormData()
formData.append(this.name, rawFile)
this.doUploadFile(formData,(response)=>{
let url = this.parseResponse(response)
this.url = url
this.afterUpdateFile(rawFile,newName,url)
})
},
generateName(name){
while(this.fileList.filter(n=>n.name === name).length > 0){
let dotIndex = name.lastIndexOf('.')
let nameWithoutExtension = name.substring(0,dotIndex)
let extension = name.substring(dotIndex)
//每一次在.前面加一個(1)
name = nameWithoutExtension + '(1)'+extension
}
return name
},
beforeUpdateFile(file,newName){
let {name,size,type}=file
this.$emit('update:fileList',[...this.fileList,{name:newName,type,size,status: 'uploading'}])
},
afterUpdateFile(rawFile,newName,url){
//因為name是唯一的,所以根據(jù)name來獲取這個文件的一些屬性
let file = this.fileList.filter(i=>i.name === newName)[0]
//file是通過fileList獲取的,fileList是props不能直接修改
let fileCopy = JSON.parse(JSON.stringify(file))
let index = this.fileList.indexOf(file)
fileCopy.url = url
fileCopy.status = 'success'
let fileListCopy = JSON.parse(JSON.stringify(this.fileList))
//將數(shù)組中之前的file刪除換成fileCopy
fileListCopy.splice(index,1,fileCopy)
this.$emit('update:fileList',fileListCopy)
},
}
實現(xiàn)上傳失敗
思路:和上面顯示上傳的思路大致相同,通過一個uploadError函數(shù),先通過name查找到當(dāng)前這個上傳的文件,然后對這個file和fileList深拷貝,拿到file在fileList中的索引,拷貝后的fileCopy.status='fail',然后從拷貝后的fileList中刪除這一項,添加fileCopy
uploadError(newName){
let file = this.fileList.filter(f=>f.name === newName)[0]
console.log(file);
console.log('this.fileList.length');
console.log(this.fileList.length);
let index = this.fileList.indexOf(file)
let fileCopy = JSON.parse(JSON.stringify(file))
fileCopy.status = 'fail'
let fileListCopy = JSON.parse(JSON.stringify(this.fileList))
fileListCopy.splice(index,1,fileCopy)
console.log(fileListCopy);
this.$emit('update:fileList',fileListCopy)
},
doUploadFile(formData,success,fail){
fail()
let xhr = new XMLHttpRequest()
xhr.open(this.method, this.action)
xhr.onload = ()=>{
success(xhr.response)
}
xhr.send(formData)
},
運行上面的代碼我們發(fā)現(xiàn)當(dāng)我們上傳的時候會報錯,我們在控制臺打印出file和fileList.length發(fā)現(xiàn)分別是undefined和0,可我們在父組件中監(jiān)聽的update:fileList卻是拿到的fileList.length為1

原因:vue的事件是同步的,你觸發(fā)一個事件,父組件會馬上得到這個事件,父組件得到這個事件后會去創(chuàng)造一個異步的ui更新任務(wù)(重新渲染頁面)
一下圖為例:

上圖中我們的fileList就是父組件傳給子組件的props,實際上它是一個數(shù)組,當(dāng)用戶點擊上傳的時候,我們不會去改變原來的filList,而是直接拷貝一個對這個拷貝的去添加一項,然后把這個拷貝后的重新賦給父組件的fileList(這個過程是同步的);父組件拿到新的fileList它不會去馬上傳給子組件,也就是這時候我們在子組件中通過this.fileList拿到的任然是舊的fileList,只有當(dāng)我們子組件重新渲染的時候才會去把新的fileList傳給子組件(父組件給子組件傳遞數(shù)據(jù)的過程是異步的)
解決方法:直接在異步中調(diào)用
doUploadFile(formData,success,fail){
let xhr = new XMLHttpRequest()
xhr.open(this.method, this.action)
xhr.onload = ()=>{
//success(xhr.response)
fail()
}
xhr.send(formData)
},
解決用戶取消選中時每次dom里多一個input的bug
思路:在每次創(chuàng)建input的時候先清空里面的input
this.$refs.tmp.innerHTML = ''
拋出失敗后對應(yīng)的提示
思路:再上傳文件失敗的函數(shù)中觸發(fā)一個error事件把信息傳出去,父組件監(jiān)聽這個error,拿到對應(yīng)的信息,同時失敗的回調(diào)還得傳入每次的請求數(shù)據(jù)
- 實現(xiàn)斷網(wǎng)狀態(tài)下提示網(wǎng)絡(luò)無法連接
主要是通過請求的狀態(tài)碼為0,判斷
this.doUploadFile(formData, (response) => {
let url = this.parseResponse(response)
this.url = url
this.afterUpdateFile(rawFile, newName, url)
}, (xhr) => {
this.uploadError(xhr,newName)
})
uploadError(xhr,newName) {
+ let error = ''
+ if(xhr.status === 0){
+ error = '網(wǎng)絡(luò)無法連接'
+ }
+ this.$emit('error',error)
},
doUploadFile(formData, success, fail) {
let xhr = new XMLHttpRequest()
xhr.open(this.method, this.action)
xhr.onload = () => {
success(xhr.response)
}
+ xhr.onerror = () => {
fail(xhr)
}
xhr.send(formData)
},
<lf-upload @error="alert">
</lf-upload>
alert(error){
window.alert(error || '上傳失敗')
}
- 文件尺寸不得超出的提示
思路:在文件上傳前的函數(shù)里判斷尺寸是否大于我們限定的,如果大于就出發(fā)error,返回false,然后把圖片不能大于的信息傳進(jìn)去,否則就觸發(fā)update:fileList,返回true;之后如果圖片信息不符我們就不能接著上傳,所以我們要在更新文件中通過判定這個上傳前的返回值是否為true,如果不為true就直接return不繼續(xù)下面的上傳操作
updateFile(rawFile) {
+ if(!this.beforeUpdateFile(rawFile, newName)){return}
let formData = new FormData()
formData.append(this.name, rawFile)
this.doUploadFile(formData, (response) => {
let url = this.parseResponse(response)
this.url = url
this.afterUpdateFile(rawFile, newName, url)
}, (xhr) => {
this.uploadError(xhr,newName)
})
},
beforeUpdateFile(file, newName) {
let {name, size, type} = file
if(size > this.sizeLimit){
this.$emit('error',`文件大小不能超過${this.sizeLimit}`)
return false
}else{
this.$emit('update:fileList', [...this.fileList, {name: newName, type, size, status: 'uploading'}])
return true
}
},
實現(xiàn)支持多文件上傳
思路:首先需要給上傳時候的input添加一個 input.multiple = true,然后在把獲取的files傳進(jìn)去,在uplodFile里對files進(jìn)行遍歷,拿到每一個file,對每一個file分別執(zhí)行單文件操作
onClickUpload() {
let input = this.createInput()
input.addEventListener('change', () => {
let files = input.files
input.remove()
this.uploadFile(files)
})
input.click()
},
uploadFile(rawFiles) {
Array.from(rawFiles).forEach(rawFile=>{
let {name, size, type} = rawFile
let newName = this.generateName(name)
if(!this.beforeuploadFile(rawFile, newName)){return}
let formData = new FormData()
formData.append(this.name, rawFile)
this.doUploadFile(formData, (response) => {
let url = this.parseResponse(response)
this.url = url
this.afteruploadFile(rawFile, newName, url)
}, (xhr) => {
this.uploadError(xhr,newName)
})
})
},
問題:上面的代碼雖然可以同時上傳多個,而且請求也會請求多個,但是最后只會顯示一個

我們在文件上傳前和上傳后分別打出this.fileList發(fā)現(xiàn)每次更新前是我們需要的每個文件的信息,而成功后就只有最后一個的了

實際上我們上面代碼中的問題就可以看成下面的
<div id="app">
{{msg}}
<my-one :msg="msg" @x="handle"></my-one>
</div>
<script>
new Vue({
el: '#app',
data: {
msg: []
},
components: {
'my-one': {
template: `<button @click="y">click</button>`,
props: ['msg'],
methods: {
y(){
this.$emit('x',[...this.msg,1])
this.$emit('x',[...this.msg,2])
this.$emit('x',[...this.msg,3])
}
}
},
},
methods: {
handle(val){
this.msg = val
}
}
})
</script>
上面的代碼我們點擊的時候不是把當(dāng)前的數(shù)組先變成[1,2,3]而是直接變成[3]
解決辦法:不要每次整體替換,而是每次觸發(fā)事件的時候把當(dāng)前元素傳給父元素,然后父元素再將當(dāng)前元素push進(jìn)去
<script>
new Vue({
el: '#app',
data: {
msg: []
},
components: {
'my-one': {
template: `<button @click="y">click</button>`,
props: ['msg'],
methods: {
y(){
this.$emit('x',1)
this.$emit('x',2)
this.$emit('x',3)
}
}
},
},
methods: {
handle(val){
this.msg.push(val)
}
}
})
</script>
將我們的代碼更改為:
- upload.vue
beforeuploadFile(file, newName) {
let {size,type} = file
if(size > this.sizeLimit){
this.$emit('error',`文件大小不能超過${this.sizeLimit}`)
return false
}else{
? this.$emit('addFile',{name: newName, type, size, status: 'uploading'})
return true
}
},
- demo
<lf-upload accept="image/*" action="http://127.0.0.1:3000/upload" name="file"
:file-list.sync="fileList" :parse-response="parseResponse"
@error="error=$event" @addFile="addFile" multiple
>
<lf-button icon="upload">上傳</lf-button>
</lf-upload>
addFile(file){
this.fileList.push(file)
}
上面雖然解決了我們上傳多個只顯示一個的問題,但是還需要用戶手動添加一個addFile事件監(jiān)聽
改進(jìn):把uploadFile里面的循環(huán)分成兩個,添加一個生成newName的循環(huán),然后再次上傳文件前先把所有的文件放到一個數(shù)組里,然后在原來的fileList的基礎(chǔ)上把這個總的數(shù)組合并進(jìn)去,之后作為數(shù)據(jù)傳給父組件
uploadFiles(rawFiles) {
let newNames = []
for(let i = 0;i<rawFiles.length;i++){
let rawFile = rawFiles[i]
let {name,size,type} = rawFile
let newName = this.generateName(name)
newNames[i] = newName
}
if(!this.beforeuploadFiles(rawFiles, newNames)){return}
Array.from(rawFiles).forEach((rawFile,i)=>{
let newName = newNames[i]
let formData = new FormData()
formData.append(this.name, rawFile)
this.doUploadFile(formData, (response) => {
let url = this.parseResponse(response)
this.url = url
this.afteruploadFile(rawFile, newName, url)
}, (xhr) => {
this.uploadError(xhr,newName)
})
})
},
beforeuploadFiles(rawFiles, newNames) {
for(let i = 0;i<rawFiles.length;i++){
let {size,type} = rawFiles[i]
if(size > this.sizeLimit){
this.$emit('error',`文件大小不能超過${this.sizeLimit}`)
return false
}else{
//把所有的文件都放到x這個數(shù)組里
let selectFiles = Array.from(rawFiles).map((rawFile,i)=>{
return {name: newNames[i],type,size,status: 'uploading'}
})
this.$emit('update:fileList',[...this.fileList,...selectFiles])
return true
}
}
},
單元測試
- uplode.spec.js
import chai, {expect} from 'chai'
import sinon from 'sinon'
import sinonChai from 'sinon-chai'
import {mount} from '@vue/test-utils'
import Upload from '@/upload.vue'
chai.use(sinonChai)
describe('Upload.vue', () => {
it('存在.', () => {
expect(Upload).to.exist
})
it('可以上傳一個文件', ()=>{
const wrapper = mount(Upload, {
propsData: {
name: 'file',
action: '/xxx',
parseResponse: ()=>{}
},
slots: {
//構(gòu)造一個按鈕來點擊
default: '<button id="x">click me</button>'
}
})
console.log(wrapper.html())
//點擊當(dāng)前按鈕頁面會多一個input標(biāo)簽,然后會彈出對話框
wrapper.find('#x').trigger('click')
console.log(wrapper.html())
})
})
問題1:我們沒法操作對話框,而我們操作對話框是為了選中文件把文件放到input里面去,所以如果我們能用js把文件放到input中去就可以不操作對話框了,往input里面放文件就是改input.files
let inputWrapper = wrapper.find('input[type="file"]')
let input = inputWrapper.element
//new File接受兩個參數(shù)第一個文件內(nèi)容(必須是數(shù)組),第二個是文件名
let file1 = new File(['xxxx'], 'xxx.txt')
let file2 = new File(['yyyy'], 'yyy.txt')
const data = new DataTransfer()
data.items.add(file1)
data.items.add(file2)
input.files = data.files
如何測試ajax:做一個假的ajax測試請求
新建一個http.js
function core(method, url, options) {
let xhr = new XMLHttpRequest()
xhr.open(method, url)
xhr.onload = () => {
options.success && options.success(xhr.response)
}
xhr.onerror = () => {
options.fail && options.fail(xhr)
}
xhr.send(options.data)
}
export default {
post(url, options) {
return core('post', url, options)
},
get(){}
}
- upload.vue
doUploadFile(formData, success, fail) {
http[this.method.toLowerCase()](this.action,{
success,
fail,
data: formData
})
},
- upload.spec.js
import http from '../../src/http.js'
it('可以上傳一個文件', (done)=>{
// 當(dāng)我們上傳的時候把我們的ajax請求改成自己mock的
http.post = (url, options) => {
setTimeout(()=>{
options.success({id: "123123"})
done()
},1000)
}
const wrapper = mount(Upload, {
propsData: {
name: 'file',
action: '/xxx',
method: 'post',
parseResponse: ()=>{}
},
slots: {
default: '<button id="x">click me</button>'
}
})
上面之所以要單獨在一個對象里寫post方法,是因為如果我們直接寫成一個對象或者函數(shù),那我們更改它,只是更改了引用地址,原來的還是不會變,而我們通過對象里的引用來修改外層引用一直不會變,所以改了里面的引用其他的也會跟著變
上面的代碼運行后發(fā)現(xiàn)會有bug,主要原因是我們在使用組件的時候是通過.sync來更新fileList的,但是我們在做單元測試的時候沒有這一步,所以我們必須手動更新fileList
- upload.spec.js
propsData: {
name: 'file',
action: '/xxx',
method: 'post',
parseResponse: ()=>{},
fileList: []
},
slots: {
default: '<button id="x">click me</button>'
},
listeners: {
'update:fileList': (fileList) => {
wrapper.setProps({fileList})
}
}
檢測上傳loading時顯示的菊花
首先在upload.vue中文件上傳成功后添加一行觸發(fā)uploaded事件的代碼
- upload.vue
afteruploadFile(){
...
this.$emit('uploaded')
}
it('可以上傳一個文件', (done)=>{
http.post = (url, options) => {
setTimeout(()=>{
options.success({id: "123123"})
done()
},1000)
}
const wrapper = mount(Upload, {
propsData: {
name: 'file',
action: '/xxx',
method: 'post',
parseResponse: (response)=>{
let object = JSON.parse(response)
return `/preview/${object.id}`
},
fileList: []
},
slots: {
default: '<button id="x">click me</button>'
},
listeners: {
'update:fileList': (fileList) => {
wrapper.setProps({fileList})
},
//上傳成功
'uploaded': () => {
expect(wrapper.find('use').exists()).to.eq(false)
//第一個fileList里的url就是你上面設(shè)置的
expect(wrapper.props().fileList[0].url).to.eq('/preview/123123')
}
}
})
wrapper.find('#x').trigger('click')
let inputWrapper = wrapper.find('input[type="file"]')
let input = inputWrapper.element
//new File接受兩個參數(shù)第一個文件內(nèi)容(必須是數(shù)組),第二個是文件名
let file1 = new File(['xxxx'], 'xxx.txt')
const data = new DataTransfer()
data.items.add(file1)
input.files = data.files
// 沒上傳成功前顯示菊花
let use = wrapper.find('use').element
expect(use.getAttribute('xlink:href')).to.eq('#i-loading')
})