Router-前端路由的简单实现

简单实现前端路由

Talk is cheap, show me the code

路由的概念来源于服务端,而在Web前端的SPA应用中,路由主要用于建立URL和UI的映射关系,即URL 变化引起 UI 更新,无需刷新页面。

前端路由

前端路由主要解决两个问题:

  • URL改变不引起页面刷新
  • 检测URL变化

常用方法有两种: hash mode和history mode

hash mode

hash的来源是锚点,在页面内进行内部导航和跳转用的,也就是#后面的部分

通过hashchange可以监听URL的变化。

改变URL的方式只有以下几种:

  • 浏览器前进后退
  • 通过a标签改变
  • 通过window.location改变

这几种改变会触发hashchange

history mode

history提供pushState和replaceState方法,这两个方法改变path部分不引起页面刷新

history也有类似hashchange事件就是popstate事件。

不同的地方是,通过浏览器前进后退会触发popstate

通常通过pushState/replaceState和a标签改变URL是不会触发popstate
但是可以通过拦截pushState/replaceState和a标签的调用来检测URL变化。

原生实践

基于hash

html部分

1
2
3
4
5
6
7
8
9
// html
<div class="app">
<ul>
<li><a href="#/home">Home</a></li>
<li><a href="#/main">Main</a></li>
<li><a href="#/about">About</a></li>
</ul>
<div id="routerview"></div>
</div>

script部分

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
<script>
window.addEventListener('DOMContentLoaded', onLoad);
window.addEventListener('hashchange', onHashChange);
var routerview = null;
function onLoad() {
routerview = window.document.getElementById('routerview');
onHashChange();
}
function onHashChange() {
console.log(location.hash);
switch (location.hash) {
case '#/home':
routerview.innerHTML = 'Home'
break;
case '#/main':
routerview.innerHTML = 'Main'
break;
case '#/about':
routerview.innerHTML = 'About'
break;
default:
break;
}
}
</script>

基于history

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
27
28
29
30
31
32
33
34
35
36
37
<script>
window.addEventListener('DOMContentLoaded', onLoad);
window.addEventListener('popstate', onPopState);
var routerview = null;
onPopState();
function onLoad() {
routerview = window.document.getElementById('routerview');
var linkList = document.querySelectorAll('a[href]');
console.log(linkList);
linkList.forEach((item) => item.addEventListener('click', (e) => {
e.preventDefault();
console.log(item.getAttribute('href'));
history.pushState(null, '', item.getAttribute('href'));
onPopState();
}))
}
function onPopState() {
console.log(location.pathname);
switch (location.pathname) {
case '/home':
routerview.innerHTML = 'Home'
break;
case '/main':
routerview.innerHTML = 'Main'
break;
case '/about':
routerview.innerHTML = 'About'
break;
default:
break;
}
}
</script>

Vue Router

基于hash

router-link 组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- router link -->
<template>
<a @click.prevent="onClick" href=''>
<slot></slot>
</a>
</template>
<script>
export default {
props: {
to: String
},
methods: {
onClick() {
window.location.hash = '#' + this.to
}
}
}
</script>

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
<!-- router view -->
<template>
<component :is="routeView" />
</template>
<script>
export default {
data() {
return {
routeView: null
}
},
created() {
this.boundHashChange = this.onHashChange.bind(this)
},
beforeMount() {
window.addEventListener('hashchange', this.boundHashChange)
},
mounted() {
this.onHashChange()
},
beforeDestroy() {
window.removeEventListener('hashchange', this.boundHashChange)
},
methods: {
onHashChange() {
const href = window.location.href
const path = href.split('#')[1]
console.log(path);
console.log(this.$root.$routes);
this.routeView = this.$root.$routes[path] || null
console.log('vue:hashchange:', path)
}
}
}
</script>

main.js入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import Vue from 'vue'
import App from './App3.vue'
// import router from './router'
Vue.config.productionTip = false
const routes = {
'/home': {
template: '<h2>Home</h2>'
},
'/main': {
template: '<h2>Main</h2>'
},
'/about': {
template: '<h2>About</h2>'
}
}
Vue.prototype.$routes = routes;
new Vue({
// router,
render: h => h(App)
}).$mount('#app')

基于history

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
<template>
<div>
<a href="" @click.prevent="go">
<slot></slot>
</a>
</div>
</template>
<script>
export default {
props: {
to: {
type: String,
default: '/'
}
},
data() {
return {
};
},
methods: {
go: function() {
history.pushState(null, '', this.to);
this.$root.$emit('popstate');
}
}
}
</script>

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
<template>
<component :is="routeView"></component>
</template>
<script>
export default {
data() {
return {
routeView: null
};
},
created() {
this.boundPopState = this.onPopState.bind(this)
},
beforeMount() {
this.$root.$on('popstate', this.boundPopState)
},
beforeDestroy() {
this.$root.$off('popstate', this.boundPopState)
},
methods: {
onPopState() {
const href = window.location.href;
const path = '/' + href.split('/')[href.split('/').length - 1];
console.log(path);
console.log(this.$root.$routes);
this.routeView = this.$root.$routes[path] || null;
console.log('[Vue] popstate:', path);
}
}
}
</script>

main.js入口文件一样

参考

前端路由原理解析