HeaderSearch 处理方案
# HeaderSearch 原理及方案分析
所谓
headerSearch
指 页面搜索
# 原理:
headerSearch
是复杂后台系统中非常常见的一个功能,它可以:在指定搜索框中对当前应用中所有页面进行检索,以 select
的形式展示出被检索的页面,以达到快速进入的目的
功能点:
- 根据指定内容对所有页面进行检索
- 以
select
形式展示检索出的页面 - 通过检索页面可快速进入对应页面
根据指定内容检索所有页面,把检索出的页面以 select
展示,点击对应 option
可进入
# 方案:
创建
headerSearch
组件,用作样式展示和用户输入内容获取获取所有的页面数据,用作被检索的数据源
根据用户输入内容在数据源中进行 模糊搜索 (opens new window)
把搜索到的内容以
select
进行展示监听
select
的change
事件,完成对应跳转
# 方案落地:创建 headerSearch 组件
创建components/headerSearch/index` 组件:
<template>
<div :class="{ show: isShow }" class="header-search">
<svg-icon
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="Search"
:remote-method="querySearch"
@change="onSelectChange"
>
<el-option
v-for="option in 5"
:key="option"
:label="option"
:value="option"
></el-option>
</el-select>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 控制 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 querySearch = () => {
console.log('querySearch')
}
// 选中回调
const onSelectChange = () => {
console.log('onSelectChange')
}
</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>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
在 navbar
中导入该组件
<header-search class="right-menu-item hover-effect"></header-search>
import HeaderSearch from '@/components/HeaderSearch'
2
接下来处理对应的 检索数据源
那么对于我们当前的业务而言,我们希望被检索的页面其实就是左侧菜单中的页面,那么我们检索数据源即为:左侧菜单对应的数据源
<script setup>
import { ref, computed } from 'vue'
import { filterRouters, generateMenus } from '@/utils/route'
import { useRouter } from 'vue-router'
...
// 检索数据源
const router = useRouter()
const searchPool = computed(() => {
const filterRoutes = filterRouters(router.getRoutes())
console.log(generateMenus(filterRoutes))
return generateMenus(filterRoutes)
})
console.log(searchPool)
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
# 方案落地:对检索数据源进行模糊搜索
如果我们想要进行 模糊搜索 (opens new window) 的话,那么需要依赖一个第三方的库 fuse.js (opens new window)
-
npm install --save [email protected]
1 初始化
Fuse
,更多初始化配置项 可点击这里 (opens new window)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 } ] })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24参考 Fuse Demo (opens new window) 与 最终效果,可以得出,我们最终期望得到如下的检索数据源结构
[ { "path":"/my", "title":[ "个人中心" ] }, { "path":"/user", "title":[ "用户" ] }, { "path":"/user/manage", "title":[ "用户", "用户管理" ] }, { "path":"/user/info", "title":[ "用户", "用户信息" ] }, { "path":"/article", "title":[ "文章" ] }, { "path":"/article/ranking", "title":[ "文章", "文章排名" ] }, { "path":"/article/create", "title":[ "文章", "创建文章" ] } ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48所以我们之前处理了的数据源并不符合我们的需要,所以我们需要对数据源进行重新处理
# 方案落地:数据源重处理,生成 searchPool
我们明确了最终我们期望得到数据源结构,那么接下来我们就对重新计算数据源,生成对应的
searchPoll
创建 compositions/HeaderSearch/FuseData.js
import path from 'path'
import i18n from '@/i18n'
/**
* 筛选出可供搜索的路由对象
* @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]
}
// 当前存在 meta 时,使用 i18n 解析国际化数据,组合成新的 title 内容
// 动态路由不允许被搜索
// 匹配动态路由的正则
const re = /.*\/:.*/
if (route.meta && route.meta.title && !re.exec(route.path)) {
const i18ntitle = i18n.global.t(`msg.route.${route.meta.title}`)
data.title = [...data.title, i18ntitle]
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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
在 headerSearch
中导入 generateRoutes
<script setup>
import { computed, ref } from 'vue'
import { generateRoutes } from './FuseData'
import Fuse from 'fuse.js'
import { filterRouters } from '@/utils/route'
import { useRouter } from 'vue-router'
...
// 检索数据源
const router = useRouter()
const searchPool = computed(() => {
const filterRoutes = filterRouters(router.getRoutes())
return generateRoutes(filterRoutes)
})
/**
* 搜索库相关
*/
const fuse = new Fuse(searchPool.value, {
...
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
通过 querySearch
测试搜索结果
// 搜索方法
const querySearch = query => {
console.log(fuse.search(query))
}
2
3
4
# 方案落地:渲染检索数据
渲染检索出的数据
<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23完成对应跳转
// 选中回调 const onSelectChange = val => { router.push(val.path) }
1
2
3
4
# 方案落地:剩余问题处理
到这里我们的 headerSearch
功能基本上就已经处理完成了,但是还存在一些小 bug
,那么最后这一小节我们就处理下这些剩余的 bug
- 在
search
打开时,点击body
关闭search
- 在
search
关闭时,清理searchOptions
headerSearch
应该具备国际化能力
search两个问题:
/**
* 关闭 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)
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
国际化的问题:
只需要:监听语言变化,重新计算数据源初始化
fuse
即可
在
utils/i18n
下,新建方法watchSwitchLang
import { watch } from 'vue' import store from '@/store' /** * * @param {...any} cbs 所有的回调 */ export function watchSwitchLang(...cbs) { watch( () => store.getters.language, () => { cbs.forEach(cb => cb(store.getters.language)) } ) }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15在
headerSearch
监听变化,重新赋值<script setup> ... import { watchSwitchLang } from '@/utils/i18n' ... // 检索数据源 const router = useRouter() let searchPool = computed(() => { const filterRoutes = filterRouters(router.getRoutes()) return generateRoutes(filterRoutes) }) /** * 搜索库相关 */ // 封装起来,方便调用 let fuse const initFuse = searchPool => { fuse = new Fuse(searchPool, { ... } initFuse(searchPool.value) ... // 处理国际化 watchSwitchLang(() => { searchPool = computed(() => { const filterRoutes = filterRouters(router.getRoutes()) return generateRoutes(filterRoutes) }) initFuse(searchPool.value) }) </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# headerSearch 方案总结
- 根据指定内容对所有页面进行检索
- 以
select
形式展示检索出的页面 - 通过检索页面可快速进入对应页面
关于细节的处理,可能比较复杂的地方有两个:
- 模糊搜索
- 检索数据源