vue-router 模拟嵌套路由
vue-router 源码
src 目录结构
- components
- link.js router-link组件
- view.js router-view组件
- history 历史管理,包含3个模式
- hash.js - hash模式
- html5.js - history模式
- abstract.js - 服务端渲染时使用的抽象模式
- base.js - 上面3个模式的父类,提供一些公共的内容
- errors.js - abstract.js使用的方法
- util 存放一些公共方法
- create-matcher.js 导出 createMatcher 方法
- createMatcher - 创建并返回一个匹配器 matcher,匹配器包含 match 方法和 addRoutes 方法
- match - 根据路由地址匹配相应的路由规则对象
- addRoutes - 动态添加路由
- createMatcher - 创建并返回一个匹配器 matcher,匹配器包含 match 方法和 addRoutes 方法
- create-route-map.js 导出 createRouteMap 方法
- createRouteMap - 把所有的路由规则解析成路由表
- pathList - 一个存储所有路由地址的数组
- pathMap - 路由表
- key - 路由地址
- value - record(路由记录),一个记录路由各种信息的对象
- createRouteMap - 把所有的路由规则解析成路由表
- index.js - 创建VueRouter实例
- install.js - 插件注册的方法
模拟实现
约定
约定下划线 ‘_’ 开头的成员是不希望被外部调用的。
搭建基本结构
- my-vue-router
- components
- link.js
- view.js
- history
- base.js
- html5.js
- hash.js
- util
- route.js
- create-matcher.js
- create-route-map.js
- index.js
- install.js
- components
编写基本代码
// index.js
import install from './install'
export default class VueRouter {
constructor(options) {
this._routes = options.routes || []
}
// 注册路由变化的事件
init(app) {}
}
// 挂载install方法
VueRouter.install = install
// install.js
export let _Vue = null
export default function install (Vue) {
_Vue = Vue
// 混入钩子函数
_Vue.mixin({
beforeCreate () {
// router -> VueRouter实例
// 将router注入到 Vue实例 及其 所有组件
// Vue组件的创建顺序:
// 1. 父组件 beforeCreate create ->
// 2. 子组件 beforeCreate create ->
// 3. 子组件 mounted ->
// 4. 父组件 mounted
if (this.$options.router) {
// 此时this是Vue根实例(最先执行)
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
} else {
// 此时this是组件实例(父组件先于子组件执行)
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
}
})
}
简单实现router-link、router-view组件
简单实现两个组件用于启动测试,之后边测试边扩展。
link.js和view.js导出一个包含组件选项的对象。
当前使用运行时版本的Vue,直接写render(不用template)。
// link,js
export default {
props: {
to: {
type: String,
required: true
}
},
render(h) {
return h(
'a',
{
domProps: {
href: '#' + this.to
}
},
[this.$slots.default]
)
}
}
// view.js
export default {
render(h) {
return h('div', '--router-view')
}
}
// install.js
import View from './components/view'
import Link from './components/link'
export let _Vue = null
export default function install(Vue) {
_Vue = Vue
// 混入钩子函数
_Vue.mixin({
beforeCreate() {
// ...
}
})
// 注册插件
_Vue.component('RouterView', View)
_Vue.component('RouterLink', Link)
}
配置路由规则
在官方示例的基础上增加一些路由及嵌套路由(暂时使用静态路由)。
import Vue from 'vue'
// import VueRouter from "vue-router";
import VueRouter from '../my-vue-router'
import Home from '../views/Home.vue'
import Music from '../views/music/Index.vue'
import Pop from '../views/music/Pop.vue'
import Rock from '../views/music/Rock.vue'
Vue.use(VueRouter)
// 路由规则
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () =>
import(/* webpackChunkName: "about" */ '../views/About.vue')
},
// 新增路由
{
path: '/music',
name: 'Music',
component: Music,
children: [
{
path: 'pop',
name: 'pop',
component: Pop
},
{
path: 'rock',
name: 'rock',
component: Rock
}
]
}
]
const router = new VueRouter({
mode: 'hash',
base: process.env.BASE_URL,
routes
})
export default router
// music/Index.vue
<template>
<div class="music">
<div id="nav">
<router-link to="/music/pop">Pop</router-link> |
<router-link to="/music/rock">Rock</router-link>
</div>
<router-view></router-view>
</div>
</template>
解析路由表
将路由规则解析到一个路由表(routeMap)中。
createMatcher 创建路由匹配器
create-matcher.js 导出的方法。
创建并返回一个匹配器 matcher,匹配器包含 match 方法和 addRoutes 方法
- match - 根据路由地址匹配相应的路由规则对象
- addRoutes - 动态添加路由
这个匹配器会被定义在VueRouter实例的 matcher 属性上。
// create-matcher.js
import createRouteMap from './create-route-map'
export default function createMatcher(routes) {
// 把所有的路由规则,解析到一个路由表中
// pathList --> ['/', '/music', ...]
// pathMap --> { path: { component ....} }
const { pathList, pathMap } = createRouteMap(routes)
// 打印查看结果
console.log(pathList, pathMap)
function match() {}
// addRoutes({ path, component })
function addRoutes(routes) {
// createRouteMap(添加的路由, 加入到的列表, 加入到的映射表)
createRouteMap(routes, pathList, pathMap)
}
return {
match,
addRoutes
}
}
createRouteMap 解析路由规则
create-route-map.js 导出 createRouteMap 方法
把所有的路由规则解析成路由表
- pathList - 一个存储所有路由地址的数组
- pathMap - 路由表
- key - 路由地址
- value - record(路由记录),一个记录路由各种信息的对象
record 是一个路径对应的记录。
// create-route-map.js
export default function createRouteMap(routes, oldPathList, oldPathMap) {
const pathList = oldPathList || []
const pathMap = oldPathMap || {}
// 递归遍历所有的路由规则,解析到路由表中
routes.forEach(route => {
addRouteRecord(route, pathList, pathMap)
})
return {
pathList,
pathMap
}
}
// 解析 route,把解析好的规则放入 pathList pathMap 中
function addRouteRecord(route, pathList, pathMap, parentRecord) {
const path = parentRecord ? `${parentRecord.path}/${route.path}` : route.path
const record = {
path,
component: route.component,
// 如果是子路由的话,记录子路由对应的父 record
parent: parentRecord
}
// 如果已经有了path,相同的path直接跳过
if (!pathMap[path]) {
pathList.push(path)
pathMap[path] = record
}
// 判断 route 中是否有子路由
if (route.children) {
// 遍历子路由,把子路由也添加到路由表中
route.children.forEach(childRoute => {
addRouteRecord(childRoute, pathList, pathMap, record)
})
}
}
match
createMatcher 返回的方法。
根据路由地址,匹配路由记录。
如果是子路由,需要匹配和这个子路由地址相关的组件(父组件),即多个路由记录。
// create-matcher.js
//...
export default function createMatcher(routes) {
// ...
function match(path) {
const record = pathMap[path]
if (record) {
// 返回子路由匹配搭配到的所有相关组件
return createRoute(record, path)
}
// 返回匹配空数组的结果
return createRoute(null, path)
}
}
createRoute
根据路由地址,创建route路由规则对象。
获取path匹配到的所有相关路由。
返回对象:
- path:用于匹配的路径
- matched:匹配到的record
如果是子路由,找到它的所有父路由对应的record,插入到数组的第一项中。
因为渲染(create)组件时,先渲染父组件。
// util/createRoute.js
// 根据path匹配到的所有的record,放到matched数组中
export default function createRoute(record, path) {
// 如果path是子路由,record会有parent属性
// Vue渲染(create)组件时,先渲染父组件
// 此时应该把 parent(parentRecord)放在 record(childRecord)前面
// [ParentRecord, childRecord]
const matched = []
while (record) {
matched.unshift(record)
record = record.parent
}
return {
path,
matched
}
}
History 历史管理
将3种(当前只实现hash 和 html5)模式抽象成单个类。
将它们中相同的部分,抽象到一个父类中(History)。
base.js - History 父类
- router - VueRouter实例
- current - 记录当前路径对应的路由规则对象
- createRoute创建返回的对象:{path, matched}
- transitionTo()
- 跳转到指定的路径,根据当前路径获取匹配的路由规则对象 route,然后更新视图
// history/base.js
import createRoute from '../util/route'
export default class History {
constructor(router) {
this.router = router
// 初始化时为首次加载页面的路径对应('/')的路由
this.current = createRoute(null, '/')
}
// 跳转到其他位置,最终会渲染路由对应的组件
transitionTo(path, onComplete) {
// 重新改变 current
this.current = this.router.matcher.match(path)
// 调试
console.log(this.current)
// 首次跳转完成后,调用onComplete监听hash变化
onComplete && onComplete()
}
}
hash.js - HashHistory 类
- 继承 History
- 确保首次访问地址为 #/
- getCurrentLocation() 获取当前的路由地址(# 后面的部分)
- setupListeners() 监听路由地址改变的事件
// history/hash.js
import History from './base'
export default class HashHistory extends History {
constructor(router) {
super(router)
// 确保 首次 访问的路径是 #/
ensureSlash()
}
// 获取当前的路由地址
getCurrentLocation() {
return window.location.hash.substr(1)
}
// 监听 hashchange 监听路由地址的变化
setupListeners() {
window.addEventListener('hashchange', () => {
this.transitionTo(this.getCurrentLocation())
})
}
}
function ensureSlash() {
// 判断当前是否有 hash
if (window.location.hash) {
return
}
window.location.hash = '/'
}
更新 index.js
// index.js
import install from './install'
import createMatcher from './create-matcher'
import HashHistory from './history/hash'
import HTML5History from './history/html5'
export default class VueRouter {
constructor(options) {
this._routes = options.routes || []
// 匹配器 createMatcher(routes) => {match, addRoutes}
this.matcher = createMatcher(this._routes)
// 根据模式设置不同的类
const mode = options || 'hash'
this.mode = mode
switch (mode) {
case 'hash':
this.history = new HashHistory(this)
break
case 'history':
this.history = new HTML5History(this)
break
default:
throw new Error('mode error')
}
}
// 注册路由变化的事件
init() {
const history = this.history
history.transitionTo(history.getCurrentLocation(), () => {
// 包裹一层是为了使 setupListeners 的调用者是 history
// 即确保this指向history
history.setupListeners()
})
}
}
// 挂载install方法
VueRouter.install = install
渲染 router-view
当前路由(current)发生变化时,重新渲染对应的组件。
给 router 对象设置响应式的 _route 属性
方案对比:
- Vue.observable(Object)
- 创建返回一个响应式的对象,不是向Vue实例设置一个响应式的属性
- Vue.set()
- 无法向Vue实例,或data(根数据对象)添加响应式属性
- Vue.util.defineReactive(Object, key, value)
- 向对象中设置/添加一个响应式的属性,即初始值。
- 类似 defineProperty
- Vue内部方法
// install.js
import View from './components/view'
import Link from './components/link'
export let _Vue = null
export default function install(Vue) {
_Vue = Vue
// 混入钩子函数
_Vue.mixin({
beforeCreate() {
if (this.$options.router) {
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
// 定义一个响应式的属性 _route
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
}
})
// 注册插件
_Vue.component('RouterView', View)
_Vue.component('RouterLink', Link)
}
实现 current改变时重新对 _route 赋值:
创建一个回调变量 cb,用于对 _route 重新赋值。
transitionTo 内部执行这个回调。
创建一个方法 listen,用于设置这个回调。
// history/base.js
import createRoute from '../util/route'
export default class History {
constructor(router) {
this.router = router
this.current = createRoute(null, '/')
// 这个回调函数,在 hashhistory 中赋值,作用是更改 vue实例上的 _route
this.cb = null
}
listen(cb) {
this.cb = cb
}
transitionTo(path, onComplete) {
this.current = this.router.matcher.match(path)
// 调用 cb
this.cb && this.cb(this.current)
onComplete && onComplete()
}
}
// index.js
// import ...
export default class VueRouter {
constructor(options) {
// ..
}
init(app) {
const history = this.history
history.transitionTo(history.getCurrentLocation(), () => {
history.setupListeners()
})
// 添加回调,更新_route
history.listen(route => {
app._route = route
})
}
}
VueRouter.install = install
定义 $route / $router
定义 $route / $router 的目的是让所有的Vue实例和组件都能访问的到。
- $route 路由规则对象
- $router 路由对象(VueRouter实例)
Vue Router 源码直接使用Object.defineProperty向Vue原型添加这两个属性。
注意:它们是只读的(仅设置了get)。
// install.js
import View from './components/view'
import Link from './components/link'
export let _Vue = null
export default function install(Vue) {
_Vue = Vue
_Vue.mixin({
// ...
})
// 定义 $router / $route
Object.defineProperty(Vue.prototype, '$router', {
get() {
return this._routerRoot._router
}
})
Object.defineProperty(Vue.prototype, '$route', {
get() {
return this._routerRoot._route
}
})
// ...
}
router-view
- 获取当前组件的 $route 路由规则对象
- 找到里面的 matched 匹配的 record (获取里面的component)
- 如果没有子路由(/music),matched 匹配到一个,直接渲染对应的组件
- 如果有子路由(/music/pop),matched 匹配到两个 record(第一个是父组件,第二个是子组件)
Vue渲染组件的顺序是:父组件 -> 子组件。
所以render会先创建父路由的compnent。
渲染的模板中有 router-view,再继续渲染子路由的component
matched返回的顺序也是,父组件在前。
通过 this.$parent 的层级获取 matched 中对应的路由记录(record)。
// component/view.js
export default {
render(h) {
// 当前匹配到的路由对象
const route = this.$route
// 记录深度
let depth = 0
// 记录当前组件为 router-view(其他组件不会被匹配到 matched)
this.routerView = true
let parent = this.$parent
// 遍历父组件,获取深度,作为从 matched 获取 record 的依据
while (parent) {
// 如果嵌套了 router-view 组件 深度+1
if (parent.routerView) {
depth++
}
parent = parent.$parent
}
const record = route.matched[depth]
if (!record) {
// h函数不传参,默认创建一个注释节点:<!---->
return h()
}
return h(record.component)
}
}
到此模拟结束。
Vue.util.defineReactive()
简化 defineReactive 源码,解析 Vue 向对象设置响应式的方法
export function defineReactive(obj: Object, key: string, val: any) {
// 获取属性原描述符
const property = Object.getOwnPropertyDescriptor(obj, key)
// 如果属性不可配置,直接返回
if (property && property.configurable === false) {
return
}
// 获取属性预定义的 getter setter
const getter = property && property.get
const setter = property && property.set
// 获取预定义的初始值(!getter || setter 没理解)
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 如果预定义了 getter,直接使用
const value = getter ? getter.call(obj) : val
return value
},
set(newVal) {
// 获取旧值
const value = getter ? getter.call(obj) : val
// 判断值是否变化 !== 判断的是 NaN
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
// 如果属性本身是响应式的,判断是否设置了 setter
// 如果没有设置,则什么也不做
if (getter && !setter) return
// 如果设置了,则调用预定义的 setter
if (setter) {
setter.call(obj, newVal)
} else {
// 如果不是响应式的属性,则直接设置
val = newVal
}
}
})
}