这是对vue-router 3 版本的源码分析。
本次分析会按以下方法进行:
- 按官网的使用文档顺序,围绕着某一功能点进行分析。这样不仅能学习优秀的项目源码,更能加深对项目的某个功能是如何实现的理解。这个对自己的技能提升,甚至面试时的回答都非常有帮助。
- 在围绕某个功能展开讲解时,所有不相干的内容都会暂时去掉,等后续涉及到对应的功能时再加上。这样最大的好处就是能循序渐进地学习,同时也不会被不相干的内容影响。省略的内容都会在代码中以…表示。
- 每段代码的开头都会说明它所在的文件目录,方便定位和查阅。如果一个函数内容有多个函数引用,这些都会放在同一个代码块中进行分析,不同路径的内容会在其头部加上所在的文件目录。
本章讲解router中 router-link 组件是如何实现导航的。
另外我的vuex3源码分析也发布完了,欢迎大家学习:
vuex3 最全面最透彻的源码分析
还有vue-router的源码分析:
vue-router 源码分析——1. 路由匹配
vue-router 源码分析——2. router-link 组件是如何实现导航的
官网例子
- 在官网例子中,对做了三个注释:这是个组件、传入to属性,渲染成标签。
- 以我们主要分析 router-link 组件如果利用必要的数据来实现导航的。
- 这里先解释一下,渲染html页面的功能,是vue-router调用了vue的render函数,这个是vue的核心功能不做分析。所以这里是分析router是如何定位到对应路由以及做了哪些信息收集和处理的。
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<div id="app">
<h1>Hello App!</h1>
<p>
<!-- 使用 router-link 组件来导航. -->
<!-- 通过传入 `to` 属性指定链接. -->
<!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
<router-link to="/foo">Go to Foo</router-link>
<router-link to="/bar">Go to Bar</router-link>
</p>
</div>
Vue实例挂载link组件,从而可以使用标签
- 这里的install.js实际上是一个VUE的插件,这样当创建和挂载根实例时自动导入了插件。
- 这里面还涉及到VUE的混入(Vue.mixin),实现了在任意组件内通过this.$ router和this.$route来访问路由器和当前路由。感兴趣的小伙伴可以去看VUE官网的解释。
// ./install.js
import Link from './components/link'
export function install(Vue) {
Vue.component('RouterLink', Link)
}
link.js源码内容
- 传入了一个to变量,对应’/foo’,tag默认为’a’,即标签.
- render函数是vue用来描述如果构建虚拟DOM的,即如何按某个定义来构建组件。
- 如何渲染为html是vue中的核心内容,这里不做讨论,只分析vue-router中的源码内容,即resolve方法。
const { location, route, href } = router.resolve(
this.to,
current,
this.append
)
...
const classes = {}
const data: any = { class: classes }
...
return h(this.tag, data, this.$slots.default)
}
}
路由实例的 resolve 方法
- resolve入参的to是我们传入的字符串’/foo’,current是当前路由this.$route,append是undefine。
- resolve 方法内部又调用了很多函数和方法,得到需要的数据并返回。
- 让我们接着分析调用的函数(normalizeLocation,this.match, createHref)。
// ./router.js
export default class VueRouter {
...
resolve (
to: RawLocation,
current?: Route,
append?: boolean
) {
...
const location = normalizeLocation(to, current, append, this)
const route = this.match(location, current)
const fullPath = route.redirectedFrom || route.fullPath
const base = this.history.base
const href = createHref(base, fullPath, this.mode) // this.mode 看做 'hash' 即可
return {
location,
route,
href
...
}
}
}
normalizeLocation 函数分析
- 入参说明:raw = ‘/foo’, current = this.route, append = undifune, router = this.$router
- parsePath函数暂时没有额外影响,这里不做分析,只是说明 parsePath 的具体内容
- 最终返回一个对象,里面有 _normalized: true 和 path: '/foo’相关内容。
// ./util/location.js
export function normalizeLocation(
raw: RawLocation,
current: ?Route,
append: ?boolean,
router: ?VueRouter
): Location {
let next: Location = typeof raw === 'string' ? { path: raw } : raw
if (next._normalized) {
return next
}
...
// parsePath 的实际内容为 {'path': '/foo', 'query': '', 'hash': ''}
const parsedPath = parsePath(next.path || '')
// basePath 为当前的路由的路径
const basePath = (current && current.path) || '/'
// resolvePath函数对'/foo'也没有额外影响,可以理解直接返回了'/foo'赋值给path
const path = parsedPath.path
? resolvePath(parsedPath.path, basePath, append || next.append)
: basePath
...
return {
_normalized: true,
path,
...
}
}
this.match方法获得的route是什么
- 对应代码 const route = this.match(location, current),current = this.$route。
- match方法和resolve方法一样,是定义在VueRouter中的,它直接返回了路由匹配器的match方法。
- 路由匹配器中的match方法会遍历pathList和pathMap,利用正则表达式查看对应的path是否匹配,如果匹配,这里则返回 _createRoute 函数调用。
-
- pathList和pathMap是在初始化router时生成的,这里为方便理解,再说明一下。
-
- pathList是一个数组,记录着我们初始化时定义的各个url path,例如’/foo’,‘/bar’
-
- pathMap是一个哈希结构,key为path,value为相关的数据(比如path, component, regex等等)。
- _createRoute 函数又调用了 ./util/route.js 的 createRoute 函数。它返回了一个被冻结的对象,里面主要有path和matched属性,matched在这里可以看做等于 [record]。
- 所以this,match方法返回的是一个和我们输入的路径to匹配的一个route对象,里面有我们需要的path,record等内容。
// ./router.js
export default class VueRouter {
...
match (raw: RawLocation, current?: Route, redirectedFrom?: Location): Route {
return this.matcher.match(raw, current, redirectedFrom)
}
}
// ./create-matcher.js
export function createMatcher(...) {
...
function match {
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
}: Route {
// 由于传入的raw是已经标准化过的,所以这里的location和raw没有任何区别
const location = normalizedLocation(raw, currentRoute, false, router)
const { name } = location
if (name) {
...
} else if (location.path) {
location.params = {}
for (let i = 0; i < pathList.length; i++) {
const path = pathList[i]
const record = pathMap[path]
if (matchRoute(record.regex, location.path, location.param)) {
return _createRoute(record, location, redirectedFrom)
}
}
}
}
function matchRoute(
regex: RouteRegExp,
path: string,
params: Object
): boolean {
const m = path.match(regex)
if (!m) {
return false
} else if (!params) {
return true
}
...
return true
}
function _createRoute(
record: ?RouteRecord,
location: Location,
redirectedFrom?: Location
): Route {
...
return createRoute(record, location. redirectedFrom, router)
}
}
// ./util/route.js
export function createRoute(
record: ?RouteRecord,
location: Location,
redirectedFrom?: ?Location,
router?: VueRouter
): Route {
const route: Route = {
path: location.path || '/',
matched: record ? formatMatch(record) :[], // 这里可以先简单理解为 [record]
...
}
return Object.freeze(route)
}
createHref函数分析
- 对应代码 href = createHref(base, fullPath, this.mode),对应base = undefine, fullPath = ‘/foo’, this.mode = ‘hash’。
- 在vue-route中,默认为hash模式,所以所有的的path前面都会带一个#号
- 这个#号就是在这个函数中体现的
// ./router.js
function createHref(base: string, fullPath: string, mode) {
var path = mode === 'hash' ? '#' + fullPath : fullPath
return base ? cleanPath(base + '/' + path) : path
}
总结
- recolve返回的对象里面的内容主要为location, route , href。
- location是标准化后的to,并且打上了标记表示已标准化,防止多次标准化,提升效率。
- route是通过遍历pathList和pathMap,利用正则表达式找到的和to匹配的路由对象,里面包含很多需要的内容。
- href在默认的hash模式下,会在to的前面加上#号,例如这里的’#/foo’。
// ./router.js
export default class VueRouter {
...
resolve(to, current, append) {
const location = normalizeLocation(to, current, append, this)
const route = this.match(location, current)
const fullPath = route.redirectedFrom || route.fullPath
const base = this.history.base
const href = createHref(base, fullPath, this.mode)
return {
location,
route,
href,
...
}
}
}
- 后续的内容就是vue利用h函数将link组件渲染为html的内容,例如设置类名,定义handler函数处理跳转,绑定点击事件,指定 a 标签等等。
- 这里再对vue-router官方文档中提到的匹配成功后自动设置的class属性值,通过源码分析一下:
-
- 通过下面的代码可以看出,如果你不想要 router-link-active 的类名,可以在初始化router时,加入options.linkActiveClass属性就可以了