您的位置:首页 > Web前端

Web开发中 前端路由 实现的几种方式和适用场景

2016-10-01 19:47 609 查看
故事从名叫Oliver的绿箭虾`说起,这位大虾酷爱社交网站,一天他打开了Twitter,从发过的tweets的选项卡一路切到followers选项卡,Oliver发现页面的内容变化了,URL也变化了,但为什么页面没有闪烁刷新呢?于是Oliver打开的网络监控器(没错,Oliver是个程序员),他惊讶地发现在切换选项卡时,只有几个XHR请求发生,但页面的URL却在对应着变化,这让Oliver不得不去思考这一机制的原因…

叙事体故事讲完,进入正题。首先,我们知道传统而经典的Web开发中,服务器端承担了大部分业务逻辑,但随着2.0时代ajax的到来,前端开始担负起更多的数据通信和与之对应的逻辑。

在过去,Server端处理来自浏览器的请求时,要根据不同的Url路由,拼接出对应的视图页面,通过Http返回给浏览器进行解析渲染。Server不得不承担这份艰巨的责任,谁叫他是Server,而不是Owner -_-“。为了让Server端更好地把重心放到实现核心逻辑和看守数据宝库,把部分数据交互的逻辑交给前端担负,让前端来分担Server端的压力显得尤为重要,前端也有这个责任和能力。

那么问题来了,前端的能力是什么呢,有哪些能力呢?

大部分的复杂的网站,都会把业务解耦为模块进行处理。这些网站中又有很多的网站会把适合的部分应用Ajax进行数据交互,展现给用户,很明显处理这样的数据通信交互,不可避免的会涉及到跟URL打交道,让数据交互的变化反映到URL的变化上,进而可以给用户机会去通过保存的URL链接,还原刚才的页面内容板块的布局,这其中包括Ajax局部刷新的变化。

通过记录URL来记录web页面板块上Ajax的变化,我们可以称之为
Ajax标签化
,比较好实现可以参考Pjax等。而对于较大的framework,我们称之为
路由系统
,比如AngularJs等。

我们先熟悉几个新的H5 history Api:

?
上边是Mozilla在HTML5中实现的几个History api的官方文档描述,我们先来关注下最后边的两个api,
history.pushState
history.replaceState
,这两个history新增的api,为前端操控浏览器历史栈提供了可能性:

?
这两个Api都会操作浏览器的历史栈,而不会引起页面的刷新。不同的是,pushState会增加一条新的历史记录,而replaceState则会替换当前的历史记录。所需的参数相同,在将新的历史记录存入栈后,会把传入的data(即state对象)同时存入,以便以后调用。同时,这俩api都会更新或者覆盖当前浏览器的title和url为对应传入的参数。

url参数可以为绝对路径,如:
http://tonylee.pw?name=tonylee
https://www.tonylee.pw/name/tonylee
;也可以为相对路径:
?name=tonylee
/name/tonylee
;等等的形式,让我们来在console中做个测试:

?
可以看到,url作为一个改变当前浏览器地址的参数,用法是很灵活的,replaceState和pushState具有和上边测试相同的特性,传入的url如果可能,总会被做适当的处理,这种处理默以”/”相隔,也可以自己指定为”?”等。要注意,这两个api都是不能跨域的!比如在
http://tonylee.pw
下,只能在同域下进行调用,如二级域名
http://www.tonylee.pw
就会产生错误。没错,我想你已经猜到了前边讲到的Oliver看到URL变化,页面板块变化,页面发出XHR请求,页面没有reload等等特性,都是因此而生!

?
至于api中的data参数,实际上是一个state对象,也即是javascript对象。Firefox的实现中,它们是存在用户的本地硬盘上的,最大支持到640k,如果不够用,按照FF的说法你可以用
sessionStorage
or
localStorage
-_-“。如:

?
如果当前页面经过这样的过程,历史栈对应的条目,被存入了stateObj,那么我们可以随时主动地取出它,如果页面只是一个普通的历史记录,那么这个state就是null。如:

?
mozilla有一个应用pushState和replaceState小demo大家可以看一下:

?
You are at coordinate 5 on the line.

Advance to 6 or retreat
to 4?
<script> var currentPage = 5; // prefilled by server!!!! function go(d) { setupPage(currentPage + d); history.pushState(currentPage, document.title, '?x=' +
currentPage); } onpopstate = function(event) { setupPage(event.state); } function setupPage(page) { currentPage = page; document.title = "Line Game - " + currentPage; document.getElementById('coord').textContent = currentPage; document.links[0].href = '?x='
+ (currentPage+1); document.links[0].textContent = 'Advance to ' + (currentPage+1); document.links[1].href = '?x=' + (currentPage-1); document.links[1].textContent = 'retreat to ' + (currentPage-1); } </script>

仔细阅读就会看到,这个demo已经快成为一个Ajax标签化或者前端路由系统的雏形了!

了解这俩api还不够,再来看下上边的demo中涉及到的
popstate
事件,我担心解释的不到位,所以看看mozilla官方文档的解释:

?
简而言之,就是说当同一个页面在历史记录间切换时,就会产生popstate事件。正常情况下,如果用户点击后退按钮或者开发者调用:history.back() or history.go(),页面根本就没有处理事件的机会,因为这些操作会使得页面reload。所以popstate只在不会让浏览器页面刷新的历史记录之间切换才能触发,这些历史记录一般由pushState/replaceState或者是由hash锚点等操作产生。并且在事件的句柄中可以访问state对象的引用副本!而且单纯的调用pushState/replaceState并不会触发popstate事件。页面初次加载时,知否会主动触发popstate事件,不同的浏览器实现也不一样。下边是官方的一个demo:

?
这里便是通过event.state拿到的state的引用副本!

H5还新增了一个
hashchange
事件,也是很有用途的一个新事件:

?
当页面hash(#)变化时,即会触发hashchange。锚点Hash起到引导浏览器将这次记录推入历史记录栈顶的作用,
window.location
对象处理“#”的改变并不会重新加载页面,而是将之当成新页面,放入历史栈里。并且,当前进或者后退或者触发hashchange事件时,我们可以在对应的事件处理函数中注册ajax等操作!

但是hashchange这个事件不是每个浏览器都有,低级浏览器需要用轮询检测URL是否在变化,来检测锚点的变化。当锚点内容(location.hash)被操作时,如果锚点内容发生改变浏览器才会将其放入历史栈中,如果锚点内容没发生变化,历史栈并不会增加,并且也不会触发hashchange事件。

想必你猜到了,这里说的低级浏览器,指的就是可爱的IE了。比如我有一个url从
http://tonylee.pw#hash_start=1
变化到
http://tonylee.pw#hash_start=2
,实现良好的浏览器是会触发一个名为
hashchange
的事件,但是对于低版本的IE(稍后我会对具体的兼容性做个总结),我们只能通过设置一个Inerval来不断的轮询url是否发生变化,来判断是否发生了类似hashchange的事件,同时可以声明对应的事件处理函数,从而模拟事件的处理。如下是当浏览器不支持hashchange事件时的模拟方法:

?
熟悉了这些新的H5 api,大概对前端路由的实现方式,有了一个小小的模型了。我们来看下兼容性:

?
由上边的测试我得出了一些兼容性概览:

?
只有webkit内核浏览器才会默认触发
popstate
(chrome>34的可能实现的有问题,safari就很正常)。

到这里,说了这么多api, 其实我们对标签化/路由系统应该有了一个大概的了解。如果考虑H5的api,过去facebook和twitter实现路由系统时,约定用”#!”实现,这估计也是一个为了照顾搜索引擎的约定。毕竟前端路由系统涉及到大量的ajx,而这些ajax对应url路径对于搜索引擎来说,是很难匹配起来的。

路由大概的实现过程可以这么理解, 对于高级浏览器,利用H5的新Api做好页面上不同板块ajax等操作与url的映射关系,甚至可以自己用javascript书写一套历史栈管理模块,从而绕过浏览器自己的历史栈。而当用户的操作触发popstate时,可以判断此时的url与板块的映射关系,从而加载对应的ajax板块。这样你就可以把一个具有很复杂ajax版面结构页面的url发送给你的朋友了,而你的朋友在浏览器中打开这个链接时,前端路由系统url和板块映射关系会解析并还原出整个页面的原貌!一般SPA(单页面应用)和一些复杂的社交站应用,会普遍拥有自己的前端路由系统。

看到这里,想必你也想到一个问题,浏览器第一次打开某个链接时,肯定会首先被定向到server端进行路由解析,上边所说的前端路由系统,都是建立在页面已经打开,并且前端可以利用H5等的api拦截下这些URL变化,确保这些URL变化不会发送的server端返回新的页面。但是考虑这种情况,链接是在一个新的浏览器tab中打开的,那么这时候前端就无法拦截下这个url,所以,这就要求serer和前端制定好一个规则,那些url是需要前端解析的,那些url是属于后端的,而server判断出这个url的某部分结构不是自己应该解决的部分时,它就应该意识到,这是前端路由系统的URL部分,需要定向到拥有前端路由系统javascript代码的页面,交给前端处理,比如,nodejs中:

?
通过这样的方式,属于前端的路由系统始终可以被正确的交给前端路由系统去handle。对于php,.net也都是类似的配置server路由,给前端路由留下出口即可。

AngularJS框架中路由一般都这样配置:

?
可以看到,angular正是将URL、模块模板、模块控制器,进行一个系统的映射,从而实现出一套前端路由系统。这套路由系统默认是以#号开始的,url中锚点#号后边的url即标志着前端路由系统URL部分的开始。这么做是为了照顾到更多浏览器,因为利用hash方案,IE对这套路由系统也会有很好的支持性(前边已经说到,低版本IE对H5的新Api支持不好)。而如果项目压根就不想考虑IE,在Ng中,就可以直接调用
$locationProvider.html5Mode(true)
来利用H5的api实现路由系统,从而去掉#号,不用hash方案,这样做URL可能会更美观一些-_-“。

正常情况下,URL中的”/”一般是server端路由采用的标记,而”?”或者”#”再或者”#!”,则一般为前端路由采用的开始标记,我们可以在这些符号后边,通过键值对的形式,描述一个页面具有哪些板块配置信息。也不乏有的网站为了美观,前后端共用”/”进行路由索引(比如前边说的twitter)。

我们来看两个比较经典的网站:

?
?
最后总结下:

?
不论哪种方案,最终的目的都是希望能解决ajax标签化的问题。以上说了这么多,仅仅是分析了这些路由系统大概的实现方式和兼容性解决方案,如果有机会,我会再写一篇文章介绍下主流框架中或者类库中,具体是如何实现这套路由系统的,javascript版本的历史栈管理模块又是怎么样的,实现思路如何。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  前端路由