造輪子-圖片上傳組件

用戶圖片上傳思路:

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

功能
api設(shè)計(jì)
<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: 上傳到的哪個(gè)網(wǎng)址
name: 上傳的文件名稱
fileList: 文件上傳成功后的url數(shù)組集合

如何做到瀏覽器把文件傳到你的服務(wù)器
  1. form表單必須設(shè)置action對(duì)應(yīng)你服務(wù)器的路徑芍耘,必須設(shè)置method="post" enctype="multipart/form-data"
  2. 必須指定文件的name
  3. 自己寫一個(gè)server
    1). 首先運(yùn)行npm init -y
    2). 安裝express multer和cors
    3). 使用express響應(yīng)一個(gè)頁(yè)面
  • index.js
const express = require('express')

const app = express()
app.get('/',(req,res)=>{
    res.send('hello')
})
app.listen(3000)

這樣當(dāng)我們打開(kāi)localhost:3000的時(shí)候頁(yè)面就會(huì)顯示hello
4). 如何實(shí)現(xiàn)把用戶上傳的圖片保存下來(lái)

  • index.js
 //把用戶傳來(lái)的文件存到我服務(wù)器的yyy目錄下斋竞,沒(méi)有這個(gè)目錄它會(huì)自動(dòng)創(chuàng)建
+ const upload = multer({dest: 'yyy/'})
//下面的single('xxx')里的xxx與你傳來(lái)的文件名要一致
app.post('/upload',upload.single('xxx'),(req,res)=>{
    console.log(req.file)
    res.send('hello')
})
  • 前臺(tái)頁(yè)面代碼
<form action="http://127.0.0.1:3000/upload" method="post" enctype="multipart/form-data">
  <input type="file" name="xxx">
  <input type="submit">
</form>

運(yùn)行node控制臺(tái)打印出

我們可以通過(guò)req.file.filename獲取到上傳成功后的文件名

上面的做法我們無(wú)法拿到這個(gè)url,因?yàn)閒orm表單一旦提交頁(yè)面就刷新了鳄袍,所以我們要通過(guò)阻止表單提交的默認(rèn)行為拗小,然后通過(guò)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)
})

運(yùn)行上面的代碼會(huì)報(bào)一個(gè)錯(cuò)誤阅束,因?yàn)樗辉试S你跨域

所以我們需要在node里設(shè)置一個(gè)允許跨域的響應(yīng)頭

app.post('/upload',upload.single('xxx'),(req,res)=>{
+    res.set('Access-Control-Allow-Origin','*')
    res.send(req.file.filename)
})

實(shí)現(xiàn)上傳成功的文件在前臺(tái)頁(yè)面中顯示(下載你上傳的文件)
我們?cè)赼jax請(qǐng)求成功后息裸,給img設(shè)置一個(gè)src,路徑是根目錄下的preview里也就是

xhr.onload = function(){
    img.src = `http://127.0.0.1:3000/preview/${xhr.response}`
  }

在我們的node里我們通過(guò)設(shè)置preview這個(gè)路徑來(lái)下載你上傳的圖片從而在前臺(tái)頁(yè)面展示

//這里面的:key就是用戶上傳后文件的文件名
app.get('/preview/:key',(req,res)=>{
    //通過(guò)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
在所有需要跨域的域名路徑里添加一個(gè)cors就可以

  • index.js
const express = require('express')
const multer = require('multer')
const cors = require('cors')
//把用戶傳來(lái)的文件存到我服務(wù)器的uploads目錄下宿亡,沒(méi)有這個(gè)目錄它會(huì)自動(dòng)創(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)

前臺(tái)頁(yè)面代碼

<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ù)器
因?yàn)槲覀儧](méi)法保證我們的server一直在自己的服務(wù)器上開(kāi)著,所以需要將我們的node代碼上傳到heroku
這里要注意:因?yàn)閔eroku里的端口號(hào)是隨機(jī)給的圈匆,不一定是3000跃赚,所以我們的端口號(hào)不能寫死纬傲,要通過(guò)環(huán)境獲取端口號(hào)

  • index.js
