实现效果
所谓
headerSearch指 页面搜索
原理:
headerSearch 是复杂后台系统中非常常见的一个功能,它可以:在指定搜索框中对当前应用中所有页面进行检索,以 select 的形式展示出被检索的页面,以达到快速进入的目的
那么明确好了 headerSearch 的作用之后,接下来我们来看一下对应的实现原理
根据前面的目的我们可以发现,整个 headerSearch 其实可以分为三个核心的功能点:
- 根据指定内容对所有页面进行检索
- 以
select形式展示检索出的页面 - 通过检索页面可快速进入对应页面
那么围绕着这三个核心的功能点,我们想要分析它的原理就非常简单了:根据指定内容检索所有页面,把检索出的页面以 select 展示,点击对应 option 可进入
方案:
对照着三个核心功能点和原理,想要指定对应的实现方案是非常简单的一件事情了
- 创建
headerSearch组件,用作样式展示和用户输入内容获取 - 获取所有的页面数据,用作被检索的数据源
- 根据用户输入内容在数据源中进行 模糊搜索
- 把搜索到的内容以
select进行展示 - 监听
select的change事件,完成对应跳转
方案落地:创建 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
-
安装 fuse.js
npm install --save fuse.js@6.4.6 -
初始化
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 } ] }) -
参考 Fuse Demo 与 最终效果,可以得出,我们最终期望得到如下的检索数据源结构
- 但之前路由里的数据是这样的:
- 所以我们之前处理了的数据源并不符合我们的需要,所以我们需要对数据源进行重新处理
数据源重处理,生成 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
}
数据源处理完成之后,最后我们就只需要完成:
- 渲染检索出的数据
- 完成对应跳转
那么下面我们按照步骤进行实现:
-
渲染检索出的数据
<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> -
完成对应跳转
// 选中回调 const onSelectChange = val => { router.push(val.path) }
headerSearch 方案总结
那么到这里整个的 headerSearch 我们就已经全部处理完成了,整个 headerSearch 我们只需要把握住三个核心的关键点
- 根据指定内容对所有页面进行检索
- 以
select形式展示检索出的页面 - 通过检索页面可快速进入对应页面
保证大方向没有错误,那么具体的细节处理我们具体分析就可以了。
关于细节的处理,可能比较复杂的地方有两个:
- 模糊搜索
- 检索数据源
对于这两块,我们依赖于 fuse.js 进行了实现,大大简化了我们的业务处理流程。