浅谈Web路由

关于秋招的一些感想

最近一段时间忙着秋招,一路经历了阿里终面挂,美团终面挂等等,感觉自己有点苦逼(尤其是因为没HC而挂是最难受的)。虽然到现在还没得到满意的结果,但这个过程还是有所收获的,这是虽然一个自我怀疑的过程,但也是一个不断弥补自身不足,学习的过程,其中与一些面试我的师兄在交流的过程中也是受益匪浅,并且也对自身以后要走的路有了一个更加明确的认识,所以前途漫漫还需努力呀!

一、路由

在现代前端开发中,路由是非常重要的一环。但路由到底是什么呢?有些说:路由就是指随着浏览器地址栏的变化,展示给用户的页面也不相同。这是从路由的用途上来解释路由是什么的,还有一种说法是:路由就是URL到函数的映射。这是从路由的实现原理上来解释路由是什么的。这两种说法都很有道理,但我个人认为还是第二种比较切合我自己对路由的理解吧。

上面已经说了上面是路由,而路由本身也经历了不同的发展阶段:

  1. 后端路由
  2. 前端路由

后端路由又可称之为服务器端路由,因为对于服务器来说,当接收到客户端发来的HTTP请求,就会根据所请求的相应URL,来找到相应的映射函数,然后执行该函数,并将函数的返回值发送给客户端。对于最简单的静态资源服务器,可以认为,所有URL的映射函数就是一个文件读取操作。对于动态资源,映射函数可能是一个数据库读取操作,也可能是进行一些数据的处理,等等。然后根据这些读取的数据,在服务器端就使用相应的模板来对页面进行渲染后,再返回渲染完毕的页面。这种方式在早期的前端开发中非常普遍,它的好处与坏处都很明显:

  • 好处:安全性好,SEO好。
  • 缺点:加大服务器的压力,不利于用户体验,代码冗合。

也正是由于后端路由还存在着自己的不足,前端路由才有了属于自己的一片天地与发展的空间。对于前端路由来说,路由的映射函数通常是进行一些DOM的显示和隐藏操作。这样,当访问不同的路径的时候,会显示不同的页面组件。前端路由主要有以下两种实现方案:

  • hash
  • history API

当然,前端路由也存在缺陷:使用浏览器的前进,后退键时会重新发送请求,来获取数据,没有合理地利用缓存。但总的来说,现在前端路由已经是实现路由的主要方式了,我们常用的诸如react-router等前端框架的路由控制都是基于前端路由进行开发的,因此将前端路由进行一个了解
还是很有必要的。

二、前端路由的实现

2.1 基于hash

早期的前端路由的实现就是基于location.hash来实现的。其实现原理也很简单,location.hash的值就是URL中#后面的内容。比如下面这个网站,它的location.hash=’#me’:

https://www.srtian.com#me

此外,hash也存在下面几个特性:

  • URL中hash值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash部分不会被发送。
  • hash值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash的切换。
  • 我们可以使用hashchange事件来监听hash的变化。

出发hsah变化的方式也有两种,一种是通过a标签,并设置href属性,当用户点击这个标签后,URL就会发生改变,也就会触发hashchange事件了:

1
<a href="#srtian">srtian</a>

还有一种方式就是直接使用JavaScript来对loaction.hash进行赋值,从而改变URL,触发hashchange事件:

1
location.hash="#srtian"

2.2 基于History API

前面的hash虽然也很不错,但使用时都需要加上#,并不是很美观。因此到了HTML5,又提供了History API来实现URL的变化。其中做最主要的API有以下两个:history.pushState()和history.repalceState()。

这两个API可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录。此外,这两个api都接受三个参数:

1
window.history.pushState(null, null, "http://www.163.com");
  • 状态对象(state object):一个JavaScript对象,与用pushState()方法创建的新历史记录条目关联。无论何时用户导航到新创建的状态,会触发popstate事件,并能在事件中使用该对象。
  • 标题(title):一般浏览器会忽略,最好传入null。
  • 地址(URL):就是需要新增的历史记录的地址,浏览器不会去直接加载改地址,但后面也可能会去尝试加载该地址。此外需要注意的是,传入的URL与当前URL应该是同源的。

这一块的详情可以去看MDN,它做了一个比较详细的介绍:

https://developer.mozilla.org/en-US/docs/Web/API/History

此外,还提供了popstate事件来监听历史记录的变化。

可以看看下面这个实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<p id="example">
<a href="/name" title="name">name</a>
<a href="/age" title="age">age</a>?
</p>
<div class="main" id="main"></div>
<script>
;(function(){
var examplebox = document.getElementById('example')
var mainbox = document.getElementById('main')

examplebox.addEventListener('click', function(e){
e.preventDefault()
var elm = e.target
var uri = elm.href
var tlt = elm.title
history.pushState({path:uri,title:tlt}, null, uri)
mainbox.innerHTML = 'current page is '+tlt
})
window.addEventListener('popstate',function(e){
var state = e.state
mainbox.innerHTML = 'current page is ' + state.title
})
})()
</script>

两种实现方式的对比:基于Hash的路由实现,兼容性更好;而基于History API的路由,则更正式,更美观,可以设置与当前URL同源的任意URL,路径更直观。此外,基于Hash的路由不需要对服务器做改动,基于History API的路由需要对服务器做一些改造,需要对不同的路由进行相应的设置才行。

三、React-Router

由于我现在也就对React-Router比较熟悉,Vue学了太久了,忘记了。Angular刚学,还没到路由这块这来,因此就以React-Router为例,来聊聊现代前端框架的路由实现思路。先来看看一个简单的React中的Router代码:

1
2
3
4
5
6
<Router>
<Switch>
<Route path="/about" component={About}/>
<Route path="/:user" component={User}/>
</Switch>
</Router>

其实现思路也很简单如下图所示:
image

其实现思路很简单:About和User这两个component一直都存在。当路由发生改变时,与URL相匹配的component机会被成功渲染。而不匹配的component就设置为null。

具体实现,大家可以看这篇文章,这位大佬做了很详细的介绍:

https://github.com/youngwind/blog/issues/109

需要注意的是,react-router的每一个路由都是一个react组件,因此在进行路由跳转时,它也会比对前后页面的差异,然后再进行渲染,因此使用react-router较之传统的路由在页面渲染上的表现也会较好。

参考资料: