即时搜索功能的实现:利用好element-ui文档上未声明的好资源

125 阅读3分钟

(21年总计的一篇文章,可能最新版element ui库组件的很多语法已经改变。放上来,仅提供一些思路的思考和分享交流。)

elment-ui库中存在不少好资源,其官方文档并未介绍。我是在参考学习它的组件input的autocomplete源代码时发现的。以下为个人实现的一个需求:即时搜索。结合这些发现的资源,记录这个功能实现的思路。

技术栈:vue 2、element ui

1 需求

视觉同事提出了一个需求,在element-ui autocomplete组件即时搜索的基础上,增加了历史搜索关键词展示的功能:

  • 当输入框还未输入文字时,展示最近5个历史搜索词;输入文字后则进行远程搜索匹配
  • 历史搜索词存于浏览器,不作后端存储

20210525003.jpg

2 实现过程

2.1 实现思路

  1. div[contentEditable=true]代替传统的input
<template>
  <div class="autocomplete-input">
    <!-- 模拟input -->
    <span
      v-text="innerText"
      contenteditable="plaintext-only"
      :placeholder="placeholder"
      @input="changeText"
      @focus="handleFocus"
      @blur="handleBlur"
      @keydown.enter.prevent="handleSearchClick"
      ref="autocomplete"></span>
  </div>

  <!-- 搜索按钮 -->
  <div class="icon-search" @click.stop="handleSearchClick"></div>
</template>

<script lang="ts">
@Component
export default class AutoComplete extends Vue {

  private keyword = '' // 搜索关键词
  private innerText = ''
  private isLocked = false // 用于keyword和innerText的值
  private isFocus = false // 搜索框是否focus

  private suggestionDisabled = true // 是否可以请求获取建议

  public handleFocus () {
    this.isFocus = true
    this.isLocked = true
    this.suggestionDisabled = false
  }

  public handleBlur () {
    this.isLocked = false
  }

  public changeText (e: any) {
    this.keyword = e.target.innerText
    this.suggestionDisabled = false
  }

  public handleWordDel () {
    this.keyword = ''
    // this.autocompleteRef.innerText = '' // 暂时解决输入框中的innerText不消失
    this.handleSearchClick()
  }

  public handleClickOutside () {
    this.suggestionDisabled = true
    this.isFocus = false
  }

  // contenteditable的div不能添加change事件
  @Watch('keyword')
  onKeywordChange (n: string, o: string) {
    if (!this.isLocked && !this.innerText) {
      this.innerText = n
    }
    if (!this.isLocked && n !== o) {
      this.innerText = n
    }
    if (this.isFocus) {
      if (n) {
        this.debouncedGetData() // 防抖 获取远程匹配建议
      } else {
        this.getHistoryWordList() // 获取 客户端存储的历史搜索词
      }
    }
  }

}
<script>
  1. 搜索匹配建议相关:ElScrollbar作为滚动容器,存储建议list;实时搜索查询时使用throttle-debounce中的debounce防抖

首先,我采用了element-ui的Scrollbar组件来作为远程搜索建议的滚动条容器:

<template>
  <el-scrollbar
    wrap-class="autocomplete-suggestion__wrap"
    v-if="hintList.length"
    v-show="hintListShow">
    <ul class="suggestions-list">
      <li
        class="ellipsis"
        v-for="(item, index) in hintList"
        :key="index"
        @click.stop="handleHintClick(item.value)"
        role="option">{{item.value}}</li>
    </ul>
  </el-scrollbar>
</template>

<script lang="ts">
@Component
export default class AutoComplete extends Vue {

  // 下拉建议是否展示(为true时场景:1.获取建议时;2.keyword&&isFocus)
  private get hintListShow () {
    return !!this.hintList.length && this.isFocus && !!this.keyword
  }
  private hintList: Hint[] = []

  // 点击“建议”item进行搜索
  public handleHintClick (value: string) {}

}
<script>

其次,我使用了throttle-debounce/debounce插件实现的debounce防抖技术:

<script lang="ts">
import debounce from 'throttle-debounce/debounce'

@Component
export default class AutoComplete extends Vue {
  @Prop({ type: Number, default: 500 }) debounce?: number

  private created () {
    this.debouncedGetData = debounce(this.debounce, false, this.getSeachHint)
  }

