开源项目页面搜索组件headerSearch 的封装

445 阅读4分钟

实现效果

wdy.gif

所谓 headerSearch页面搜索

原理:

headerSearch 是复杂后台系统中非常常见的一个功能,它可以:在指定搜索框中对当前应用中所有页面进行检索,以 select 的形式展示出被检索的页面,以达到快速进入的目的

那么明确好了 headerSearch 的作用之后,接下来我们来看一下对应的实现原理

根据前面的目的我们可以发现,整个 headerSearch 其实可以分为三个核心的功能点:

  1. 根据指定内容对所有页面进行检索
  2. select 形式展示检索出的页面
  3. 通过检索页面可快速进入对应页面

那么围绕着这三个核心的功能点,我们想要分析它的原理就非常简单了:根据指定内容检索所有页面,把检索出的页面以 select 展示,点击对应 option 可进入

方案:

对照着三个核心功能点和原理,想要指定对应的实现方案是非常简单的一件事情了

  1. 创建 headerSearch 组件,用作样式展示和用户输入内容获取
  2. 获取所有的页面数据,用作被检索的数据源
  3. 根据用户输入内容在数据源中进行 模糊搜索
  4. 把搜索到的内容以 select 进行展示
  5. 监听 selectchange 事件,完成对应跳转

方案落地:创建 headerSearch 组件

创建 components/headerSearch/index 组件:

<template>
  <div :class="{ show: isShow }" class="header-search">
    <svg-icon
      id="guide-search"
      class-name="search-icon"
      icon="search"
      @click.stop="onShowClick"
    />
    <el-select
      ref="headerSearchSelectRef"
      class="header-search-select"
      v-model="search"
      filterable
      default-first-option
      remote
      placeholder="请输入页面(路由)名字"
      :remote-method="querySearch"
      @change="onSelectChange"
    >
      <el-option
        v-for="option in searchOptions"
        :key="option.item.path"
        :label="option.item.title.join(' > ')"
        :value="option.item"
      ></el-option>
    </el-select>
  </div>
</template>

<script setup>
import { computed, ref, watch } from 'vue'
import { generateRoutes } from './FuseData'
import Fuse from 'fuse.js'
import { filterRouters } from '@/utils/route'
import { useRouter } from 'vue-router'

// 控制 search 显示
const isShow = ref(false)
// el-select 实例
const headerSearchSelectRef = ref(null)
const onShowClick = () => {
  isShow.value = !isShow.value
  headerSearchSelectRef.value.focus()
}

// search 相关
const search = ref('')
// 搜索结果
const searchOptions = ref([])
// 搜索方法
const querySearch = query => {
  if (query !== '') {
    searchOptions.value = fuse.search(query)
  } else {
    searchOptions.value = []
  }
}
// 选中回调
const onSelectChange = val => {
  router.push(val.path)
  onClose()
}

// 检索数据源
const router = useRouter()
let searchPool = computed(() => {
  const filterRoutes = filterRouters(router.getRoutes())
  console.log("f",filterRoutes)
  return generateRoutes(filterRoutes)
})
console.log("s",searchPool)
/**
 * 搜索库相关
 */
let fuse
const initFuse = searchPool => {
  fuse = new Fuse(searchPool, {
    // 是否按优先级进行排序
    shouldSort: true,
    // 匹配算法放弃的时机, 阈值 0.0 需要完美匹配(字母和位置),阈值 1.0 将匹配任何内容。
    threshold: 0.4,
    // 匹配长度超过这个值的才会被认为是匹配的
    minMatchCharLength: 1,
    // 将被搜索的键列表。 这支持嵌套路径、加权搜索、在字符串和对象数组中搜索。
    // name:搜索的键
    // weight:对应的权重
    keys: [
      {
        name: 'title',
        weight: 0.7
      },
      {
        name: 'path',
        weight: 0.3
      }
    ]
  })
}
initFuse(searchPool.value)

/**
 * 关闭 search 的处理事件
 */
const onClose = () => {
  headerSearchSelectRef.value.blur()
  isShow.value = false
  searchOptions.value = []
}
/**
 * 监听 search 打开,处理 close 事件
 */
watch(isShow, val => {
  if (val) {
    document.body.addEventListener('click', onClose)
  } else {
    document.body.removeEventListener('click', onClose)
  }
})
</script>

<style lang="scss" scoped>
.header-search {
  font-size: 0 !important;
  .search-icon {
    cursor: pointer;
    font-size: 18px;
    vertical-align: middle;
  }
  .header-search-select {
    font-size: 18px;
    transition: width 0.2s;
    width: 0;
    overflow: hidden;
    background: transparent;
    border-radius: 0;
    display: inline-block;
    vertical-align: middle;

    ::v-deep .el-input__inner {
      border-radius: 0;
      border: 0;
      padding-left: 0;
      padding-right: 0;
      box-shadow: none !important;
      border-bottom: 1px solid #d9d9d9;
      vertical-align: middle;
    }
  }
  &.show {
    .header-search-select {
      width: 210px;
      margin-left: 10px;
    }
  }
}
</style>

