教你一个vue小技巧,一般人我不说的

qcloudcommunity · · 1322 次点击 · 开始浏览    置顶
这是一个创建于 的主题,其中的信息可能已经有所发展或是发生改变。

> 本文由云+社区发表 ## 1. 需求 最近的项目中,需要实现在vue框架中动态渲染带提示框的单选/多选文本框,具体的效果如下图所示,在输入框聚焦时,前端组件通过接收的kv参数渲染出选项,用户点击选项,可以将选择的选项的key拼装到输入框中,同时允许用户自由输入。 ![img](https://ask.qcloudimg.com/http-save/1065788/ejdptkippr.jpeg?imageView2/2/w/1620) 由于项目中使用的element-ui,首选考虑使用组件的input和select组件,然而实际使用中发现框架提供的组件不能很好满足此需求。例如,使用带输入建议的input组件,能够实现提示框和单选,但并不能方便地实现多选(重复选择会覆盖输入框内的内容)。 ![img](https://ask.qcloudimg.com/http-save/1065788/psvk8evzjj.png?imageView2/2/w/1620) 而使用框架提供的select选择器的远程搜索功能,能够实现提示框,也能轻松实现单选与多选,但select组件的内容只能通过用户选择(文本框内容必须包含于提示选项中),不允许用户自由输入文本内容。 ![img](https://ask.qcloudimg.com/http-save/1065788/57molhnn0q.png?imageView2/2/w/1620) 再加上设计稿需要实现三列布局,最终的返回结果需要动态拼装选项key值,若对现有的element组件进行改造成本过高,因此,尝试封装带提示框的单选/多选文本框组件,记录下封装过程中组件交互方面遇到的问题。 ## 2. 接口参数设计 组件支持传入6个参数,分别为 1. size (尺寸,String, medium / small / mini) 2. value (输入值,String,可以使用sync修饰符实现双向绑定) 3. opt (选项列表,Array,kv数组形如{key:1, value:xxx}) 4. seperator (分隔符,String,如','、'|'、'-') 5. multiple (是否支持多选,Boolean) 6. placeholder (提示,String) 调用方式如下: ```js <cs-select size="mini" // 尺寸 :value.sync="value" // value :opt="optParams.kv" // 选项 seperator="," // 分隔符 :multiple="true"> </cs-select> ``` ## 3. 提示框显示隐藏交互实现 细化上述需求,需要在用户点击输入框(获取焦点)时,显示提示框,在用户点击空白区域时隐藏提示框,点击组件自身时不做任何操作。组件的模板结构如下,通过show变量控制提示框的显示与隐藏,在组件的输入框绑定聚焦和失焦事件: `@focus="onfocus"` 和 `@blur="onblur"`,在focus时设置this.show为true,blur时为false,由于点击了输入框外的选项元素必然导致输入框失焦从而自动关闭,所有问题的关键在于如何实现点击提示选项而不隐藏提示框。 ```js <template> <div> <!-- 输入框 --> <el-input @focus="onfocus @blur="onblur> </el-input> <!-- 提示框 --> <div v-if="show && opt.length > 0"> <el-row> <el-col :span="8" v-for="(item, index) in opt" :key="index"> {{item.value}} </el-col> </el-row> </div> </div> </template> ``` ### 3.1 尝试方案1: click事件主动聚焦 根据上述需求,毫无疑问联想到可以为选项绑定click事件,调用el-input的`focus()`方法进行主动聚焦,实现如下,此处使用了vue的ref,通过$ref来查找dom元素。 ```js clickEvent () { this.show = true // 设置提示框显示 this.$refs.input.$el.querySelector('input').focus() // 设置主动聚焦 } ``` **问题:**实际开发过程中发现,每次点击提示选项后,提示框会闪烁一次,原因在于js的事件机制,`blur`事件先于`click`事件执行,导致提示框隐藏后再显示,造成**闪烁**。 ### 3.2 尝试方案2: blur事件添加延时器 + 开关变量 由于方案1`blur`事件先于`click`事件执行,因此考虑使用`settimeout`延时器来改变执行时间,实现如下。 ```js blurEvent () { setTimeout(() => { this.show = false }, 200) } ``` **问题:**实际开发过程中发现,延时器延时执行关闭操作,导致输入框获取焦点后,主动关闭了提示框,不再自动打开,不满足需求,因此考虑使用开关变量`canClose`判断当前是否需要执行关闭,实现如下。 ```js focusEvent () { this.show = true this.canClose = true // 聚焦时打开开关 }, blurEvent () { if (this.canClose) { setTimeout(() => { this.show = false // 只有开关打开时才执行关闭 }, 200) } }, clickEvent (key) { this.canClose = false // 点击提示选项,关闭开关 this.show = true ... } ``` **问题:**实际开发过程中发现,大多数情况下,提示框能够显示与隐藏,但是当操作较快时,会**偶尔出现提示框不能关闭或提前关闭**的情况,分析原因在于,延时器期间任何对开关的操作可能导致组件开关状态变化,致使状态紊乱。 ### 3.3 尝试方案3: 不使用blur,关闭方法改为事件委托,动态绑定class 如果关闭不使用blur,而是通过点击事件触发,则不会存在上述时序问题,因此考虑在全局使用事件委托,监听用户的点击事件,通过判断节点特殊class实现提示框关闭,实现如下。 ```js $('body').on('click', (event) => { this.show = false }) $('body').on('click', className, (event) => { this.show = true }) ``` **问题1:**事件委托,使用固定的class,当同时渲染多个组件时,无法实现单独管理提示框的开关,因此**无法渲染多组件**,因此class使用动态绑定,每个组件使用不同的class,实现如下。 **问题2:**阻止冒泡,如果组件的父容器阻止了冒泡,则无法触发body上绑定的关闭方法,需要针对父容器单独处理。 ```js let randId = Math.round(Math.random()*100000) this.className = `cs-select-${randId}` // 单独处理父容器,在父容器上绑定关闭事件 ... ``` 改造后的组件表面看起来已经基本可用,实际存在诸多问题: **问题1:**组件中对父组件绑定了事件,违反了设计模式的迪米特法则,**增加了组件间的耦合**,不利于后期维护。 **问题2:**上述操作只考虑了点击事件的关闭,忽略了其他可能关闭的情况,如使用`tab`按键切换输入框时也需要能正常显示隐藏提示框。 **问题3:**绑定事件过多会带来性能隐患甚至导致意想不到的问题发生。 ### 3.4 尝试方案4: onfocus + onblur + mousedown + 开关 由于focus事件先于click事件执行,导致了上述方案1和方案2问题的产生,通过查阅资料可知,`mousedown`事件先于focus事件执行,因此,使用onfocus + onblur + mousedown + 开关能够很好解决上述执行时序问题,具体实现如下。 ```js focusEvent () { this.show = true this.canClose = true // 聚焦时打开开关 }, blurEvent () { if (this.canClose) { this.show = false // 只有开关打开时才执行关闭 } }, mousedownEvent (key) { this.canClose = false // 点击提示选项,关闭开关 this.show = true this.$refs.input.$el.querySelector('input').focus() ... } ``` **问题:**实际开发中发现,由于组件是动态渲染的,mousedownEvent事件中无法直接获取到当前对象的dom元素`this.$refs.xxx`,导致自动聚焦失败。 ### 3.5 实现方案 在方案4的基础上,使用`nextTick`**异步更新队列**能够解决dom渲染时序问题,具体实现针对方案4稍作修改即可。 > `$nextTick`: 在vue官方深入响应式原理中说明了 vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后使用 $nextTick,则可以在回调中获取更新后的 DOM,官方示例:<https://cn.vuejs.org/v2/guide/reactivity.html#search-query-sidebar>focusEvent () { this.show = true this.canClose = true // 聚焦时打开开关 }, blurEvent () { if (this.canClose) { this.show = false // 只有开关打开时才执行关闭 } }, mousedownEvent (key) { this.canClose = false // 点击提示选项,关闭开关 this.show = true this.$nextTick(() => { this.$refs.input.$el.querySelector('input').focus() }) ... }4. 组件数据双向绑定 为了方便组件内数据的处理,传入组件的输入值value会首先被split分解为key数组,然后添加watcher观察器,监听输入值的变化,更新提示框的选中状态,并通过`$emit`方法同步到父组件中,实现数据的双向绑定,输入值的watch如下所示: ```js watch: { inputVal: { handler () { let selectArray = this.inputFilter() this.inputVal = selectArray.join(this.seperator) // 更新选中状态 this.updateActive() // 同步数据 this.$emit('update:value', this.inputVal) // 可改为v-model }, immediate: true } } ``` ## 5. 组件应用与改进 带提示框的单选/多选文本框组件的应用场景较多,典型的场景如封装企业联系人的选择器,用户输入用户名关键词,提示框显示相关联系人,同时允许用户自由输入用户名。 组件还有不少可以改进的地方,例如: 1. 目前的设计通过监听mousedown来阻止提示框的关闭,很明显不能兼容移动端,可以考虑添加touch事件; 2. 在css布局方面没有判断用户可见的友好性,在极端情况下可能会超出屏幕范围; 3. 还不支持slot插槽和动态设置class等。 随着整体项目的迭代可以逐步完善。 **此文已由作者授权腾讯云+社区发布** ****

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1322 次点击  
加入收藏 微博
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传