用戶圖片上傳思路:
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ù)器
- form表單必須設(shè)置action對(duì)應(yīng)你服務(wù)器的路徑芍耘,必須設(shè)置method="post" enctype="multipart/form-data"
- 必須指定文件的name
- 自己寫一個(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ò),解決辦法:
- 強(qiáng)制規(guī)定每一個(gè)上傳的文件都必須返回一個(gè)唯一的id
- 每次判斷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ù)
- 實(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 || '上傳失敗')
}
- 文件尺寸不得超出的提示
思路:在文件上傳前的函數(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')
})