let port = process.env.PORT || 3000
app.listen(port)

然后給package.json中添加一個(gè)start命令

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
+    "start": "node index.js"
  },

使用heroku必須注意兩點(diǎn)

1.script里必須配置start
2.必須配置環(huán)境端口號(hào)

創(chuàng)建upload

思路:當(dāng)我們引入這個(gè)組件的時(shí)候,用戶自己寫入一個(gè)按鈕汁雷,點(diǎn)擊彈出選擇文件窗口侠讯,我們可以通過(guò)slot继低,把用戶的按鈕放到插槽里袁翁,然后點(diǎn)擊按鈕粱胜,在它的下面的兄弟元素下創(chuàng)建一個(gè)input標(biāo)簽焙压,然后默認(rèn)點(diǎn)擊它涯曲,之后監(jiān)聽(tīng)input的chage事件幻件,拿到對(duì)應(yīng)的文件名和相應(yīng)的相應(yīng)绰沥,發(fā)送ajax請(qǐng)求

  • 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>
初步實(shí)現(xiàn)upload

后端給前端的接口返回的必須是JSON格式的字符串零截,原因是http協(xié)議只支持字符串形式涧衙,后端通過(guò)JSON.stringify將對(duì)象轉(zhuǎn)換為字符串這叫做序列化绍撞,前端拿到這個(gè)JSON格式的字符串,通過(guò)JSON.parse將字符串轉(zhuǎn)成對(duì)象非洲,這叫做反序列化

  • 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}`
}

上面的代碼的問(wèn)題我們的upload組件必須得接受一個(gè)JSON格式的字符串两踏,然后對(duì)它反序列化梦染,但我們沒(méi)法保證用戶用的是JSON格式帕识,他有可能不用JSON格式肮疗,所以我們不能在onload里寫上面兩句代碼伪货,要讓用戶去寫碱呼,然后通過(guò)props接受傳進(jìn)來(lái)的這個(gè)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)                   
}

對(duì)代碼進(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)
            }
        }
使用一個(gè)fileList對(duì)每次上傳的文件信息進(jìn)行存儲(chǔ)
<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}])
    })
},
}

上面的代碼,因?yàn)橛锌赡苣忝看紊蟼鞯膱D片的name都是一樣的,但是我們綁定的key必須得是唯一值炭分,所以當(dāng)你上傳同一張圖片就會(huì)報(bào)錯(cuò),解決辦法:

  1. 強(qiáng)制規(guī)定每一個(gè)上傳的文件都必須返回一個(gè)唯一的id
  2. 每次判斷fileList數(shù)組里的每一項(xiàng)里是否有當(dāng)前name呀忧,有的話就在現(xiàn)在的name后面加一個(gè)(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}])
})

效果如下:

實(shí)現(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('你確定要?jiǎng)h除嗎?')
    if(confirm){
        copy.splice(index,1)
        this.$emit('update:fileList',copy)
    }
}
顯示上傳中

思路:定義兩個(gè)鉤子函數(shù)一個(gè)是上傳成功后(afterUploadFile)觸發(fā)泞辐,一個(gè)是上傳時(shí)(beforeUploadFile)觸發(fā)咐吼,在beforeUPloadFIle里給fileList中添加一個(gè)status屬性為uploading锯茄,然后成功后我們先通過(guò)唯一的name在fileList中查找name等于我們現(xiàn)在的name的一項(xiàng)肌幽,之后對(duì)它進(jìn)行深拷貝然后給這一項(xiàng)添加一個(gè)url和status改為success牍颈,之后拿到這一項(xiàng)的索引讥蔽,在對(duì)fileList深拷貝后刪除這一項(xiàng)改為修改后的(這里因?yàn)橐猲ame唯一所以我們需要把修改name的操作放在updateFile最開(kāi)始的地方)

  • 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)
        //每一次在.前面加一個(gè)(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){
    //因?yàn)閚ame是唯一的,所以根據(jù)name來(lái)獲取這個(gè)文件的一些屬性
    let file = this.fileList.filter(i=>i.name === newName)[0]
    //file是通過(guò)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)
},
}
實(shí)現(xiàn)上傳失敗

思路:和上面顯示上傳的思路大致相同芋类,通過(guò)一個(gè)uploadError函數(shù)侯繁,先通過(guò)name查找到當(dāng)前這個(gè)上傳的文件贮竟,然后對(duì)這個(gè)file和fileList深拷貝,拿到file在fileList中的索引惰拱,拷貝后的fileCopy.status='fail'弓颈,然后從拷貝后的fileList中刪除這一項(xiàng)翔冀,添加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)
},

運(yùn)行上面的代碼我們發(fā)現(xiàn)當(dāng)我們上傳的時(shí)候會(huì)報(bào)錯(cuò),我們?cè)诳刂婆_(tái)打印出file和fileList.length發(fā)現(xiàn)分別是undefined和0控硼,可我們?cè)诟附M件中監(jiān)聽(tīng)的update:fileList卻是拿到的fileList.length為1

原因:vue的事件是同步的卡乾,你觸發(fā)一個(gè)事件幔妨,父組件會(huì)馬上得到這個(gè)事件古话,父組件得到這個(gè)事件后會(huì)去創(chuàng)造一個(gè)異步的ui更新任務(wù)(重新渲染頁(yè)面)

一下圖為例:

上圖中我們的fileList就是父組件傳給子組件的props陪踩,實(shí)際上它是一個(gè)數(shù)組肩狂,當(dāng)用戶點(diǎn)擊上傳的時(shí)候婚温,我們不會(huì)去改變?cè)瓉?lái)的filList,而是直接拷貝一個(gè)對(duì)這個(gè)拷貝的去添加一項(xiàng),然后把這個(gè)拷貝后的重新賦給父組件的fileList(這個(gè)過(guò)程是同步的)掺逼;父組件拿到新的fileList它不會(huì)去馬上傳給子組件吕喘,也就是這時(shí)候我們?cè)谧咏M件中通過(guò)this.fileList拿到的任然是舊的fileList氯质,只有當(dāng)我們子組件重新渲染的時(shí)候才會(huì)去把新的fileList傳給子組件(父組件給子組件傳遞數(shù)據(jù)的過(guò)程是異步的)

解決方法:直接在異步中調(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)
},
解決用戶取消選中時(shí)每次dom里多一個(gè)input的bug

思路:在每次創(chuàng)建input的時(shí)候先清空里面的input

this.$refs.tmp.innerHTML = ''
拋出失敗后對(duì)應(yīng)的提示

思路:再上傳文件失敗的函數(shù)中觸發(fā)一個(gè)error事件把信息傳出去,父組件監(jiān)聽(tīng)這個(gè)error辕漂,拿到對(duì)應(yīng)的信息钉嘹,同時(shí)失敗的回調(diào)還得傳入每次的請(qǐng)求數(shù)據(jù)

  1. 實(shí)現(xiàn)斷網(wǎng)狀態(tài)下提示網(wǎng)絡(luò)無(wú)法連接
    主要是通過(guò)請(qǐng)求的狀態(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ò)無(wú)法連接'
+    }
+    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 || '上傳失敗')
}
  1. 文件尺寸不得超出的提示
    思路:在文件上傳前的函數(shù)里判斷尺寸是否大于我們限定的宏蛉,如果大于就出發(fā)error拾并,返回false,然后把圖片不能大于的信息傳進(jìn)去之碗,否則就觸發(fā)update:fileList褪那,返回true博敬;之后如果圖片信息不符我們就不能接著上傳偏窝,所以我們要在更新文件中通過(guò)判定這個(gè)上傳前的返回值是否為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',`文件大小不能超過(guò)${this.sizeLimit}`)
        return false
    }else{
        this.$emit('update:fileList', [...this.fileList, {name: newName, type, size, status: 'uploading'}])
        return true
    }
},
實(shí)現(xiàn)支持多文件上傳

思路:首先需要給上傳時(shí)候的input添加一個(gè) input.multiple = true,然后在把獲取的files傳進(jìn)去括勺,在uplodFile里對(duì)files進(jìn)行遍歷,拿到每一個(gè)file乱豆,對(duì)每一個(gè)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)
        })
    })
},

問(wèn)題:上面的代碼雖然可以同時(shí)上傳多個(gè)瑟啃,而且請(qǐng)求也會(huì)請(qǐng)求多個(gè)蛹屿,但是最后只會(huì)顯示一個(gè)

我們?cè)谖募蟼髑昂蜕蟼骱蠓謩e打出this.fileList發(fā)現(xiàn)每次更新前是我們需要的每個(gè)文件的信息,而成功后就只有最后一個(gè)的了

實(shí)際上我們上面代碼中的問(wè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>

上面的代碼我們點(diǎn)擊的時(shí)候不是把當(dāng)前的數(shù)組先變成[1,2,3]而是直接變成[3]

解決辦法:不要每次整體替換,而是每次觸發(fā)事件的時(shí)候把當(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',`文件大小不能超過(guò)${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)
            }

