Vue中如何设计面包屑
最近做的项目层级结构比较深,需要使用面包屑来定位用户所在的位置,方便用户跳转层级。所以就遇到 web app 如何设计面包屑问题。
# 根据路由匹配设置面包屑
当路由是下面这种情况的时候:
let routes = [
{
path: '/users',
name: 'users-index',
meta: {
text: 'Users'
},
children: [
{
path: 'settings',
name: 'user-setting'
},
// ...
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
我们可以使用$route.matched
属性来生成面包屑。示例代码如下:
<ul>
<li v-for="route in $route.matched">
<router-link :to="{name: route.name}">
{{ route.meta.text }}
</router-link>
</li>
</ul>
2
3
4
5
6
7
这种方法只适用于解决静态面包屑,当我们需要处理例如路径是users/1/settings
这种情况,显然路由匹配就不是最佳选择了。并且路由层级的加深也并不意味着页面嵌套的加深。
当资源层级很深的时候,我们需要做很多空的父节点来处理。
# 每个页面设置面包屑
let routes = [
{
path: '/users/userId(\\d+)',
component: userLayout
children: [
{
path: 'settings',
name: 'user-setting',
component: userSettings
},
{
path: '',
name: 'user-dashboard'
}
// ...
]
}
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
当 URL 路径是users/1/settings
的时候,我们期望的面包屑应该是Users > Lee > Settings
。但是我们要怎么达到这个效果呢,特别是姓名信息我们还需要异步从服务器获取。
解决办法是使用Vuex
来存储面包屑的信息。有使用数组来存储面包屑的方案,但是需要注意面包屑加入的顺序问题。为了更灵活的控制面包屑,我选择了使用链表的方式来存储面包屑。
# 定义数据结构
下面来看看面包屑的结构。
interface BreadCrumb {
key: symbol;
text?: string;
to?: Route;
}
interface BreadCrumbItem {
prev: BreadCrumbList;
breadCrumb: BreadCrumb;
next: BreadCrumbList;
}
type BreadCrumbList = BreadCrumbItem | null;
2
3
4
5
6
7
8
9
10
11
12
使用双向链表可以更方便的查询节点,每个节点主要存储三个信息,key 主要用来标识这个节点的唯一性,所以是Symbol
类型,text 是当前节点展示的文本,to 是配合 vue-router 跳转到其他页面。
prev 和 next 当然就是指向上一个节点或者下一个节点啦。
# 定义 state 和 mutation
const state = {
breadCrumbList: null,
breadCrumbTail: null
};
2
3
4
state 里存储的分别是面包屑的头指针和尾指针。这样方便使用。
const mutations = {
set(state, breadCrumbItem) {
state.breadCrumbList = breadCrumbItem;
state.breadCrumbTail = state.breadCrumbList;
},
add(state, breadCrumbItem) {
if (state.breadCrumbTail) {
state.breadCrumbTail.next = breadCrumbItem;
breadCrumbItem.prev = state.breadCrumbTail;
state.breadCrumbTail = breadCrumbItem;
}
},
replace(state, breadCrumbItem) {
let point = state.breadCrumbList;
while (point) {
if (point.breadCrumb.key === breadCrumbItem.breadCrumb.key) {
point.breadCrumb.text = breadCrumbItem.breadCrumb.text;
point.breadCrumb.to = breadCrumbItem.breadCrumb.to;
return;
} else {
point = point.next;
}
}
},
remove(state, breadCrumbItem) {
let point = state.breadCrumbTail;
while (point) {
if (point.breadCrumb === breadCrumbItem.breadCrumb) {
let before = point.prev;
let after = point.next;
if (before) {
before.next = after;
} else {
state.breadCrumbList = after;
}
if (after) {
after.prev = before;
} else {
point.next = null;
}
return;
} else {
point = point.prev;
}
}
},
empty(state, breadCrumbItem) {
state.breadCrumbList = null;
state.breadCrumbTail = null;
}
};
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
面包屑提供的方法有,set, add, replace, remove, empty。 功能都顾名思义。
# 每个组件设置面包屑
根据上面的路由设计,我们需要在userLayout
组件里面完成Users > Lee
前两级面包屑的填充。然后在userSettings
组件里完成最后一级面包屑的填充。
由于第二级面包屑需要异步请求数据去填充,所以我们需要先事先添加一个空面包屑,之后再 replace。
// userLayout
mounted() {
const breadCrumbIndex = {
prev: null,
breadCrumb: {
key: Symbol('users'),
text: 'Users',
to: {
name: 'user-index'
}
},
next: null
}
const breadCrumbItem = {
prev: null,
breadCrumb: {
key: Symbol('users')
},
next: null
};
store.commit('set', breadCrumbIndex)
store.commit('add', breadCrumbItem);
axios.get(`/api/user/${userId}`).then(res => {
store.commit('replace', {
...breadCrumbItem,
breadCrumb: {
...breadCrumbItem.breadCrumb,
text: res.data.name,
to: {
name: 'user-dashboard'
}
}
})
})
this.$once('hook:beforeDestroy', () => {
store.commit('remove', breadCrumbIndex);
store.commit('remove', breadCrumbItem);
})
}
// userSettings
mounted() {
const breadCrumbItem = {
prev: null,
breadCrumb: {
key: Symbol('users'),
name: 'Settings',
to: this.$router.currentRoute
},
next: null
};
store.commit('add', breadCrumbItem);
this.$once('hook:beforeDestroy', () => {
store.commit('remove', breadCrumbItem);
})
}
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
# 组件切换生命周期的执行顺序
当我们加载 userDashboard
并切换到 userSettings
页面的时候,组件执行生命周期执行顺序是。
beforeCreate userDashboard
created userDashboard
beforMount userDashboard
mounted userDashboard // userDashboard show
beforeCreate userSettings // route to userSettings
created userSettings
beforMount userSettings
beforeDestory userDashboard
destroyed userDashboard
mounted userSettings
2
3
4
5
6
7
8
9
10
我们发现组件在beforeDestory
后面之前的是mounted
。所以如果把添加面包屑的代码放在beforeMount
里的时候,其实是先添加面包屑再删除之前的面包屑的。不过好在我们用的是链表,所以顺序不影响面包屑的生成。
# 生成面包屑
breadCrumb() {
const breadcrumb = [];
let point = store.state.breadCrumbList;
while (point && point.breadCrumb.name) {
breadcrumb.push(point.breadCrumb);
point = point.next;
}
return breadcrumb;
}
2
3
4
5
6
7
8
9
10
把面包屑的链表转换成数组,然后使用 vue 的v-for
来渲染就可以了。
# 总结
面包屑的这两种实现方式我都有尝试过,个人感觉针对动态面包屑第二种方式比较优雅。如果你有更好的实现方式,欢迎讨论。
参考:
Handling breadcrumbs with VueX in a VueJS Single Page Application (opens new window)