这是对vue-router 3 版本的源码分析。
本次分析会按以下方法进行:
- 按官网的使用文档顺序,围绕着某一功能点进行分析。这样不仅能学习优秀的项目源码,更能加深对项目的某个功能是如何实现的理解。这个对自己的技能提升,甚至面试时的回答都非常有帮助。
- 在围绕某个功能展开讲解时,所有不相干的内容都会暂时去掉,等后续涉及到对应的功能时再加上。这样最大的好处就是能循序渐进地学习,同时也不会被不相干的内容影响。省略的内容都会在代码中以…表示。
- 每段代码的开头都会说明它所在的文件目录,方便定位和查阅。如果一个函数内容有多个函数引用,这些都会放在同一个代码块中进行分析,不同路径的内容会在其头部加上所在的文件目录。
本章讲解router中命名视图是如何实现的。
另外我的vuex3源码分析也发布完了,欢迎大家学习:
vuex3 最全面最透彻的源码分析
还有vue-router的源码分析:
vue-router 源码分析——1. 路由匹配
vue-router 源码分析——2. router-link 组件是如何实现导航的
vue-router 源码分析——3. 动态路由匹配
vue-router 源码分析——4.嵌套路由
vue-router 源码分析——5.编程式导航
vue-router 源码分析——6.命名路由
官方定义
- 有时候想同时 (同级) 展示多个视图,而不是嵌套展示,例如创建一个布局,有 sidebar (侧导航) 和 main (主内容) 两个视图,这个时候命名视图就派上用场了。你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。如果 router-view 没有设置名字,那么默认为 default。特意强调确保正确使用 components 配置 (带上 s)。
<router-view class="view one"></router-view>
<router-view class="view two" name="a"></router-view>
<router-view class="view three" name="b"></router-view>
const router = new VueRouter({
routes: [
{
path: '/',
components: {
default: Foo,
a: Bar,
b: Baz
}
}
]
})
- 注意上面使用了命名路由时,routes中使用了components做key,而不是component。同时结构变成了一个对象,存储的内容就是上面代码中的components对象。
命名视图初始化
- 这里对应了router在初始化并创建匹配器时,对record的component的不同处理:
// create-route-map.js
function addRouteRecord(
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>,
route: RouteConfig,
parent?: RouteRecord,
matchAs?: string
) {
const { path, name } = route
...
const record: RouteRecord = {
components: route.components || {default: route.component},
...
}
...
}
- 然后,在使用router-view组件时,传入了一个name。如果对vue开发非常熟练的,大概已经知道router是如果定位到需要的component了:
-
- 在view.js组件中,用props接受父组件传入的name,同时给他一个默认值default
-
- 在匹配到对应路由的record后,对应的component即为recoed.components[name]
-
- 这样就实现了基本的命名视图路由匹配逻辑。
-
- 同时没有匹配的record或者component,则渲染一个空节点,这个也适合后面的嵌套路由逻辑。
// ./components/view.js
...
export default {
...
props: {
name: {
type: String,
default: 'default'
}
},
render(...) {
const name = props.name
const matched = route.matched[depth]
const component = matched && matched.components[name]
if (!matched || !component) {
return h()
}
...
return h(conponent, ...)
}
}
嵌套命名视图
官网例子:
/settings/emails /settings/profile
+-----------------------------------+ +------------------------------+
| UserSettings | | UserSettings |
| +-----+-------------------------+ | | +-----+--------------------+ |
| | Nav | UserEmailsSubscriptions | | +------------> | | Nav | UserProfile | |
| | +-------------------------+ | | | +--------------------+ |
| | | | | | | | UserProfilePreview | |
| +-----+-------------------------+ | | +-----+--------------------+ |
+-----------------------------------+ +------------------------------+
<!-- UserSettings.vue -->
<div>
<h1>User Settings</h1>
<NavBar/>
<router-view/>
<router-view name="helper"/>
</div>
// 路由配置
{
path: '/settings',
// 你也可以在顶级路由就配置命名视图
component: UserSettings,
children: [{
path: 'emails',
component: UserEmailsSubscriptions
}, {
path: 'profile',
components: {
default: UserProfile,
helper: UserProfilePreview
}
}]
}
- 命名视图的路由匹配规则并没有改变,这里只是多个一个children属性,同时children中的每个元素的内容又符合路由配置内容,即可以看做一个单独的route。所以在router初始化并记录路由的record时,对children进行了递归处理:
// create-route-map.js
function addRouteRecord(
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>,
route: RouteConfig,
parent?: RouteRecord,
matchAs?: string
) {
const { path, name } = route
...
const record: RouteRecord = {
components: route.components || {default: route.component},
parent, // 记录父record
...
}
if (route.children) {
route.children.forEach(child => {
// 递归调用addRouteRecord函数,父record会赋值到child的record.parent上
addRouteRecord(pathList, pathMap, nameMap, child, record)
})
}
...
}
- 回到开头的官网例子,如果我们访问的是/settings/emails,在匹配到对应的路由后,由于无法取得components[helper],所以 router-view name=“helper” 会被vue渲染成一个空节点。
<!-- UserSettings.vue -->
<div>
<h1>User Settings</h1>
<NavBar/>
<router-view/>
<router-view name="helper"/>
</div>
// 路由配置
{
path: '/settings',
// 你也可以在顶级路由就配置命名视图
component: UserSettings,
children: [{
path: 'emails',
component: UserEmailsSubscriptions
}, {
path: 'profile',
components: {
default: UserProfile,
helper: UserProfilePreview
}
}]
}
// ./components/view.js
export default {
...
props: {
name: {
type: String,
default: 'default'
}
},
render(...) {
...
const matched = route.matched[depth]
const component = matched && matched.components[name]
// 这里取不到 matched.components["helper"]
if (!matched || !component) {
...
return h()
}
}
}