上面雖然解決了我們上傳多個(gè)只顯示一個(gè)的問(wèn)題,但是還需要用戶手動(dòng)添加一個(gè)addFile事件監(jiān)聽(tīng)
改進(jìn):把uploadFile里面的循環(huán)分成兩個(gè)刃跛,添加一個(gè)生成newName的循環(huán)跌帐,然后再次上傳文件前先把所有的文件放到一個(gè)數(shù)組里谨敛,然后在原來(lái)的fileList的基礎(chǔ)上把這個(gè)總的數(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',`文件大小不能超過(guò)${this.sizeLimit}`)
            return false
        }else{
            //把所有的文件都放到x這個(gè)數(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
        }
    }
},
單元測(cè)試
  • 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('可以上傳一個(gè)文件', ()=>{
        const wrapper = mount(Upload, {
            propsData: {
                name: 'file',
                action: '/xxx',
                parseResponse: ()=>{}
            },
            slots: {
              //構(gòu)造一個(gè)按鈕來(lái)點(diǎn)擊
                default: '<button id="x">click me</button>'
            }
        })
        console.log(wrapper.html())
        //點(diǎn)擊當(dāng)前按鈕頁(yè)面會(huì)多一個(gè)input標(biāo)簽窒所,然后會(huì)彈出對(duì)話框
        wrapper.find('#x').trigger('click')
        console.log(wrapper.html())
    })
})

