VueRouter原理

路由

路由概念最早出现在后端

后端路由
1
http://www.xxx.com/login
  • 浏览器发出请求
  • 服务器监听端口请求,解析url路径
  • 根据服务器的路由配置,返回相应信息
  • 浏览器根据数据包的content-type决定怎么解析数据

简单来说,路由就是用来跟后端的一种交互方式,通过不同路径,请求不同资源.

前端路由
  • hash模式
    随着ajax的流行,页面异步请求交互实现了无刷新,现在单页应用的流行,页面交互和跳转都不用刷新了.单页应用是实现,依赖于前端路由.
    前端路由原理很简单,就是通过url的变化,在特定位置渲染需要的页面内容.但url变化都会造成页面刷新,所以出现了通过监听hash变化同时不会引起页面刷新.

    1
    2
    // http://www.xxx.com/#/login
    location.hash // #/login

    hash的变化不会引起重新请求和页面刷新,通过监听hashchange事件,实现逻辑操作

  • history模式
    history是H5标准,多了两个API,pushState和replaceState,以及监听事件popstate,通过这两个API可以改变url且不发送请求,这种方式没有#,更美观.但刷新页面时,还是会向服务器发请求,此时如果不是根路径,就会404,因此,需要服务端配合,将所有路径重定向到根页面.


vue-router实现

vue-router是vue插件,通过两种形式实现前端路由,下面通过源码,分析其中原理逻辑
首先,从VueRouter构造函数入手

1
2
3
4
const router = new VueRouter({
mode: 'history',
routes: [...]
})

精简代码分析如下

1
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
/* @flow */

import { install } from './install'
import { createMatcher } from './create-matcher'

import { HashHistory } from './history/hash'
import { HTML5History } from './history/html5'
import { AbstractHistory } from './history/abstract'

export default class VueRouter {
constructor (options: RouterOptions = {}) {
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []

/*
export function createMatcher (routes, router) {
// 创建路由映射表
const { pathList, pathMap, nameMap } = createRouteMap(routes)

function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
// 路由匹配
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
//...
}
return { // matcher
match,
addRoutes
}
}
*/
// createMatcher:创建路由映射表(pathList, pathMap, nameMap),然后通过闭包的方式让 addRoutes 和 match函数能够使用路由映射表的几个对象,最后返回一个matcher 对象
// this.matcher:路由匹配对象,其中有match和addRoutes方法用来匹配当前路由和添加路由
this.matcher = createMatcher(options.routes || [], this)

let mode = options.mode || 'hash' // 默认hash模式
this.fallback =
mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) { // 如浏览器不支持,'history'模式需回滚为'hash'模式
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract' // 非浏览器环境下运行需强制为'abstract'模式
}
this.mode = mode

switch (mode) { // 根据不同模式,确定history类并实例化.一下三种history类都继承自同一baseHistory类,做了不同是实现.
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
}

match (raw: RawLocation, current?: Route, redirectedFrom?: Location): Route {
return this.matcher.match(raw, current, redirectedFrom)
}

get currentRoute (): ?Route {
return this.history && this.history.current
}

init (app: any /* Vue component instance */) {
/*
export function install (Vue) { VueRouter的install方法
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
this._router = this.$options.router
this._router.init(this)
Vue.util.defineReactive(this, '_route', this._router.history.current) 响应化_route
}
registerInstance(this, this)
},
})
}
*/
// 初始化,注意初始化时机是在new Vue()时触发的,因为Vue.use(VueRouter),vuRouter作为插件安装,
//在VueRouter的install方法中Vue.mixin({})混入_router并init

this.apps.push(app) // app即Vue组件实例,此处维护了所有可能产生的组件实例

app.$once('hook:destroyed', () => {
const index = this.apps.indexOf(app)
if (index > -1) this.apps.splice(index, 1)
if (this.app === app) this.app = this.apps[0] || null

if (!this.app) this.history.teardown()
})

if (this.app) {
return
}

this.app = app // this.app是根组件实例

const history = this.history

if (history instanceof HTML5History || history instanceof HashHistory) { // 路由跳转相关逻辑
/* history类简码
history{
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const route = this.router.match(location, this.current)
this.confirmTransition(route, () => {
this.updateRoute(route)
...
})
}
updateRoute (route: Route) { 更新方法,调用history.listen
this.cb && this.cb(route)
}
listen (cb: Function) { 生成更新方法的回调函数
this.cb = cb
}
}

*/
history.transitionTo( // 其中包括push,replace等操作路由方法
history.getCurrentLocation(),
setupListeners,
setupListeners
)
}

history.listen(route => { //history的回调函数,更新组件_route值,确保每个组件都能获得相同的当前路由,在init初始化时,混入过程中将_route添加到app中,且实现响应式.所以_route的变化会引起render
this.apps.forEach(app => {
app._route = route
})
})
}

// 钩子hook
beforeEach (fn: Function): Function {
return registerHook(this.beforeHooks, fn)
}
beforeResolve ...
afterEach ...
onReady ...

// 路由操作方法,这里只是代理了history中的方法,而history中的方法是根据不同模式继承自bastHistory的
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
this.history.push(location, resolve, reject)
})
} else {
this.history.push(location, onComplete, onAbort)
}
}
replace ...
go ...
back ...
forward ...
}
VueRouter.install = install // install方法通过上边引入并添加为VueRouter的静态方法

if (inBrowser && window.Vue) { // 在浏览器环境且实例化Vue后自动安装VueRouter插件
window.Vue.use(VueRouter)
}



从设置路由改变到视图更新的流程如下:

1
$router.push() --> HashHistory.push()/HTML5History.push() --> History.transitionTo() --> History.updateRoute() --> {app._route = route} --> vm.render()

