造輪子-圖片上傳組件

用戶圖片上傳思路:

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ù)器
  1. form表單必須設(shè)置action對應(yīng)你服務(wù)器的路徑,必須設(shè)置method="post" enctype="multipart/form-data"
  2. 必須指定文件的name
  3. 自己寫一個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)你上傳同一張圖片就會報錯,解決辦法:

  1. 強制規(guī)定每一個上傳的文件都必須返回一個唯一的id
  2. 每次判斷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ù)

  1. 實現(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 || '上傳失敗')
}
  1. 文件尺寸不得超出的提示
    思路:在文件上傳前的函數(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')
    })
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容