什么是前端路由?
路由是根据不同的 url 地址展示不同的内容或页面,就是把不同路由对应不同的内容或页面的任务交给前端来做,之前是通过服务端根据 url 的不同返回不同的页面实现的。
什么时候使用前端路由?
在单页面应用中,大部分页面结构不变,只改变部分内容的使用。
前端路由有什么优点和缺点?
优点:用户体验好,不需要每次都从服务器全部获取,快速展现给用户
缺点:使用浏览器的前进,后退键的时候会重新发送请求,没有合理地利用缓存,单页面无法记住之前滚动的位置,无法在前进,后退的时候记住滚动的位置。
实现的几种方式和几种场景
故事从名叫 Oliver
的绿箭虾说起,这位大虾酷爱社交网站,一天他打开了 Twitter
,从发过的 tweets
的选项卡一路切到 followers
选项卡,Oliver
发现页面的内容变化了,URL也变化了,但为什么页面没有闪烁刷新呢?于是Oliver
打开的网络监控器,他惊讶地发现在切换选项卡时,只有几个XHR请求发生,但页面的URL却在对应着变化,这让 Oliver
不得不去思考这一机制的原因…
叙事体故事讲完,进入正题。首先,我们知道传统而经典的 Web 开发中,服务器端承担了大部分业务逻辑,但随着 2.0 时代 ajax 的到来,前端开始担负起更多的数据通信和与之对应的逻辑。
在过去,Server
端处理来自浏览器的请求时,要根据不同的 Url 路由,拼接出对应的视图页面,通过 Http 返回给浏览器进行解析渲染。Server
不得不承担这份艰巨的责任。为了让 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标签化的问题。以上说了这么多,仅仅是分析了这些路由系统大概的实现方式和兼容性解决方案。
参考文章: 什么是“前端路由”?什么时候适合使用“前端路由”?“前端路由”有哪些优点和缺点?& Web开发中 前端路由 实现的几种方式和适用场景