問(wèn)題1:我們沒(méi)法操作對(duì)話框硝逢,而我們操作對(duì)話框是為了選中文件把文件放到input里面去仑乌,所以如果我們能用js把文件放到input中去就可以不操作對(duì)話框了,往input里面放文件就是改input.files

let inputWrapper =  wrapper.find('input[type="file"]')
        let input = inputWrapper.element
        //new File接受兩個(gè)參數(shù)第一個(gè)文件內(nèi)容(必須是數(shù)組)泥彤,第二個(gè)是文件名
        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
如何測(cè)試ajax:做一個(gè)假的ajax測(cè)試請(qǐng)求

新建一個(gè)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('可以上傳一個(gè)文件', (done)=>{
      // 當(dāng)我們上傳的時(shí)候把我們的ajax請(qǐng)求改成自己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>'
            }
        })

上面之所以要單獨(dú)在一個(gè)對(duì)象里寫post方法卿啡,是因?yàn)槿绻覀冎苯訉懗梢粋€(gè)對(duì)象或者函數(shù)吟吝,那我們更改它,只是更改了引用地址颈娜,原來(lái)的還是不會(huì)變剑逃,而我們通過(guò)對(duì)象里的引用來(lái)修改外層引用一直不會(huì)變浙宜,所以改了里面的引用其他的也會(huì)跟著變

上面的代碼運(yùn)行后發(fā)現(xiàn)會(huì)有bug,主要原因是我們?cè)谑褂媒M件的時(shí)候是通過(guò).sync來(lái)更新fileList的蛹磺,但是我們?cè)谧鰡卧獪y(cè)試的時(shí)候沒(méi)有這一步粟瞬,所以我們必須手動(dòng)更新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})
                }
            }

