miniVueRouter
小米糖糖 11/14/2020 vue-routerminivue
# VueRouter 原理
利用 Vue 的响应式属性,保存当前路由
利用浏览器监听 hash 或者 url 变化,更新响应式的路由信息
利用 mixIn 给组件添加生命周期初始化路由
将用户配置的路由信息生成路由表并进行保存
注册 router-link router-view 组件,在内部通过当前路由信息匹配路由表从而进行渲染
嵌套路由,这个稍微比较麻烦,在源码中,路由表是嵌套递归的,每次匹配时会生成一个 match 数组,对应索引就是层级,在 route-view 中根据对应层级,展示对应组件
# mini 实现效果
# mini 实现代码
实现较为简单,并且只做了 hash 模式,未实现嵌套路由
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<!-- <script src="https://unpkg.com/vue-router@2.0.0/dist/vue-router.js"></script> -->
<title>miniVueRouter</title>
</head>
<body>
<div id="app">
<h2>VueRouterDemo</h2>
<router-link to='/'>home</router-link>
<router-link to='/about'>about</router-link>
<hr>
<router-view></router-view>
</div>
</body>
<script src="./router-link.js"></script>
<script src="./router-view.js"></script>
<script src="./miniVueRouter.js"></script>
<script src="./component.js"></script>
<script src="./app.js"></script>
</html>
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
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
- mini 版本 引用了 vue,作为 router 原理简单讲解
- 文件进行了拆分,组件单独拆到对应的文件,分模块查看更加清晰
app.js 与正常使用一致,将 vuerouter 替换为官方的 VueRouter 可以正常运行
app.js
// 正常使用即可
Vue.use(VueRouter)
const router = new VueRouter({
mode: "hash",
routes: [
{ path: "/", component: Home },
{ path: "/about", component: About },
]
})
const app = new Vue({ el: "#app", router })
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
component.js
const Home = {
template: "<div>{{title}}</div>",
data() { return { title: '这是Home' } }
}
const About = {
template: "<div>{{title}}</div>",
data() { return { title: '这是About' } }
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 重要部分
miniVueRouter.js
let _Vue
class VueRouter {
constructor(options) {
this.$options = options
this.init()
}
// 在源码里比这个复杂得多,通过 createMatcher 创建一个闭包, 返回一个 {match,addRoutes}
// 其他地方用的都是 这个叫做matcher的对象,所以 才有了element-admin中动态路由的hach方式
// 直接生成一个新的VueRouter 使用其matcher对正在使用的matcher进行一个替换 就做到了更新
createMatcher(routes = []) {
console.log(routes)
this.pathObj = {}
routes.forEach(({ path, component }) => {
this.pathObj[path] = _Vue.extend(component)
})
}
match(path) {
path = path || this.current
return this.pathObj[path]
}
initEvent() {
const hashChange = e => {
this.current = location.hash.slice(1) || '/'
}
window.addEventListener('hashchange', hashChange)
hashChange()
}
init() {
this.createMatcher(this.$options.routes)
this.initEvent()
Vue.util.defineReactive(this, 'current', '/')
}
}
VueRouter.install = function (Vue, options) {
_Vue = Vue
Vue.mixin({
beforeCreate() {
if (this.$options.router) {
Vue.prototype.$router = this.$options.router
this.$router.init(this)
}
}
})
Vue.component('router-link', RouterLink)
Vue.component('router-view', RouterView)
}
if (window && window.Vue) {
Vue.use(VueRouter)
}
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
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
在源码里比这个复杂得多,通过 createMatcher 创建一个闭包, 返回一个 {match,addRoutes}
其他地方用的都是 这个叫做 matcher 的对象,所以 才有了 element-admin 中动态路由的 hach 方式
直接生成一个新的 VueRouter 使用其 matcher 对正在使用的 matcher 进行一个替换 就做到了更新
router-link.js
const RouterLink = {
functional: true,
props: {
to: { type: [String, Object], default: "" }
},
render(h, context) {
const handleClick = (e) => {
e.preventDefault()
location.hash = context.props.to
Vue.prototype.$router.current = context.props.to
}
return h('a', { attrs: { href: context.props.to }, on: { click: handleClick } }, context.children)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
router-view.js
const RouterView = {
functional: true,
render(h, context) {
const com = Vue.prototype.$router.match()
return h(com)
}
}
1
2
3
4
5
6
2
3
4
5
6
# 部分源码
源码中处理嵌套部分
// router-view 的render 部分代码
let depth = 0
let inactive = false
// 只要父组件不是根组件就继续遍历
while (parent && parent._routerRoot !== parent) {
const vnodeData = parent.$vnode ? parent.$vnode.data : {}
// 只要发现routerView 就把深度加一。。。。以匹配嵌套路由
if (vnodeData.routerView) {
depth++
}
...
const matched = route.matched[depth]
const component = matched && matched.components[name]
...
return h(component, data, children)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
createMatcher 源码
export function createMatcher(routes: Array<RouteConfig>, router: VueRouter): Matcher {
// 根据传递的routes 递归加入生成一个 path列表,并把*放在最后
const { pathList, pathMap, nameMap } = createRouteMap(routes)
// addRoutes就是继续加路由,,,
function addRoutes(routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
function match(raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location): Route {
// 调用match的时候是新建一个route对象,该对象有一个matched matched是通过recoed 由下至上parent 组成的数组 嵌套路由比较有用
const location = normalizeLocation(raw, currentRoute, false, router)
const { name } = location
if (name) {
//如果解析到了名字属性,直接通过名字查找 record
const record = nameMap[name]
if (process.env.NODE_ENV !== 'production') {
warn(record, `Route with name '${name}' does not exist`)
}
if (!record) return _createRoute(null, location)
const paramNames = record.regex.keys.filter((key) => !key.optional).map((key) => key.name)
if (typeof location.params !== 'object') {
location.params = {}
}
if (currentRoute && typeof currentRoute.params === 'object') {
for (const key in currentRoute.params) {
if (!(key in location.params) && paramNames.indexOf(key) > -1) {
location.params[key] = currentRoute.params[key]
}
}
}
location.path = fillParams(record.path, location.params, `named route "${name}"`)
return _createRoute(record, location, redirectedFrom)
} else if (location.path) {
location.params = {}
for (let i = 0; i < pathList.length; i++) {
const path = pathList[i]
const record = pathMap[path]
// 内部通过 record的正则表达式进行匹配 判断是不是目标路由 如果是 就通过record 生成新的$route
// $route 不是 record 扩展了很多东西 比如 matched
if (matchRoute(record.regex, location.path, location.params)) {
return _createRoute(record, location, redirectedFrom)
}
}
}
// no match
return _createRoute(null, location)
}
function redirect(record: RouteRecord, location: Location): Route {
const originalRedirect = record.redirect
let redirect = typeof originalRedirect === 'function' ? originalRedirect(createRoute(record, location, null, router)) : originalRedirect
if (typeof redirect === 'string') {
redirect = { path: redirect }
}
if (!redirect || typeof redirect !== 'object') {
if (process.env.NODE_ENV !== 'production') {
warn(false, `invalid redirect option: ${JSON.stringify(redirect)}`)
}
return _createRoute(null, location)
}
const re: Object = redirect
const { name, path } = re
let { query, hash, params } = location
query = re.hasOwnProperty('query') ? re.query : query
hash = re.hasOwnProperty('hash') ? re.hash : hash
params = re.hasOwnProperty('params') ? re.params : params
if (name) {
// resolved named direct
const targetRecord = nameMap[name]
if (process.env.NODE_ENV !== 'production') {
assert(targetRecord, `redirect failed: named route "${name}" not found.`)
}
return match(
{
_normalized: true,
name,
query,
hash,
params,
},
undefined,
location
)
} else if (path) {
// 1. resolve relative redirect
const rawPath = resolveRecordPath(path, record)
// 2. resolve params
const resolvedPath = fillParams(rawPath, params, `redirect route with path "${rawPath}"`)
// 3. rematch with existing query and hash
return match(
{
_normalized: true,
path: resolvedPath,
query,
hash,
},
undefined,
location
)
} else {
if (process.env.NODE_ENV !== 'production') {
warn(false, `invalid redirect option: ${JSON.stringify(redirect)}`)
}
return _createRoute(null, location)
}
}
function alias(record: RouteRecord, location: Location, matchAs: string): Route {
const aliasedPath = fillParams(matchAs, location.params, `aliased route with path "${matchAs}"`)
const aliasedMatch = match({
_normalized: true,
path: aliasedPath,
})
if (aliasedMatch) {
const matched = aliasedMatch.matched
const aliasedRecord = matched[matched.length - 1]
location.params = aliasedMatch.params
return _createRoute(aliasedRecord, location)
}
return _createRoute(null, location)
}
function _createRoute(record: ?RouteRecord, location: Location, redirectedFrom?: Location): Route {
if (record && record.redirect) {
return redirect(record, redirectedFrom || location)
}
if (record && record.matchAs) {
return alias(record, location, record.matchAs)
}
return createRoute(record, location, redirectedFrom, router)
}
return {
match,
addRoutes,
}
}
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
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
- 可以看到 接收的是 用户配置的 routes,以及 router 实例
- 最终返回的是 match, addRoutes,
- 在 router 中使用的匹配相关都是使用的此对象
- 所以动态生成路由原理就是替换为新的 matcher