  // ajax远程获取匹配建议
  public getSeachHint () {}

  // 监听到搜索的关键词改变,则进行搜索匹配建议
  @Watch('keyword')
  onKeywordChange (n: string) {
    if (n) {
      this.debouncedGetData()
    }
  }
}
<script>
  1. 历史关键词查询、存储、删除等,前端缓存使用IndexedDB

  2. 移动安卓端搜索时,软键盘被顶起的问题,可以分3步解决:

首先,判断软键盘是否弹起:通过页面可视区域的高度改变来判断;

<script lang="ts">
export default class App extends Vue {
  private mounted () {
    this.$nextTick(() => {
      this.bodyClientHeight = body.clientHeight;  // 键盘未弹出前页面可视区域的高度
    })
  }

  // 是否软键盘弹出
  public isKeyboardEjected (): boolean {
    const body = document.querySelector('body') as HTMLElement
    return body.clientHeight - this.bodyClientHeight < 0 // 软键盘弹出时,当前的页面可视区域高度变小
  }
}
</script>

其次,通知页面软键盘弹起:通过vue的EventBus通知;

<script lang="ts">
import Bus from '@/utils/bus.js';
export default class App extends Vue {
  private mounted () {
    window.addEventListener('resize', () => {
      this.emitKeyboardStatus()
    })
  }

  // 安卓端解决input键盘弹出导致页面压缩变形
  public emitKeyboardStatus () {
    if (isAndroid) {
      const isKeyboardEjected = this.isKeyboardEjected()
      Bus.$emit('keyboardPop', isKeyboardEjected)
    }
  }
}
</script>

最后,对应修改页面布局代码:

  • 软键盘弹起后,若匹配建议的容器高度由于太大、位置错乱,可以设置较小的高度;软键盘收起后,再恢复原高度
  • 当前页面离开前,记得让输入框失去焦点,避免因软键盘弹起造成的问题遗留
<script lang="ts">
import Bus from '@/utils/bus.js'

@Component
export default class AutoComplete extends Vue {

  private mounted () {
    Bus.$on('keyboardPop', (val: boolean) => {
      this.isKeyboardEjected = val
    })
  }
  
}
<script>
  1. 点击搜索组件外的任意空白处,展示 建议或者历史搜索词 的区域收起:这是一个细节问题,element-ui中的clickoutside就是用于解决此问题的。
<template>
  <div class="autocomplete-box" v-clickoutside="handleClickOutside">
  </div>
</template>

<script lang="ts">

@Component
export default class AutoComplete extends Vue {

  public handleClickOutside () {
    this.suggestionDisabled = true
    this.isFocus = false // input失去焦点,软键盘收起
  }
  
}
<script>

3 总结记录

3.1 Scrollbar

这是一个隐藏组件,官网文档并没有提供相关API。按需加载需要单独加载Scrollbar组件并使用,同时我们需要修改css样式,如height值、分析具体情况是否要隐藏x轴滚动条等。它允许传的props如下:

props: {
  native: Boolean,
  wrapStyle: {},
  wrapClass: {},
  viewClass: {},
  viewStyle: {},
  noresize: Boolean, // 如果 container 尺寸不会发生变化,最好设置它可以优化性能
  tag: {
    type: String,
    default: 'div'
  }
}

3.2 throttle-debounce

throttle-debounce提供了throttle(节流)和debounce(防抖) 两个函数,通过它们可以限制函数的执行效率,避免短时间内函数多次执行造成性能问题。

3.3 Vue的事件总线Eventbus

EventBus事件总线可以用来通信,vue中所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件,组件平行通知其他组件。但使用太频繁会造成难以维护的“灾难”。

解决软键盘弹出问题中的bus.js代码如下:

import Vue from 'vue'
export default new Vue()

3.4 clickoutside指令

clickoutside是element-ui实现的一个自定义指令,用来处理目标节点之外的点击事件,常用来处理下拉菜单等展开内容的关闭,在element-ui的Select选择器、Dropdrow下拉菜单、Popover弹出框等组件中都用到了。源码学习可Element源码学习--指令 v-clickoutside

在我们项目中,我们需要注册指令才能使用:

import Vue from 'vue'
import Clickoutside from 'element-ui/src/utils/clickoutside'

Vue.directive('clickoutside', Clickoutside) //全局注册指令 v-clickoutside