这是对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.命名路由
vue-router 源码分析——7.命名视图
vue-router 源码分析——8.重定向
vue-router 源码分析——9.别名
官网例子
- 路由组件传参的作用是将组件和路由状态进行解耦,使得组件可以在多个不同url路径的路由中使用。
- 官网例子虽然解释了使用props将路由和组件解耦的方法,但是没有直观地展示这么做的好处,下面是我写的一个例子:在这里例子可以看到,多个路由中重用这个UserProfile组件,而不管路由的参数是如何命名的。这样不仅体现了解耦,更体现了组件的复用性和易维护性。
const UserProfile = {
props: ['userId'],
template: '<div>User Profile for {{ userId }}</div>'
}
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: UserProfile, props: route => ({ userId: route.params.id }) },
{ path: '/profile/:userId', component: UserProfile, props: route => ({ userId: route.params.userId }) },
]
})
三种模式
- 布尔模式:如果 props 被设置为 true,route.params 将会被设置为组件属性。
- 对象模式:如果 props 是一个对象,它会被按原样设置为组件属性。当 props 是静态的时候有用。
- 函数模式:可以创建一个函数返回 props。这样你便可以将参数转换成另一种类型,将静态值与基于路由的值结合等等。
const User = {
props: ['id'],
template: '<div>User {{ id }}</div>'
}
// 布尔模式
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User, props: true },
]
})
// 对象模式
const router = new VueRouter({
routes: [
{
path: '/promotion/from-newsletter',
component: Promotion,
props: { newsletterPopup: false }
}
]
})
// 函数模式
const router = new VueRouter({
routes: [
{
path: '/search',
component: SearchUser,
props: route => ({ query: route.query.q })
}
]
})
路由记录(Record)对 props 的处理
- 在路由初始化,生成路由记录record时,用了多个三元运算符按不同情况将props进行了格式化处理。
- 如果路由没有设置props,则为空对象。
- 如果使用了命名视图,则直接将设置的props赋值给record.props。
- 如果不属于以上两种情况,则将设置的props赋值给一个新对象的default属性,并将此对象赋值给record.props。
// create-route-map.js
export function createRouteMap (...) : {...} {
routes.forEach(route => {
addRouteRecord(pathList, pathMap, nameMap, route, parentRoute)
})
}
...
function addRouteRecord(...) {
const record: RouteRecord = {
...
props:
route.props == null
? {} // 未设置props或props为空
: route.components
? route.props // 使用了命名视图时
: { default: route.props } // 设置了props且未使用命名视图
}
}
触发路由导航时对 props 的处理
- 触发路由导航时,会尝试获取匹配的路由记录的props数据。
- 通过 switch - case 按props的不同类型做不同处理(这里就实现了props传参的三种方法:布尔、对象和函数)。
- 将处理后的props数据赋值给组件的props属性,这样组件就能通过设置props来获取需要的数据了。
// ./components/view.js
export default {
...
props: {
name: {
type: String,
default: 'default'
}
},
render(_, { props, children, parent, data }) {
...
const matched = route.matched[depth]
const configProps = matched.props && matched.props[name] // configProps 是按照当前视图名称获取的,在使用了命名视图时,需要分别为每个命名视图添加 `props` 选项
if (configProps) {
...
fillPropsinData(component, data, route, configProps) // 将 configProps 填充到data中
}
return h(component, data, children)
}
}
function fillPropsinData (component, data, route, configProps) {
// resolve props
let propsToPass = data.props = resolveProps(route, configProps)
if (propsToPass) {
// clone to prevent mutation
propsToPass = data.props = extend({}, propsToPass)
...
}
}
// resolveProps函数中的switch-case处理了路由传参的三种方法
function resolveProps (route, config) {
switch (typeof config) {
case 'undefined':
return
case 'object':
return config // 对象模式,没有动态转换,所以适合静态props
case 'function':
return config(route) // 函数模式,接受route作为唯一参数
case 'boolean':
return config ? route.params : undefined // 布尔模式,将动态路由的params赋值给props
default:
if (process.env.NODE_ENV !== 'production') {
warn(
false,
`props in "${route.path}" is a ${typeof config}, ` +
`expecting an object, function or boolean.`
)
}
}
}
源码分析补充:组件可以取到它未使用的 props 吗?
- 假设有如下设置:路由中定义的props中有两个属性:userId 和 defaultId,但是组件 UserProfile 的props 只接收 userId。如果我们想要在组件中使用 defaultId,但是又不想在 props 中接收它,同时也不能使用 this.$route 来获取,那该怎么办呢?
- 通过分析源码发现,vue-router 会将路由组件传参中未被组件 props 定义接受的参数传给 data.attrs属性。
const UserProfile = {
props: ['userId'],
template: '<div>User Profile for {{ userId }}</div>'
}
const router = new VueRouter({
routes: [{
path: '/user/:id',
component: UserProfile,
props: route => ({
userId: route.params.id,
defaultId: 123456
})
}]
})
- 所以在 UserProfile 组件中,可以通过 this.$attrs 来访问 defaultId:
// ./components/view.js
...
function fillPropsinData (component, data, route, configProps) { // resolve props
let propsToPass = data.props = resolveProps(route, configProps)
if (propsToPass) {
propsToPass = data.props = extend({}, propsToPass)
const attrs = data.attrs = data.attrs || {}
// 处理 data.attrs 的逻辑
for (const key in propsToPass) {
if (!component.props || !(key in component.props)) {
attrs[key] = propsToPass[key]
delete propsToPass[key]
}
}
}
}