什么是前端路由?

路由

后端路由

浏览器渲染文章中,我提到了

我们在地址栏键入地址,到看到完整的页面,这个过程中浏览器都经历了写什么?

  1. 查询DNS得到相应ip地址
  2. 与ip对应的服务器握手建立TCP连接
  3. 向服务器发起HTTP请求
  4. 服务器做出相应并返回document
  5. 浏览器接收相应并下载html字符串,随即开始渲染

从第3点浏览器发起请求,到第4点服务器做出响应,经历了一个过程,才能使服务器做出正确响应,这个过程就是路由。

举个栗子:
目前你看到的url是/2016/09/24/router1,这个就是浏览器发起的请求路径,于是,clearaki.xyz对应ip的服务器收到了这个请求,于是服务器的后端代码会引导这个请求到相应的代码做出相应处理,如果能引导成功,返回200并返回数据,如果不存在就返回404。那么这段引导代码就是路由,我们叫后端路由。

打个比方:
你的朋友打电话告诉你,他们在今夜缘KTV的666号房等你,于是你

  1. 打开地图APP找到了KTV的地址
  2. 打了一辆车来到了KTV
  3. 问前台小姐姐666号房在哪
  4. 小姐姐告诉你在6楼电梯口
  5. 你得到消息成功找到小伙伴

我们假设这个前台的小姐姐是新来的,那么她便会:
查看是否有666号房,并查找方位图上的位置
这就是后端路由。

前端路由

最初的雏形

在web1.0时代,每次用户的点击都会请求到新的页面,由于那时候页面简单,重新加载页面也不用多长时间。但随着前端技术的发展,页面开始丰富,浏览网页变得普遍,网页的体积也慢慢增加,于是和发展缓慢的网速形成了矛盾,结果就是——加载白屏。为了解决这个问题,诞生了Ajax技术,使得用户的操作可以部分更新,不用刷新整个网页。

如果我只加载一次,以后都是部分更新,这样体验不是很友好?于是SPA(Single page Web Application)出现了。
我们发现,url中改变#后面的值,也就是hash,是不会向服务器发送请求的,只要改变这个值,并且改变页面中的表现,那看起来的效果就和后端路由一样了!所以,我们把这种方式称为前端路由

实现前端路由

首先整理一下需要处理的地方:

  • 直接键入url到地址栏
  • 刷新
  • 点击a标签
  • 浏览器后退前进

除了刷新操作和第一次键入地址会请求服务器,触发页面生命周期,其他操作均触发hashchange

1
2
3
4
<nav>
<a href="/#/foo">foo</a>
<a href="/#/bar">bar</a>
</nav>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const router = path => {
switch (path) {
case '/foo':
// Todo: 处理页面数据
break;
default:
// 作为404处理
break;
}
}
window.addEventListener('hashchange', e => {
const path = e.newURL.split('#')[1]
router(path)
})
window.addEventListener('DOMContentLoaded', () => {
const path = location.hash.split('#')[1]
if (path) router(path)
else location.hash = '#/foo'
})

一个简单的前端路由就实现了,一个简单的demo

使用html5

使用hash来实现纵然简单方便,但是url上会出现不太和谐的#,如果不兼容远古浏览器,我们可以使用更加灵活的HTML5 API来实现。

History API

1
history.pushState(state, title, url)
  • state:用来储存需要的数据,可以在前进后退操作中得到该数据
  • title:浏览器的标题字符串,相当于改变了HTML的<title>里的内容,不过现代浏览器几乎都忽略了该属性,所以这里一般写null
  • url:浏览器地址栏显示的url

这个操作会向历史记录集合添加一条新的记录,使得可以通过浏览器的前进后退来操作。

1
2
3
4
<nav>
<a href="/foo" data-path="/foo">foo</a>
<a href="/bar" data-path="/bar">bar</a>
</nav>

1
2
3
4
5
6
7
8
9
10
const nav = document.querySelector('nav')
nav.addEventListener('click', e => {
e.preventDefault()
const a = e.target
if (a.tagName === 'A') {
const path = a.dataset.path
history.pushState({ path }, null, path)
router(path)
}
})

而浏览器的前进后退操作,会触发浏览器的前进后退功能,会触发一个popstate事件,我们给该事件绑上方法,就可以处理相应的操作了:

1
2
3
4
window.addEventListener('popstate', e => {
const { state: { path } } = e
router(path)
})

popstate的事件对象里,有一个state的属性,这个属性就是当初用pushState()保存起来的那个对象。

不同于使用hash实现的方式,使用history API时,当用户直接键入地址或刷新时,仍然会向服务请求该路径,为了把权限交给前端,需要服务器将请求重定向到根页面,同样的在页面生命周期里去处理。

1
2
3
4
5
window.addEventListener('DOMContentLoaded', () => {
const path = location.pathname
if (path !== '/') router(path)
else history.replaceState({ path: '/foo' }, null, '/foo')
})

这里用到的API

1
history.replaceState(state, title, url)

pushState()的参数一样,不同的是不会添加新的记录,而是修改当前记录。
另一个简单的demo

参考资料:HTML5 History