目录
第一章 接到需求
第二章 了解需求
第三章 解决需求
第四章 优化代码
第五章 解决问题
第一章 接到需求
- 最近开发的时候遇到这么一个事,技术经理是个全栈,已经把接口生成了,而且前端页面也写好了一个初稿,操作什么的功能基本上已经布局好了,如下:
经理说需求了:这里有两个按钮,一个是导入文件的,一个是导入机构的,你看看怎么把这按钮功能实现,把excel文件导入进去,后台的接口已经写好了
第二章 了解需求
- 然后小编接到需求了,clone下代码,根据路由找到对应的页面代码(注释的部分是小编已经写好的代码):
- 发现这里就是两个按钮,然后设置了权限一些其他配置。
- (注意事项:我们很多人有的时候为了语义化一些特别容易出现一个问题:就是会用到关键字,比如小编一开始看这个代码的时候,上面写的是@click="import(scope.row.id)",好家伙,不细看不知道,代码一跑吓一跳,直接控制台报错,一定要留意,import是关键字,我们不要直接定义!!!语义化命名不要命名到关键字!!!)
- 好了,接下来进入正题:当我们拿到这么一个需求,首先我们分析一下,很明显,我们拿到的需求是一个上传文件的功能实现,平常我们上传文件很多时候会用到element、antd等等一些框架类的东西来实现这么一个功能。那么问题来了,我们这里就只有按钮,样式、布局都确定了,而且不管我们怎么点击,都没反应。那么接下来我们考虑的是不是给按钮添加点击事件 -> 调出本地文件管理器 -> 点击我们需要上传的文件 -> 上传成功拿到文件的二进制流 -> 准备上传的参数 -> 像后端发送请求 -> 后端返回成功/失败的数据 -> 前端拿到数据进行处理,那么难点在哪呢:一个按钮,怎么能调出本地的文件管理器呢,又怎么知道我们点击文件上传了呢,对于用很多组件的我们来说可能就麻烦了,去找组件啥啥啥,但是小编觉得,是的,组件或许能帮我们实现这么个需求,但是我们难道没有别的方法了吗?
- 原生标签:input!!!小编回忆,最开始学习html的时候有个标签input,小编也不绕圈了,设置type="file"就是文件上传的按钮!!
- 回忆一下,看如下文档:
HTML input type 属性 | 菜鸟教程
第三章 解决需求
- 好了,用到的工具有了,现在的问题在于如何解决我们利用导入的按钮操作文件上传的按钮?
- 思路:小编为按钮与input都绑定唯一识别的ref(vue)/id(正常的html页面)以及click点击事件,原生js会告诉我们,当我们绑定id时,获取到DOM之后,我们能通过DOM.click获取到DOM上绑定的点击事件。问题解决!
- 大家根据小编的代码理解逻辑:(小编会逐步解释的)
html:
// 这里是element-ui的button组件
<el-button
v-if="hasPermission('book:book:import')" // 通过权限控制图标显示隐藏(可忽略)
type="text" //button的type(可忽略)
icon="el-icon-import" // 图标类名(可忽略)
size="small" // 尺寸
@click="toImportBookList(scope.row.id)"> // 按钮的点击事件,传的参数是该行数据的id
导入</el-button>
// 原生的input
<input
type="file" // 上传文件的类型
accept=".xls,.xlsx" // 限制上传的文件格式
:ref="`upload-book-list-file-${scope.row.id}`" // 唯一识别的ref,利用id使得渲染时ref不重名
style="display: block" // 控制该标签的显示隐藏
@click="importBookList(scope.row.id)"> // 该标签的点击事件
js:(注意一定要理解思路,而不是直接拷贝代码)
export default {
methods: { // vue:方法
toImportBookList (id) { // 按钮的点击事件
const bookListFileUplodDom = this.$refs[`upload-book-list-file-${id}`] // 通过$refs获取对应的input标签
bookListFileUplodDom.click() // 注意这句代码,他就是通过dom调用自身的点击事件
},
importBookList (id) { // input标签的点击事件
const _this = this
const bookListFileUplodDom = this.$refs[`upload-book-list-file-${id}`] // 通过$refs获取input标签
bookListFileUplodDom.addEventListener('change', function (e) { // input标签监听事件,当input发生变化时说明文件要么上传了,要么删除了,上传成功执行如下代码
const file = bookListFileUplodDom.files[0] // 该方法只针对input、type="file"或者图片,才能使用他获取到file的二进制流
const formBody = new FormData() // 这里的formData小编就不多说了,已经用到很多次了,小编会有单独的详细的文章做讲解
formBody.append('file', file)
formBody.append('id', id)
this.loading = true // 添加加载中(可忽略)
bookService.importBook(formBody).then(({data}) => { // 这里是发送请求系列信息,仅供参考
this.$message.success({ // 请求成功后返回信息
dangerouslyUseHTMLString: true,
message: data
})
this.refreshList() // 刷新列表
}).catch(err => { // 出错了则捕获错误
this.loading = false // 隐藏加载中
console.log('出错了', err)
})
}, false)
}
}
}
第四章 优化代码
- 使用e.stopPropagation()
阻止事件冒泡,使用addEventListener,三个参数,第三个参数小编设置了false,表示的是事件冒泡,由里向外触发,子元素的点击事件触发之后,之后还会触发父元素的点击事件,从而导致我们点击事件执行到不需要执行的事件,触发顺序:children -> parent -> body,添加e.stopPropagation()就只会执行我们点击的标签的点击事件。代码如下:
bookListFileUplodDom.addEventListener('change', function (e) {
e.stopPropagation() // 阻止冒泡事件(注意该方法写在代码前面)
……(其余代码逻辑)
}, false)
- addEventListener我们注册的监听'change'函数有两种情况:当上传文件成功时,input变化了,会执行代码,然后向发送请求,当然,当我们删除文件时,文件都为空了,还需要执行代码,向后端发请求吗?所以需要我们判断一下,当删除文件时,不执行代码,不向后端发送请求
通过输出可以看到,删除文件时,file拿到的值是undefined,那么我们就能通过判断来解决该问题了,代码如下:
bookListFileUplodDom.addEventListener('change', function (e) {
e.stopPropagation() // 阻止冒泡事件(注意该方法写在代码前面)
const file = bookListFileUplodDom.files[0]
if(file){ // 只有当file有数据时才执行如下逻辑
……(其余代码逻辑)
}
}, false)
- addEventListener注册了还需要removeEventListener移除事件,如何removeEventListener移除呢,在哪移除呢,解决方法在下一章节
第五章 解决问题
- 查看问题,不如不添加removeEventListener的效果,当我们执行多次导入时,也就相当于注册了很多个addEventListener('change',function(e){})事件,就会造成当我们只执行一次有效操作时,多个监听开始执行,导致最终前端会发送多个相同的请求
- (这里直接给出小编的最终解决完成的源代码,如果大家进一步知道原理,小编会在下一篇文章中说明原因,做详细的解释!)
- 原理看该文章
js基础:addEventListener与removeEventListener使用时,涉及的问题(包括事件捕获、冒泡,removeEventListener不生效问题)-CSDN博客
<script>
let bookListHandler = null // 定义一个自变量的等待赋值(移除监听函数使用)
export default {
methods: {
toImportBookList (id) {
const bookListFileUplodDom = this.$refs[`upload-book-list-file-${id}`]
bookListFileUplodDom.removeEventListener('change', bookListHandler, false) // 每次调用函数执行移除上一次的添加的监听
bookListFileUplodDom.click()
},
importBookList (id) {
const _this = this
const bookListFileUplodDom = this.$refs[`upload-book-list-file-${id}`]
console.log('bookListFileUplodDom', bookListFileUplodDom)
bookListHandler = function (e) { // 将执行的监听函数赋值
e.stopPropagation()
_this.getBookList(id, bookListFileUplodDom)
}
bookListFileUplodDom.addEventListener('change', bookListHandler, false) // 添加监听事件
},
getBookList (id, bookListFileUplodDom) {
const file = bookListFileUplodDom.files[0]
console.log('file', file)
if (file) {
const formBody = new FormData()
formBody.append('file', file)
formBody.append('id', id)
this.loading = true
bookService.importBook(formBody).then(({data}) => {
this.$message.success({
dangerouslyUseHTMLString: true,
message: data
})
this.refreshList()
}).catch(err => {
this.loading = false
console.log('出错了', err)
})
}
},
}
}
</script>