navbar 中导入该组件

<header-search class="right-menu-item hover-effect"></header-search>
import HeaderSearch from '@/components/HeaderSearch'

在有了 headerSearch 之后,接下来就可以来处理对应的 检索数据源了

检索数据源 表示:有哪些页面希望检索

那么对于我们当前的业务而言,我们希望被检索的页面其实就是左侧菜单中的页面,那么我们检索数据源即为:左侧菜单对应的数据源

对检索数据源进行模糊搜索

如果我们想要进行 模糊搜索 的话,那么需要依赖一个第三方的库 fuse.js

  1. 安装 fuse.js

    npm install --save fuse.js@6.4.6
    
  2. 初始化 Fuse,更多初始化配置项 可点击这里

    import Fuse from 'fuse.js'
    
    /**
     * 搜索库相关
     */
    const fuse = new Fuse(list, {
        // 是否按优先级进行排序
        shouldSort: true,
        // 匹配长度超过这个值的才会被认为是匹配的
        minMatchCharLength: 1,
        // 将被搜索的键列表。 这支持嵌套路径、加权搜索、在字符串和对象数组中搜索。
        // name:搜索的键
        // weight:对应的权重
        keys: [
          {
            name: 'title',
            weight: 0.7
          },
          {
            name: 'path',
            weight: 0.3
          }
        ]
      })
    
    
  3. 参考 Fuse Demo 与 最终效果,可以得出,我们最终期望得到如下的检索数据源结构

image.png

  • 但之前路由里的数据是这样的:

image.png

  1. 所以我们之前处理了的数据源并不符合我们的需要,所以我们需要对数据源进行重新处理

数据源重处理,生成 searchPool

我们明确了最终我们期望得到数据源结构,那么接下来我们就对重新计算数据源,生成对应的 searchPoll

创建 compositions/HeaderSearch/FuseData.js

import path from 'path-browserify'

/**
 * 筛选出可供搜索的路由对象
 * @param routes 路由表
 * @param basePath 基础路径,默认为 /
 * @param prefixTitle
 */
export const generateRoutes = (routes, basePath = '/', prefixTitle = []) => {
  // 创建 result 数据
  let res = []
  // 循环 routes 路由
  for (const route of routes) {
    // 创建包含 path 和 title 的 item
    const data = {
      path: path.resolve(basePath, route.path),
      title: [...prefixTitle]
    }
    // 动态路由不允许被搜索
    // 匹配动态路由的正则
    //不显示在左侧菜单栏的理由也要过滤掉
    const re = /.*/:.*/
    if (
        route.meta &&
        route.meta.title &&
        !route.hidden &&
        !re.exec(route.path) &&
        !res.find(item => item.path === data.path)
    ) {
      data.title = [...data.title, route.meta.title]
      res.push(data)
    }

    // 存在 children 时,迭代调用
    if (route.children) {
      const tempRoutes = generateRoutes(route.children, data.path, data.title)
      if (tempRoutes.length >= 1) {
        res = [...res, ...tempRoutes]
      }
    }
  }
  return res
}

数据源处理完成之后,最后我们就只需要完成:

  1. 渲染检索出的数据
  2. 完成对应跳转

那么下面我们按照步骤进行实现:

  1. 渲染检索出的数据

    <template>
      <el-option
          v-for="option in searchOptions"
          :key="option.item.path"
          :label="option.item.title.join(' > ')"
          :value="option.item"
      ></el-option>
    </template>
    
    <script setup>
    ...
    // 搜索结果
    const searchOptions = ref([])
    // 搜索方法
    const querySearch = query => {
      if (query !== '') {
        searchOptions.value = fuse.search(query)
      } else {
        searchOptions.value = []
      }
    }
    ...
    </script>
    
    
  2. 完成对应跳转

    // 选中回调
    const onSelectChange = val => {
      router.push(val.path)
    }
    

headerSearch 方案总结

那么到这里整个的 headerSearch 我们就已经全部处理完成了,整个 headerSearch 我们只需要把握住三个核心的关键点

  1. 根据指定内容对所有页面进行检索
  2. select 形式展示检索出的页面
  3. 通过检索页面可快速进入对应页面

保证大方向没有错误,那么具体的细节处理我们具体分析就可以了。

关于细节的处理,可能比较复杂的地方有两个:

  1. 模糊搜索
  2. 检索数据源

对于这两块,我们依赖于 fuse.js 进行了实现,大大简化了我们的业务处理流程。

源码地址:github.com/wudengyao/a…

源码地址:github.com/wudengyao/a…

源码地址:github.com/wudengyao/a…