1. 程式人生 > 其它 >當 el-upload 遇上 v-for 時應該注意的問題

當 el-upload 遇上 v-for 時應該注意的問題

技術標籤:Vue實際應用

雖然 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 -->

思路大概是這樣的

  1. 把 el-upload 封裝成一個新元件 MyUpload
  2. 把 el-upload 需要的引數, 方法全部傳給 MyUpload
  3. 把 index 也傳給 MyUpload
  4. 以 before-upload 為例, 給 el-upload 設定 :before-upload=“imgBeforeUpload”
  5. 當 el-upload 觸發 before-upload 時, 執行 imgBeforeUpload
  6. imgBeforeUpload 裡接受到 file, 這時候只需要用 arguments 和 uploadId 把資料傳入 MyUpload 繼承下來的 beforeUpload 方法
  7. 最後在父元件中就能取到需要的 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