前言
如今 React、Vue 等这些框架大行其道,为前端开发提供了不少的便利性。用这些框架开发的应用叫单页面应用(即 SPA),单页面应用就是只有一张Web页面的应用,是一种从 Web 服务器加载的富客户端,单页面跳转仅刷新局部资源 ,公共资源(JS、CSS 等)仅需加载一次,不同的页面只是挂载的在根节点上的内容不一样,那是怎么实现不同的 url 地址对应不同的页面呢,这就前端路由的作用了。
接下来本文将围绕以下三个问题来进行阐述:
- 单页面应用为什么需要路由系统?
- 单页面应用路由的原理是什么?
- vue-router 是怎么实现的?
单页面为什么需要路由系统
最开始的时候,开发网页还是前后端不分离的,意思就是 html 页面是写在后端语言中的,然后用模板引擎进行渲染。后面随着 ajax 的流行,前端获取到数据能不用刷新页面,后面慢慢有了 React、Vue 等 SPA 框架。这些框架不仅能够在不刷新页面的情况下进行交互,还能在页面之间的跳转也不刷新页面,这里面路由系统就做出了很大的贡献,如果 SPA 框架没有路由系统,就会有很多问题:
- 用户在使用的过程中,url 不会变化,那么用户在进行多次跳转之后,如果不小心刷新了页面,就会回到最开始的页面,这样用户的体验是很不好的。
- 百度、谷歌的爬虫是根据路径来爬网页的内容的,这样就不利于 SEO 和搜索引擎的收录。
现在的前端路由系统主要分为 hash 模式和 history 模式。
Hash 模式
hash 路由的一个明显的标志就是 url 上带有 #
。#
其实是 url 的锚点,代表的是网页中的一个位置,单单改变 #
后的部分,浏览器只会滚动到相应位置,不会重新加载网页,同时每一次改变 #
后的部分,都会在浏览器的访问历史中增加一个记录,使用浏览器自带的前进后退按钮,就可以回到上一个位置。而且,url 的 hash 值的改变还会触发 hashchange
事件。
总的来说,因为 url 中 hash 值的改变不会引起页面的刷新,而且还能触发 hashchange
这个事件,这样就可以很好的用来做单页面应用的路由。
所以,我们可以根据这个思路来实现一个简单的 hash 路由: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// router.js
class Router {
constructor() {
this.routes = {}; // 用来存放不同路由对应的回掉函数
this.currentUrl = ''; // 当前路由的 url
}
// 将路径和对应的回调函数存储起来
route(path, callback) {
this.routes[path] = callback || function() {};
}
// 更新视图
updateView() {
// 获取当前 url 中的 hash 路径
this.currentUrl = location.hash.slice(1) || '/';
// 执行当前 hash 路径的回调函数
this.routes[this.currentUrl] && this.routes[this.currentUrl]();
}
// 初始化,监听 load 和 hashchange 事件
init() {
window.addEventListener('load', this.updateView.bind(this), false);
window.addEventListener('hashchange', this.updateView.bind(this), false);
}
}
1 | <!-- index.html --> |
这样在对应的 html 中,只要在所有的链接路径前加 #
即可做成软路由,这样就不会触发刷新页面。
需要注意的是,在第一次进入页面的时候,需要触发一次 hashchange
事件,保证页面能够正常显示。用 hash 做路由跳转的好处就是实现起来比较简单,而且便于理解,但是,它虽然解决了单页面应用路由控制的问题,但是在 url 中却引入了 #
,这样就导致 url 不美观。接下来的 history 模式就解决了这个问题。
History 模式
上面说到的 hash 模式虽然能很好的解决问题,但是导致 url 不够美观,到 2014 年 HTML5 的新规范的发布,才很好的解决了这个问题。HTML5 中新增加了两个 API,pushState
和 replaceState
,通过这两个 API 可以改变 url 且不会刷新页面。
比如,当你执行 history.pushState({}, null, '/about')
的时候,页面 url 会从 http://xxx
跳转到 http://xxx/about
。
先简单看看 pushState
的用法:
- state: 通常是一个对象,可以用在
popstate
事件中 - title: 通常会忽略这个参数,可直接用
null
代替 - url: 任意有效的 url ,用于更新浏览器的地址
想了解 pushState
的更多信息可以戳这里。
这样看来,history 也有着控制路由的能力,hash 的改变可以触发 hashchange
事件,而 history 的改变不会触发任何事件,这样就无法直接监听 history 的改变从而做出相应的改变。所以,我们需要换个思路,我们可以列出所有可能触发 history 改变的情况,然后将这些方式一一进行拦截,这就变相的实现了对 history 改变的监听。
对一个应用而言,url 的改变(不包括 hash 值的改变)只能由下面三种情况引起:
- 点击
a
标签 - 点击浏览器的前进后退
- 在 JS 代码中触发
history.pushState
或history.replaceState
方法
针对情况2,HTML5 规范中有相应的 popstate
事件,通过它可以监听到前进或者后退按钮的点击,需要注意的是,调用 history.pushState
或 history.replaceState
并不会触发 popstate
事件。
所以,我们可以根据这个思路来实现一个简单的 history 路由: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// router.js
class Router {
constructor() {
this.routes = {}; // 用来存放不同路由对应的回掉函数
this.currentUrl = ''; // 当前路由的 url
}
// 将路径和对应的回调函数存储起来
route(path, callback) {
this.routes[path] = callback || function() {};
}
// 更新视图
updateView() {
// 获取当前 url 中的 hash 路径
this.currentUrl = location.hash.slice(1) || '/';
// 执行当前 hash 路径的回调函数
this.routes[this.currentUrl] && this.routes[this.currentUrl]();
}
// 绑定 DOM 中所有的 a 链接
bindLink() {
let allLink = document.querySelectorAll('a[data-href]');
for (let i = 0, item, len = allLink.length; i < len && (item = allLink[i]); i++) {
item.addEventListener(
'click',
e => {
e.preventDefault();
let url = item.getAttribute('data-href');
history.pushState({}, null, url);
this.updateView(url);
},
false
);
}
}
// 初始化路由,先绑定所有的 a 链接,然后再监听 load 和 popstate 事件
init() {
this.bindLink();
window.addEventListener('load', () => this.updateView('/'), false);
window.addEventListener('popstate', () => {
this.updateView(window.location.pathname);
});
}
}
1 | <!-- index.html --> |