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

浅析Web开发中前端路由实现的几种方式

主题 Web开发

故事从名叫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:

/*Returns the number of entries in the joint session history.*/
window . history . length

/*Returns the current state object.*/
window . history . state

/*Goes back or forward the specified number of steps in the joint session history.A zero delta will reload the current page.If the delta is out of range, does nothing.*/
window . history . go( [ delta ] )

/*Goes back one step in the joint session history.If there is no previous page, does nothing.*/
window . history . back()

/*Goes forward one step in the joint session history.If there is no next page, does nothing.*/
window . history . forward()

/*Pushes the given data onto the session history, with the given title, and, if provided and not null, the given URL.*/
window . history . pushState(data, title [url] )

/*Updates the current entry in the session history to have the given data, title, and,if provided and not null, URL.*/
window . history . replaceState(data, title [url] )

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

/**
*parameters
*@data {object} state对象,这是一个javascript对象,一般是JSON格式的对象
*字面量。
*@title {string} 可以理解为document.title,在这里是作为新页面传入参数的。
*@url {string} 增加或改变的记录,对应的url,可以是相对路径或者绝对路径,
*url的具体格式可以自定。
*/
history.pushState(data, title, url) //向浏览器历史栈中增加一条记录。
history.replaceState(data, title, url) //替换历史栈中的当前记录。

这两个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为:http://tonylee.pw
window.history.pushState(null, null, "http://tonylee.pw?name=tonylee");
//url变化:http://tonylee.pw -> http://tonylee.pw?name=tonylee

window.history.pushState(null, null, "http://tonylee.pw/name/tonylee");
//url变化:http://tonylee.pw -> http://tonylee.pw/name/tonylee

window.history.pushState(null, null, "?name=tonylee");
//url变化:http://tonylee.pw -> http://tonylee.pw?name=tonylee

window.history.pushState(null, null, "name=tonylee");
//url变化:http://tonylee.pw -> http://tonylee.pw/name=tonylee

window.history.pushState(null, null, "/name/tonylee");
//url变化:http://tonylee.pw -> http://tonylee.pw/name/tonylee

window.history.pushState(null, null, "name/tonylee");
//url变化:http://tonylee.pw -> http://tonylee.pw/name/tonylee

//错误的用法:
window.history.pushState(null, null, "http://www.tonylee.pw?name=tonylee");
//error: 由于跨域将产生错误

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

如果有兴趣,你也可以去twitter亲自体验twitter的这一特性,看看他的前端路由系统是如何工作的。
https://twitter.com/following -> https://twitter.com/followers

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

var stateObj = { foo: "bar" };
history.pushState(stateObj, "the blog of Tony Lee", "name = Later");

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

var currentState = history.state;  //如果没有则为null。

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

<!DOCTYPE HTML>
<!-- this starts off as http://example.com/line?x=5 -->
<title>Line Game - 5</title>
<p>You are at coordinate <span id="coord">5</span> on the line.</p>
<p>
 <a href="?x=6" onclick="go(1); return false;">Advance to 6</a> or
 <a href="?x=4" onclick="go(-1); return false;">retreat to 4</a>?
</p>
<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官方文档的解释:

An event handler for the popstate event on the window.

A popstate event is dispatched to the window every time the active history entry changes between two history entries for the same document. If the history entry being activated was created by a call to history.pushState() or was affected by a call to history.replaceState(), the popstateevent‘s state property contains a copy of the history entry‘s state object.

Note that just calling history.pushState() or history.replaceState() won‘t trigger apopstate event. The popstate event is only triggered by doing a browser action such as clicking on the back button (or calling history.back() in JavaScript). And the event is only triggered when the user navigates between two history entries for the same document.

Browsers tend to handle the popstate event differently on page load. Chrome (prior to v34) and Safari always emit a popstate event on page load, but Firefox doesn‘t.

Syntax
    window.onpopstate = funcRef;
    //funcRef is a handler function.

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

window.onpopstate = function(event) {
 alert("location: " + document.location + ", state: " +   JSON.stringify(event.state));
};

history.pushState({page: 1}, "title 1", "?page=1");
history.pushState({page: 2}, "title 2", "?page=2");
history.replaceState({page: 3}, "title 3", "?page=3");
history.back(); // alerts "location: http://example.com/example.html?page=1, state: {"page":1}"
history.back(); // alerts "location: http://example.com/example.html, state: null
history.go(2); // alerts "location: http://example.com/example.html?page=3, state: {"page":3}

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

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

