# VueRouter hash 模拟实现
本文通 path-to-regexp 来实现 Vue-Router 的 hash 模拟实现。
# 路由
我们大多数人都知道单页应用程序路由是怎么回事。要点就是把 URL 映射到组件上。因此这些练习的目标实际上是从头开始,以及如何在视图上下文中实现它。
- 简单的路由 (hashchange +
<component :is />
) - 提取路由表
- 通过
path-to-regexp
进行 URL 匹配
# Basic Hash Router
让我们来看一个非常简单的基于 hash 的路由解决方案。它甚至不是一个路由管理器,它只是使用直接浏览器 API 的路由。在浏览器中有两种路由方式,分别是 hash 和 HTML5 的 history API。后者在某种意义上更好,它支持 pop 状态,你可以得到更好的 url,但它需要进行一些服务器配置。这里为了练习我们只是用 hash 路由。
- 当 url 为
#foo
时,页面呈现 foo - 当 url 为
#bar
时,页面呈现 bar - 实现在
#foo
和 `#bar 之间的导航链接
<div id="app">
<!-- render main view here -->
<a href="#foo">foo</a>
<a href="#bar">bar</a>
</div>
<script>
window.addEventListener('hashchange', () => {
// Implement this!
})
const app = new Vue({
el: '#app',
// Implement this!
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
代码实现:
<div id="app">
<component :is="url"></component>
<a href="#foo">foo</a>
<a href="#bar">bar</a>
</div>
<script>
window.addEventListener('hashchange', () => {
app.url = window.location.hash.slice(1);
})
const app = new Vue({
el: '#app',
data: {
url: window.location.hash.slice(1)
},
components:{
bar: {template: `<div>bar</div>` },
foo:{template: `<div>foo</div>`}
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Route Table
上面的例子很简单,但是我们注意到我们的配置本质上是硬编码的。理想情况下,你有一个应用程序,你有一些路由配置。你想要这个路由以某种方式被提取出来,这样你就可以有一个路由表。你知道哪条路径对应什么,上面的例子并没有实现这个功能。
因此这个练习就是基于上述练习来实现这个配置功能。
<div id="app">
</div>
<script>
// '#/foo' -> Foo
// '#/bar' -> Bar
// '#/404' -> NotFound
const Foo = { template: `<div>foo</div>` }
const Bar = { template: `<div>bar</div>` }
const NotFound = { template: `<div>not found!</div>` }
const routeTable = {
// Implement this!
}
window.addEventListener('hashchange', () => {
// Implement this!
})
const app = new Vue({
el: '#app',
// Implement this!
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
代码实现:
const routeTable = {
'foo': Foo,
'bar': Bar
}
window.addEventListener('hashchange', () => {
app.url = window.location.hash.slice(1);
})
const app = new Vue({
el: '#app',
data: {
url: window.location.hash.slice(1)
},
render(h) {
return h('div', [
h(routeTable[this.url] || NotFound),
h('a', { attrs: { href: '#bar' } }, 'bar'),
' | ',
h('a', { attrs: { href: '#foo' } }, 'foo')
])
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Path to Regular Expressions
我们已经实现了一些基本的路由,但是实际的应用程序要复杂的多,例如 URL 地址可能如下所示:
foo/123
或者使用冒号的动态路由字段 user/:username
,我们希望提取这个动态字段,并将其作为 prop 或某种数据传递给组件。组件需要能够获取当前的动态字段,如路由是 /user/123
,那么传递到组件中的数据可能 { username: '123' }
,还有可能是以下这种形式:
'/user/123?foo=bar'
// 将原始 url 变为 JavaScript 数据结构,而不是在任何地方都进行解析
{
path: '/user/123',
params: { username: '123' },
query: { foo: 'bar' }
}
2
3
4
5
6
7
所以这是很常见的需求,在后面,我们将尝试处理其中的一些。我们不会像上面构造一个完整的route 对象。我们要做的就是像下面这样获取动态路径参数。
'/user/:username'
'/user/123'
{ username: 123 }
2
3
我们可以手写一个正则表达式来获取动态路由中的参数,但是可以使用已有的库 path to regexp 来实现我们想要的功能。这是一个非常流行的包,并被用在相当多路由管理器中。更多详细使用方法查看相关文档。
# Dynamic Routes
在这个练习中,我们还是有 Foo、Bar 和 NotFound 组件,区别是本次练习 foo 有一个动态路由,例如当 hash 为 /foo/123
时,Foo 组件接收一个 prop 为 123,并将其输出。初始代码如下所示:
<script src="../node_modules/vue/dist/vue.js"></script>
<script src="./path-to-regexp.js"></script>
<div id="app"></div>
<script>
// '#/foo/123' -> foo with id: 123
// '#/bar' -> Bar
// '#/404' -> NotFound
// path-to-regexp usage:
// const regex = pathToRegexp(pattern)
// const match = regex.exec(path)
// const params = keys.reduce((params, key, index) => {
// params[key] = match[index + 1]
// }, {})
const Foo = {
props: ['id'],
template: `<div>foo with id: {{ id }}</div>`
}
const Bar = { template: `<div>bar</div>` }
const NotFound = { template: `<div>not found!</div>` }
const routeTable = {
// Implement this!
}
window.addEventListener('hashchange', () => {
// Implement this!
})
const app = new Vue({
el: '#app',
data: {
url: window.location.hash.slice(1)
},
render (h) {
const path = '/' + this.url
let componentToRender
let props = {}
// Implement the logic to figure out proper values
// for componentToRender and props
return h('div', [
h(componentToRender, { props }),
h('a', { attrs: { href: '#foo/123' }}, 'foo'),
h('a', { attrs: { href: '#foo/234' }}, 'foo'),
' | ',
h('a', { attrs: { href: '#bar' }}, 'bar')
])
}
})
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
代码实现:
const routeTable = {
'/foo/:id': Foo,
'/bar': Bar
}
const compileRoutes = [];
Object.keys(routeTable).forEach(path => {
let dynamicSegments = [];
const regex = pathToRegexp(path, dynamicSegments);
const component = routeTable[path];
compileRoutes.push({
component,
regex,
dynamicSegments
});
})
/**
const compileRoutes = {};
Object.keys(routeTable).forEach(path => {
let dynamicSegements = [];
compileRoutes[path] = {
regex: pathToRegexp(path, dynamicSegements),
component: routeTable[path],
dynamicSegements
};
})
*/
window.addEventListener('hashchange', () => {
app.url = window.location.hash.slice(1);
})
const app = new Vue({
el: '#app',
data: {
url: window.location.hash.slice(1)
},
render(h) {
const path = '/' + this.url
let componentToRender
let props = {}
compileRoutes.some(route => {
const match = route.regex.exec(path);
if (match) {
componentToRender = route.component;
route.dynamicSegments.forEach((segment, index) => {
props[segment.name] = match[index + 1];
})
return true;
}
})
/**
Object.keys(compileRoutes).some(pattern => {
const { regex, component, dynamicSegements } = compileRoutes[pattern];
const match = regex.exec(path);
if (match) {
componentToRender = component;
dynamicSegements.forEach(({ name }, index) => {
props[name] = match[index + 1];
})
return true;
}
})
*/
return h('div', [
h(componentToRender || NotFound, { props }),
h('a', { attrs: { href: '#foo/123' } }, 'foo123'),
' | ',
h('a', { attrs: { href: '#foo/234' } }, 'foo234'),
' | ',
h('a', { attrs: { href: '#bar' } }, 'bar')
])
}
})
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