檢測(cè)上傳loading時(shí)顯示的菊花
首先在upload.vue中文件上傳成功后添加一行觸發(fā)uploaded事件的代碼

  • upload.vue
afteruploadFile(){
    ...
    this.$emit('uploaded')
}
it('可以上傳一個(gè)文件', (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)
       
//第一個(gè)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接受兩個(gè)參數(shù)第一個(gè)文件內(nèi)容(必須是數(shù)組),第二個(gè)是文件名
        let file1 = new File(['xxxx'], 'xxx.txt')
        const data = new DataTransfer()
        data.items.add(file1)
        input.files = data.files
        // 沒(méi)上傳成功前顯示菊花
        let use = wrapper.find('use').element
        expect(use.getAttribute('xlink:href')).to.eq('#i-loading')
    })
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末萤捆,一起剝皮案震驚了整個(gè)濱河市裙品,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌鳖轰,老刑警劉巖清酥,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異蕴侣,居然都是意外死亡焰轻,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門昆雀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)辱志,“玉大人,你說(shuō)我怎么就攤上這事狞膘】粒” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵挽封,是天一觀的道長(zhǎng)已球。 經(jīng)常有香客問(wèn)我,道長(zhǎng)辅愿,這世上最難降的妖魔是什么智亮? 我笑而不...
    開(kāi)封第一講書人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮点待,結(jié)果婚禮上阔蛉,老公的妹妹穿的比我還像新娘。我一直安慰自己癞埠,他們只是感情好状原,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著苗踪,像睡著了一般颠区。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上通铲,一...
    開(kāi)封第一講書人閱讀 51,370評(píng)論 1 302
  • 那天瓦呼,我揣著相機(jī)與錄音,去河邊找鬼。 笑死央串,一個(gè)胖子當(dāng)著我的面吹牛磨澡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播质和,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼稳摄,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了饲宿?” 一聲冷哼從身側(cè)響起厦酬,我...
    開(kāi)封第一講書人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎瘫想,沒(méi)想到半個(gè)月后仗阅,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡国夜,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年减噪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片车吹。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡筹裕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出窄驹,到底是詐尸還是另有隱情朝卒,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布乐埠,位于F島的核電站抗斤,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏丈咐。R本人自食惡果不足惜瑞眼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望扯罐。 院中可真熱鬧负拟,春花似錦烦衣、人聲如沸歹河。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)秸歧。三九已至,卻和暖如春衅澈,著一層夾襖步出監(jiān)牢的瞬間键菱,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工今布, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留经备,地道東北人拭抬。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像侵蒙,于是被迫代替她去往敵國(guó)和親造虎。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 第一部分 HTML&CSS整理答案 1. 什么是HTML5纷闺? 答:HTML5是最新的HTML標(biāo)準(zhǔn)算凿。 注意:講述HT...
    kismetajun閱讀 27,485評(píng)論 1 45
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)、插件犁功、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,103評(píng)論 4 62
  • 點(diǎn)擊查看原文 Web SDK 開(kāi)發(fā)手冊(cè) SDK 概述 網(wǎng)易云信 SDK 為 Web 應(yīng)用提供一個(gè)完善的 IM 系統(tǒng)...
    layjoy閱讀 13,761評(píng)論 0 15
  • 最近處理了不少學(xué)生違紀(jì)事件氓轰,處理以后整理發(fā)現(xiàn),違紀(jì)者絕大多數(shù)是成績(jī)不理想的孩子浸卦,成績(jī)優(yōu)秀的孩子基本沒(méi)有署鸡,我就...
    馨月_72c9閱讀 328評(píng)論 0 0
  • 在此感謝cc團(tuán)隊(duì)基于數(shù)據(jù)對(duì)用戶體驗(yàn)趨勢(shì)對(duì)過(guò)去所做的總結(jié),以及對(duì)未來(lái)交互行業(yè)的預(yù)測(cè)镐躲。 一储玫、可用性成為了常談話題 設(shè)計(jì)...
    Ystarsan閱讀 569評(píng)論 0 0