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