當 el-upload 遇上 v-for 時應該注意的問題
雖然 element-ui 現在幾乎不更新了, 但不能否認它的優秀
而今天要講的就是這個優秀的框架中優秀的元件 el-upload
有過 element-ui 使用經驗的小夥伴大概都用過它的上傳元件(el-upload)
單獨使用 el-upload 的文件已經很全面, 操作起來也很好用, 沒毛病
但當 el-upload 遇上 v-for 時, 要面臨的問題就多得多了
筆者就曾遇到過, 特意寫下此文
注: 文末附完整 demo
有一次, 我遇到了這樣的需求:
- 有一個數量可增可減的陣列
- 陣列每項有上傳圖片功能
我第一反應, 簡單:
<!-- 大概看下就行, 不是完整程式碼 -->
<div v-for="(item, index) in lists">
<!--something-->
<el-upload></el-upload>
<button>刪除</button>
</div>
<button>新建</button>
搞掂
Question 1
經過幾個回合的擼碼, 該顯示的都出來了:
<!-- 大概看下就行, 不是完整程式碼 -->
<div v-for="(item, index) in lists" >
<el-upload :before-upload="handleBeforeUpload">
<i class="el-icon-plus"></i>
</el-upload>
<el-button type="danger" @click="remove(item, index)">刪除</el-button>
</div>
<el-button type="primary" @click= "add">新建</el-button>
- 點兩下 “新建”, 新建了兩項
- 選擇個圖片, 也如願顯示了
- 刪除第一項, 要命! 怎麼第二項不見了
這是因為 v-for 少了 key
vue 與 react 不同, 迴圈裡的 key 並不是強制要求, 很多時候列表裡缺少 key 並沒有任何問題
但 新增刪除項 屬於必須有 key 的情況
Question 2
因為 handleBeforeUpload 是公共方法, 只傳過去的圖片, 就不能分辨該圖片是來自第幾項的
所以給 handleBeforeUpload 帶上了個引數 index
<!-- 大概看下就行, 不是完整程式碼 -->
<div v-for="(item, index) in lists" :key="item.key">
<el-upload :before-upload="handleBeforeUpload(index)">
<i class="el-icon-plus"></i>
</el-upload>
<el-button type="danger" @click="remove(item, index)">刪除</el-button>
</div>
<el-button type="primary" @click="add">新建</el-button>
handleBeforeUpload(file) {
console.log(file)
},
不加還好好的, 可加上 index 後 handleBeforeUpload 接收到的 file 就成了 index 的值, 原本的 file 不見了
這是因為, 原始dom資料被 index 覆蓋了, 在 handleBeforeUpload() 裡就沒有 file 的蹤影
缺少原始資料, 還不簡單, 用 $event 就可以解決了
Question 3
又是一番焦頭爛額, 程式碼改成這樣
<!-- 大概看下就行, 不是完整程式碼 -->
<div v-for="(item, index) in lists" :key="item.key">
<el-upload :before-upload="handleBeforeUpload($event, index)">
<i class="el-icon-plus"></i>
</el-upload>
<el-button type="danger" @click="remove(item, index)">刪除</el-button>
</div>
<el-button type="primary" @click="add">新建</el-button>
可這個時候, 控制檯直接就報錯了
[VUE WARN]:未在例項上定義屬性或方法“$event”,但在呈現期間引用了該屬性或方法。通過初始化屬性,確保此屬性是反應性的,無論是在“資料”選項中,還是對於基於類的元件。
至於啥意思呢? 老實說我不是很懂, 不過以我的經驗想, “$event 出問題了”
這時候注意到 :before-upload="", before-upload 並不是觸發事件的屬性, 它的功能僅僅是把方法傳遞下去, 所以在這個位置用 $event 是不明智的
Question 4
在這山窮水盡的狀況下, 我決定狠一把, 索性就用閉包的思路, 把 el-upload 封裝成一個元件來用
<!-- 參考文末的完整 demo -->
思路大概是這樣的
- 把 el-upload 封裝成一個新元件 MyUpload
- 把 el-upload 需要的引數, 方法全部傳給 MyUpload
- 把 index 也傳給 MyUpload
- 以 before-upload 為例, 給 el-upload 設定 :before-upload=“imgBeforeUpload”
- 當 el-upload 觸發 before-upload 時, 執行 imgBeforeUpload
- imgBeforeUpload 裡接受到 file, 這時候只需要用 arguments 和 uploadId 把資料傳入 MyUpload 繼承下來的 beforeUpload 方法
- 最後在父元件中就能取到需要的 file 資料和對應的 index 資料了
總結
- v-for 要加上 key
- 如 before-upload, 不能使用 $event, 如果傳入資料, 就會把原始資料覆蓋
- 將 el-upload 元件再次封裝, 成為新元件, 這樣才能既儲存原始資料, 又傳入自定義資料
完整 demo
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>當 el-upload 遇上 v-for 時應該注意的問題</title>
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/element-ui/2.14.1/theme-chalk/index.min.css">
<!--<link rel="stylesheet" href="./jvw/element-ui/[email protected]/element-ui.2.4.0.css">-->
</head>
<body>
<div id="swq">
<App></App>
</div>
<script type="text/x-template" id="App-template">
<div>
<div v-for="(item, index) in lists" :key="item.key">
<my-upload :action="ossSign.host" :uploadId="index" :param="param" :fileList="item.faceList" :before-upload="handleBeforeUpload" :onSuccess="handleOnSuccess" :onError="handleOnError"></my-upload>
<el-button type="danger" @click="remove(item, index)">刪除</el-button>
</div>
<el-button type="primary" @click="add">新建</el-button>
</div>
</script>
<script type="text/x-template" id="MyUpload-template">
<div>
<el-upload :action="action" :limit="1" :data="param" :file-list="fileList" :before-upload="imgBeforeUpload" :on-success="imgSuccess" :on-error="imgError" list-type="picture-card">
<i class="el-icon-plus"></i>
</el-upload>
</div>
</script>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.5.6/vue.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/element-ui/2.14.1/index.js"></script>
<!--<script src="./jvw/vue/vue.2.5.16.js"></script>-->
<!--<script src="./jvw/element-ui/[email protected]/element-ui.2.4.0.js"></script>-->
<script type="text/javascript">
var MyUpload = {
template: "#MyUpload-template",
props: {
action: null,
param: {},
uploadId: null, //接收到的自定義的引數
beforeUpload: Function,
onSuccess: Function,
onError: Function,
onRemove: Function,
fileList: null,
},
data: function() {
return {}
},
methods: {
imgRemove() {
this.onRemove(...arguments, this.uploadId);
},
imgSuccess() {
this.onSuccess(...arguments, this.uploadId);
},
imgError() {
this.onError(...arguments, this.uploadId);
},
imgBeforeUpload(file) {
this.beforeUpload(...arguments, this.uploadId);
},
},
};
var App = {
template: "#App-template",
data: function() {
return {
ossSign: {
host: 'https://jsonplaceholder.typicode.com/posts/',
key: ''
},
param: {},
lists: [{
faceList: [],
key: this.getRandom(),
}],
dialogImageUrl: '',
dialogVisible: false,
}
},
components: {
MyUpload,
},
methods: {
handleOnSuccess(response, file, fileList, uploadId) {
console.log("handleOnSuccess: ", response, file, fileList, uploadId)
},
handleOnError(response, file, fileList, uploadId) {
console.log("handleOnError: ", response, file, fileList, uploadId)
},
handleBeforeUpload(file, uploadId) {
console.log("handleBeforeUpload: ", file, uploadId)
},
add() {
this.lists.push({
faceList: [],
key: this.getRandom(),
})
},
remove(item, index) {
this.lists.splice(this.lists.indexOf(item), 1);
},
getRandom() {
// 生成個臨時 key
return ~~(Math.random() * 10000) + "" + Date.now()
},
},
};
var vu = new Vue({
el: "#swq",
components: {
App: App,
},
})
</script>
</body>
</html>
//end