对比hash和history模式
  1. 首先看push,replace方法的实现方式

因为两种模式用的api是不同的,所以push,replace的实现肯定也不一样.

1
2
3
4
5
6
7
// hash模式
window.location.hash = path
window.location.replace(window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path)

// history模式
window.history.pushState(stateObject, title, URL)
window.history.replaceState(stateObject, title, URL)

注意,改变路由的方式还可能直接修改浏览器地址栏url,此时需要监听地址栏,路由跳转方式对应两种模式的replace方法.

  1. 选哪种模式更好

hash优势:
如果开发纯本地页面,能够实现路由正常跳转,history模式不行,因为一刷新可能就404了.
history优势:
没有#,比较美观;
pushState设置的新URL可以是与当前URL同源的任意URL;而hash只可修改#后面的部分,故只可设置与当前同文档的URL
pushState设置的新URL可以与当前URL一模一样,这样也会把记录添加到栈中;而hash设置的新值必须与原来不一样才会触发记录添加到栈中
pushState通过stateObject可以添加任意类型的数据到记录中;而hash只可添加短字符串
pushState可额外设置title属性供后续使用


  • router-view

    1
    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
    59
    60
    61
    62

    export default {
    name: 'RouterView',
    functional: true, // 功能组件
    props: {
    name: {
    type: String,
    default: 'default'
    }
    },
    render (_, { props, children, parent, data }) {
    // 解决嵌套深度问题
    data.routerView = true

    const h = parent.$createElement
    const name = props.name
    const route = parent.$route
    const cache = parent._routerViewCache || (parent._routerViewCache = {})

    let depth = 0 // 当前路由深度
    let inactive = false // 是否keep-alive并且虚拟dom树是没有改变的
    while (parent && parent._routerRoot !== parent) {
    const vnodeData = parent.$vnode ? parent.$vnode.data : {}
    if (vnodeData.routerView) {
    depth++
    }
    if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
    inactive = true
    }
    parent = parent.$parent
    }
    data.routerViewDepth = depth

    // render previous view if the tree is inactive and kept-alive
    if (inactive) { // 直接渲染已经缓存的组件
    const cachedData = cache[name]
    const cachedComponent = cachedData && cachedData.component // 获取缓存的组件
    if (cachedComponent) {
    if (cachedData.configProps) {
    fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
    }
    return h(cachedComponent, data, children)
    } else {
    // render previous empty view
    return h()
    }
    }

    const matched = route.matched[depth] // 获取当前路由匹配信息
    const component = matched && matched.components[name] // 获取匹配到的组件
    if (!matched || !component) { // 没有获取路由信息,渲染空组件
    cache[name] = null
    return h()
    }
    // cache component
    cache[name] = { component }

    const configProps = matched.props && matched.props[name]
    return h(component, data, children)
    }
    }

  • router-link

    1
    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
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    export default {
    name: 'RouterLink',
    props: { // 具体参数说明见文档
    to: {
    type: toTypes,
    required: true
    },
    tag: { // 默认将router-link渲染成a标签
    type: String,
    default: 'a'
    },
    custom: Boolean,
    exact: Boolean,
    exactPath: Boolean,
    append: Boolean,
    replace: Boolean,
    activeClass: String,
    exactActiveClass: String,
    ariaCurrentValue: {
    type: String,
    default: 'page'
    },
    event: {
    type: eventTypes,
    default: 'click'
    }
    },
    render (h: Function) {
    const router = this.$router
    const current = this.$route
    const { location, route, href } = router.resolve(
    this.to,
    current,
    this.append
    )

    // 省略关于activeClass,exactActiveClass的逻辑
    // ...

    // 处理跳转事件,push和replace,并限制一些默认事件
    const handler = e => {
    if (guardEvent(e)) {
    if (this.replace) {
    router.replace(location, noop)
    } else {
    router.push(location, noop)
    }
    }
    }
    const on = { click: guardEvent }
    if (Array.isArray(this.event)) {
    this.event.forEach(e => {
    on[e] = handler
    })
    } else {
    on[this.event] = handler
    }

    const data: any = { class: classes }
    // 插槽逻辑
    const scopedSlot =
    !this.$scopedSlots.$hasNormal &&
    this.$scopedSlots.default &&
    this.$scopedSlots.default({
    href,
    route,
    navigate: handler,
    isActive: classes[activeClass],
    isExactActive: classes[exactActiveClass]
    })

    // 将router-link渲染成什么标签逻辑
    if (this.tag === 'a') {
    data.on = on
    data.attrs = { href, 'aria-current': ariaCurrentValue }
    } else {
    // find the first <a> child and apply listener and href
    const a = findAnchor(this.$slots.default)
    if (a) {
    // in case the <a> is a static node
    a.isStatic = false
    const aData = (a.data = extend({}, a.data))
    aData.on = aData.on || {}
    // transform existing events in both objects into arrays so we can push later
    for (const event in aData.on) {
    const handler = aData.on[event]
    if (event in on) {
    aData.on[event] = Array.isArray(handler) ? handler : [handler]
    }
    }
    // append new listeners for router-link
    for (const event in on) {
    if (event in aData.on) {
    // on[event] is always a function
    aData.on[event].push(on[event])
    } else {
    aData.on[event] = handler
    }
    }

    const aAttrs = (a.data.attrs = extend({}, a.data.attrs))
    aAttrs.href = href
    aAttrs['aria-current'] = ariaCurrentValue
    } else {
    // doesn't have <a> child, apply listener to self
    data.on = on
    }
    }
    // 创建元素
    return h(this.tag, data, this.$slots.default)
    }
    }

参考:

从vue-router看前端路由的两种实现
vue-router源码分析-整体流程
vue-router 源码分析-history
VueRouter 源码深度解析
前端路由简介以及vue-router实现原理