The ‘hashchange‘ event is fired when the fragment identifier of the URL has changed (the part of the URL that follows the # symbol, including the # symbol).

当页面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事件时的模拟方法:

(function(window) {

 // 如果浏览器不支持原生实现的事件,则开始模拟,否则退出。
 if ( "onhashchange" in window.document.body ) { return; }

 var location = window.location,
 oldURL = location.href,
 oldHash = location.hash;

 // 每隔100ms检查hash是否发生变化
 setInterval(function() {
     var newURL = location.href,
     newHash = location.hash;

     // hash发生变化且全局注册有onhashchange方法(这个名字是为了和模拟的事件名保持统一);
     if ( newHash != oldHash && typeof window.onhashchange === "function"  ) {
         // 执行方法
         window.onhashchange({
             type: "hashchange",
             oldURL: oldURL,
             newURL: newURL
         });

         oldURL = newURL;
         oldHash = newHash;
     }
 }, 100);
})(window);

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

<script type="text/javascript" src="./jquery-1.9.1.js"></script>
 <script>
 $(function (){
   if(history&&history.pushState){
     alert("true");
   }else{
     alert("false");
   }
   $(window).on("hashchange",function (){
     alert("hashchange");
   });
 });
 </script>

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

history&&history.pushState兼容如下:
 chrome true;
 Firefox true;
 IE10 true;
 IE<=9 false;
 PS:ie<=9既然不支持这些api那就只能采用hash方案,来实现路由系统的兼容了。

hashchange兼容如下:
 IE9 true;
 IE8 true;
 IE7 false;
 ...

页面load时,onhashchange默认触发情况:
 chrome 需主动trigger才能触发
 FF 需主动trigger才能触发
 IE 需主动trigger才能触发

页面load时,onpopstate默认触发情况:
 chrome <34版本之前的默认触发
 FF 默认不触发
 IE 默认不触发
PS:以上是我手动测试的一个大概情况,具体的兼容情况可以去这里测试(http://caniuse.com/)。

只有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中:

//Express框架的路由访问控制文件server.js,增加路由配置。
app.use(function (req, res) {
  if(req.path.indexOf(‘/routeForServerSide‘)>=0){
     res.send("这里返回的都是server端处理的路由");
  }
  //比如AngularJS页面
  else{
     res.sendfile(‘这里可以将已经配置好angularJS路由的页面返回‘);
  }
});

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

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

app.config([‘$routeProvider‘, ‘$locationProvider‘, function ($routeProvider, $locationProvider) {
 $routeProvider
 .when(‘/login‘, {
   templateUrl: ‘/login.html‘,
   controller: ‘LoginController‘
 }).otherwise({
   redirectTo: ‘/homepage‘
 });
 $locationProvider.html5Mode(true);
}])

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

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

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

1.Sina(新浪)
作为国内SNS的翘楚,新浪的路由形式也很高大上,比如:
在FF,Chrome,IE>=10时新浪的URL是这样的:

http://weibo.com/mygroups?gid=221102230086340215&wvr=5&leftnav=1

PS:可以看到从?号开始就是前端路由了,一大堆的键值对。

在IE<=9时:

http://weibo.com/mygroups?gid=221102230086340215&wvr=5&leftnav=1#!/mygroups?gid=221102230086340215&wvr=5&leftnav=1

PS:仔细观察你会发现,新浪在#!后边把路由段,复制了一遍,这是因为IE低版本不支持H5的新api,因此采用#号的hash方案(比如前边讲到的hashchange或轮询等技术),这样就照顾到所有的浏览器啦~
2.Gmail
作为一款超好用的SPA应用典范中的典范,无论从界面风格还是易用性...好吧不扯了直接说路由:
收件箱:https://mail.google.com/mail/u/1/#inbox
星标箱:https://mail.google.com/mail/u/1/#starred
发件箱:https://mail.google.com/mail/u/1/#sent
草稿箱:https://mail.google.com/mail/u/1/#drafts
PS:看到了么,Gmail表示url不是给正常人看的,一律用#来实现前端路由部分,甚是简洁明了(其实挺赞的!)。最重要的是,这种路由方案,兼容性没的说(可能是Gmail很看重IE用户群体)!

最后总结下:

H5+hash方案:兼容所以浏览器,又照顾到了高级浏览器应用新特性。

纯H5方案:表示IE是谁,我不认识-_-",这套方案应用纯H5的新特性,URL随心定制。

纯Hash方案:其实一开始我是拒绝的,可是...可是...duang...IE~~:)

不论哪种方案,最终的目的都是希望能解决ajax标签化的问题。以上说了这么多,仅仅是分析了这些路由系统大概的实现方式和兼容性解决方案,如果有机会,我会再写一篇文章介绍下主流框架中或者类库中,具体是如何实现这套路由系统的,javascript版本的历史栈管理模块又是怎么样的,实现思路如何。

加入: JS前端开发 QQ群 :147250970  欢迎加入~!气氛热情,欢乐多,妹子多!

web前端 聚集地,汇聚了全国顶尖的web前端热爱者,最新技术,最炫潮流,最靠谱的话题:
  做好现在!技术只是为了改变生活!JS前端实用开发QQ群 :147250970

时间: 2024-10-10 02:22:00

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

浅析Web开发中前端路由实现的几种方式

故事从名叫Oliver的绿箭虾`说起,这位大虾酷爱社交网站,一天他打开了 Twitter ,从发过的tweets的选项卡一路切到followers选项卡,Oliver发现页面的内容变化了,URL也变化了,但为什么页面没有闪烁刷新呢?于是Oliver打开的网络监控器(没错,Oliver是个程序员),他惊讶地发现在切换选项卡时,只有几个XHR请求发生,但页面的URL却在对应着变化,这让Oliver不得不去思考这一机制的原因… 叙事体故事讲完,进入正题.首先,我们知道传统而经典的Web开发中,服务器端

web开发中前端页面是如何跟后端服务器数据交互的

本文链接:https://blog.csdn.net/kangkanglhb88008/article/details/84446173后端服务器一般是指servlet容器,用于执行java源程序常见的网页有html,htm,shtml,asp,aspx,php,jsp等格式前两个常用于静态网页,后面几个常用于动态网页.这里前端网页以比较常见的 xx.html 和 xx.jsp 网页作为介绍,其它类似 一.静态页面xx.html如何跟后台交互:先来看一个最简单的登陆界面源代码  <body> 

Web开发中,用到的4种会话跟踪技术

会话跟踪:主要解决HTTP的无状态问题,即: 当用户发出请求时,服务器就会做出响应,客户端与服务器之间的联系是离散的.非连续的.当用户在同一网站的多个页面之间转换时,根本无法确定是否是同一个客户,会话跟踪技术就可以解决这个问题.当一个客户在多个页面间切换时,服务器会保存该用户的信息. 1.Cookie: 可以使用 cookie 存储购物会话的 ID:在后续连接中,取出当前的会话 ID,并使用这个 ID 从服务器上的查找表(lookup table)中提取出会话的相关信息. 以这种方式使用 coo

WEB开发中的页面跳转方法总结

PHP header()函数跳转 PHP的header()函数非常强大,其中在页面url跳转方面也调用简单,使用header()直接跳转到指定url页面,这时页面跳转是302重定向: $url = "http://www.helloweba.com/";   header( "Location: $url" );  我们有可能会遇到特殊的跳转时,比如网站改版有个页面地址要做301重定向,当然你可以通过web配置rewrite来实现,但现在我要告诉大家,可以使用php

移动Web 开发中的一些前端知识收集汇总

在开发DeveMobile 与EaseMobile 主题 的时候积累了一些移动Web 开发的前端知识,本着记录总结的目的,特写这篇文章备忘一下. 要说移动Web 开发与传统的PC 端开发,感觉也没什么不同,但得益于苹果对于智能机的推动,CSS3+HTML5几乎可以毫无顾忌的使用,然后浏览器端考虑webkit内核的就差不多了. webkit内核中一些私有的meta标签 1 2 3 4 <meta name="apple-mobile-web-app-capable" content

Web开发中的18个关键性错误

前几年,我有机会能参与一些有趣的项目,并且独立完成开发.升级.重构以及新功能的开发等工作. 本文总结了一些PHP程序员在Web开发中经常 忽略的关键错误,尤其是在处理中大型的项目上问题更为突出.典型的错误表现在不能很好区分各种开发环境和没有使用缓存和备份等. 下面以PHP为例,但是其核心思想对每一个Web程序员都是适用的. 应用程序级别的错误 1.在开发阶段关闭了错误报告 我唯一想问的是:为什么?为什么在开发的时候要关闭错误报告? PHP有很多级别的错误报告,在开发阶段我们必须将它们全部开启.

web开发中的长度单位(px,em,ex,rem),如何运用,看完这篇就够了!

原创 2017-03-08 web小二 web前端开发 作为一名前端开发人员,css中的长度单位,都是我们在工作中非常熟悉的名词,因为没有它们,我们就不能声明某个字符应该多大,或者某些图像周围应该留白多少,甚至有时候能导致css不能进行正常工作,所以在很多css属性中,它们都是依赖于长度单位来显示各种页面元素. 1.长度单位包括哪些? 长度单位,其实在我们的生活中,也非常常见,例如,厘米.毫米.英寸,还有经常接触到的像素(px),元素的字体高度(em).字母x的高度(ex).百分比(%)等等这些

详细阐述Web开发中的图片上传问题

Web开发中,图片上传是一种极其常见的功能.但是呢,每次做上传,都花费了不少时间. 一个"小功能"花费我这么多时间,真心不愉快. So,要得认真分析下原因. 1.在最初学习Java Web开发的时候,经验不足,属于能力问题,比如对技术认识不到位. 2.图片上传是一类问题,而不是一个问题.   比如,大家都会做饭,但每个人自己做饭是有不同的.做了一个人吃.一家人吃.喜事待客做好几桌,是不同的问题.   同样的,图片上传,是上传一张还是多张,前端的用户体验如何,后端逻辑处理是否正确,图片存

web开发中的安全问题

web开发中很多东西由前段来负责判断,比如常见的邮箱 电话号码,前端判断到不是一个正确的格式,在你点击提交时候提示你格式填错了,然后不请求后端php,直到你填写正确的格式为止.这种其实可以修改js或者干脆用python里面的requests之类的http请求库直接请求接口,那么可以成功把错误的东西提交给后端进而存储到数据库了,那么这种就不太好了. 如果系统不重要,用的人不多,可以勉强放过这一点. 但有的很重要的系统,尤其涉及到金钱的接口,后端必须也做好验证.否则有人利用漏洞来破坏规则,造成金钱的