路由是根据不同的 url 地址展示不同的内容或页面,早期的路由都是后端直接根据 url 来 reload 页面实现的,即后端控制路由。
后来页面越来越复杂,服务器压力越来越大,随着AJAX(异步刷新技术) 的出现,页面实现非 reload 就能刷新数据,让前端也可以控制 url 自行管理,前端路由因此而生。
如果直接使用AJAX加载页面片段是可以实现单页效果的,但这样会破坏浏览器的前进与后退功能,使用Hjax或Pjax技术后即可以实现单页无刷新效果又不会影响前进与后退功能。
示例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>模拟单页</title> </head> <body> <script src="../../js/jquery-1.12.4/jquery-1.12.4.js"></script> <a href="" id="page1">页面一</a> | <a href="" id="page2">页面二</a> <div id="container"> </div> <script> $("#page1").click(function () { $("#container").load("page1.html"); return false; }); $("#page2").click(function () { $.ajax({ url:"page2.html", type:"get", dataType:"html", success:function (data) { var ctx=$("<html/>").html(data); //方法一 console.log(ctx.find("div")); //方法二 console.log($("div",ctx)); $("#container").html($("body",ctx)); } }); return false; }); </script> </body> </html>
page1.html:
<div style="background: antiquewhite"> <h2>这是页面一</h2> </div>
page2.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>这是页面二</title> </head> <body> <div style="background: dodgerblue"> <h2>这是页面二</h2> </div> </body> </html>
运行结果:
单页面应用的实现,就是因为前端路由,前端路由实现主要有下面几种方法:
一、Hjax(Hash + Ajax)
1.1、原理
原理:url 中常会出现 #,一可以表示锚点(如回到顶部按钮的原理),二是路由里的锚点(hash)。Web 服务并不会解析 hash,也就是说 # 后的内容 Web 服务都会自动忽略,但是 JavaScript 是可以通过 window.location.hash 读取到的,读取到路径加以解析之后就可以响应不同路径的逻辑处理。
hashchange 事件(监听 hash 变化触发的事件),当用 window.location 处理哈希的改变时不会重新渲染页面,而是当作新页面加到历史记录中,这样我们跳转页面就可以在 hashchange 事件中注册 ajax 从而改变页面内容。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>模拟单页 - hash</title> </head> <body> <script src="../../js/jquery-1.12.4/jquery-1.12.4.js"></script> <a href="#/page1.html" id="page1">页面一</a> | <a href="#/page2.html" id="page2">页面二</a> <button onclick="getHash()">获得hash</button> <button onclick="setHash()">修改hash</button> <div id="container"> </div> <script> $("a[href]").click(function () { var url = $(this).prop("href").split("#")[1].substring(1); $.ajax({ url: url, type: "get", dataType: "html", success: function (data) { var ctx = $("<html/>").html(data); var root = $("body", ctx); if (root.size() > 0) { $("#container").html(root); } else { $("#container").html(data); } } }); }); function getHash() { console(location.hash); } function setHash() { location.hash = "123456"; } </script> </body> </html>
运行结果:
这里并没有解决前进与后退失效的问题。
1.2、URL详解
URL是统一资源定位符,对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。互联网上的每个文件都有一个唯一的URL,它包含的信息指出文件的位置以及浏览器应该怎么处理它。URI是统一资源标识符,而URL是统一资源定位符,我们把URL理解为是URI的一个子类,而另一种子类是URN。url是统一资源定位符,对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。互联网上的每个文件都有一个唯一的URL,它包含的信息指出文件的位置以及浏览器应该怎么处理它。URL中所有的字符都是ASCII字符集,如果出现非ASCII字符集,比如中文,浏览器会先进行编码再进行传输。
1.2.1、URL的构成
URL的构成基本如下
scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment]
举例如下:
http://zhangguo:123456@www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#SomewhereInTheDocument
拆解如下:
1.2.2、用户名与密码
http://zhangguo:123456@www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#SomewhereInTheDocument
这里用户名是zhangguo,密码是123456,如果带@符用户必须填写,密码选填,带用户名与密码的情况很少见,很多时候都放到了参数中了。
1.2.3、协议(Protocol)
http://zhangguo:123456@www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#SomewhereInTheDocument
http://为协议名,标明了请求需要使用的协议,通常使用的是HTTP协议或者安全协议 HTTPS.其他协议还有mailto:用户打开邮箱的客户端,和ftp:用来做文件的转换, file用来获取文件,data获取外部资源等
1.2.4、域名(Domain)
http://zhangguo:123456@www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#SomewhereInTheDocument
www.example.com为域名,标明了需要请求的服务器的地址,www是主机名,example是单位名称,.com是机构类型
1.2.5、端口(Port)
http://zhangguo:123456@www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#SomewhereInTheDocument
:80是端口号,标明了获取服务器资源的入口,端口号只有整数,范围是从0 到65535(2^16-1),周知端口是众所周知的端口号,范围从0到1023,其中80端口分配给WWW服务,21端口分配给FTP服务等。我们在IE的地址栏里输入一个网址的时候是不必指定端口号的,因为在默认情况下WWW服务的端口是“80”。动态端口的范围是从49152到65535。之所以称为动态端口,是因为它 一般不固定分配某种服务,而是动态分配。端口1024到49151,分配给用户进程或应用程序。这些进程主要是用户选择安装的一些应用程序,而不是已经分配好了公认端口的常用程序。这些端口在没有被服务器资源占用的时候,可以用用户端动态选用为源端口。
端口号用于区分服务的端口,一台拥有IP地址的服务器可以提供许多服务,比如Web服务、FTP服务、SMTP服务等.那么,服务器的资源通过“IP地址+端口号”来区分不同的服务.
如果把服务器比作房子,端口号可以看做是通向不同服务的门,
1.2.6、文件路径
http://zhangguo:123456@www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#SomewhereInTheDocument
/path/to/myfile.html表示服务器上资源的路径,过去这样的路径标记的是服务器上文件的物理路径,但是现在,路径表示的只是一个抽象地址,并不指代任何物理地址.
1.2.7、参数(query、Parameters)
http://zhangguo:123456@www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#SomewhereInTheDocument
?key1=value1&key2=value2是请求里提供的额外参数.这些参数是以键值对的形式,通过&符号分隔开来,服务器可以通过这些参数进行相应的个性化处理
1.2.8、片段(ref、fragment、hash、Anchor)
http://zhangguo:123456@www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#SomewhereInTheDocument
#SomewhereInTheDocument是对资源的部分补充.fragment可以理解为资源内部的书签.用来想服务器指明展示的内容所在的书签的点.例如对于HTML文件来说,浏览器会滚动到特定的或者上次浏览过的位置.对于音频或者视频资源来说,浏览器又会跳转到对应的时间节点。
锚记连接又叫命名锚记,命名锚记像一个迅速定位器一样是一种页面内的超级链接。
1.2.9、相对路径和绝对路径
我们上面所说的都是绝对路径,但是URL也有相对路径的表现形式.
URL所请求的资源依赖于请求所在的上下文,也就是当前环境,在浏览器的输入框内URL没有上下文,所以必须提供绝对路径.
但是当URL用于文件中时,例如HTML的页面,情况就大有不同了,因为浏览器已经拥有了文件的URL,所以可以自动填补文件内使用的URL丢失的部分,例如协议,域名,端口等,所以我们可以较为直观的区分相对路径和绝对路径.
如果URL以/开头,浏览器会从根服务器去获取资源,而不是从给定的文件夹中获取.
我们用一些例子来直观的理解下
完整的URL:
https://developer.mozilla.org/en-US/docs/Learn
隐藏协议
//developer.mozilla.org/en-US/docs/Learn
浏览器会使用文件主机的相同协议
隐藏域名
/en-US/docs/Learn
浏览器会使用文件主机的相同协议和同样的域名,注意,不能在未隐藏协议的前提下只隐藏域名
<a href="page1.html" onclick="alert(this.href)">相对路径</a> <a href="/page1.html" onclick="alert(this.href)">绝对路径,从主机名开始</a>
结果:
1.3、hash
hash即URL中"#"字符后面的部分,有很多种别名如ref、fragment、hash、Anchor,中文一般称为锚链接。
①使用浏览器访问网页时,如果网页URL中带有hash,页面就会定位到id(或name)与hash值一样的元素的位置;
②hash还有另一个特点,它的改变不会导致页面重新加载;
③hash值浏览器是不会随请求发送到服务器端的;
④通过window.location.hash属性获取和设置hash值。
window.location.hash值的变化会直接反应到浏览器地址栏(#后面的部分会发生变化),同时,浏览器地址栏hash值的变化也会触发window.location.hash值的变化,从而触发onhashchange事件。
hash 属性是一个可读可写的字符串,该字符串是 URL 的锚部分(从 # 号开始的部分)
1.3.1、#的涵义
#代表网页中的一个位置。其右面的字符,就是该位置的标识符。比如,
http://www.example.com/index.html#print
就代表网页index.html的print位置。浏览器读取这个URL后,会自动将print位置滚动至可视区域。(单页应用)
为网页位置指定标识符,有两个方法。一是使用锚点,比如<a name="print"></a>,二是使用id属性,比如<div id="print" >。
示例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>hash</title> </head> <body> <p style="height: 500px; background: blue" id="p1">第一段</p> <p style="height: 300px; background: red" id="p2">第二段</p> <p style="height: 500px; background: palegreen" id="p3">第三段</p> <div style="height: 3000px; background: dodgerblue"> </div> <a href="#p1" onclick="alert(this.href)">到第一段</a> | <a href="#p2">到第二段</a> | <a href="#p3">到第三段</a> <a href="page1.html" onclick="alert(this.href)">相对路径</a> <a href="/page1.html" onclick="alert(this.href)">绝对路径,从主机名开始</a> <form> <button>提交,是否带了hash</button> </form> </body> </html>
结果:
1.3.2、HTTP请求不包括#
#是用来指导浏览器动作的,对服务器端完全无用。所以,HTTP请求中不包括#。
比如,访问下面的网址,
http://www.example.com/index.html#print
浏览器实际发出的请求是这样的:
GET /index.html HTTP/1.1
Host: www.example.com
可以看到,只是请求index.html,根本没有"#print"的部分。
1.3.3、#后的字符
在第一个#后面出现的任何字符,都会被浏览器解读为位置标识符。这意味着,这些字符都不会被发送到服务器端。
比如,下面URL的原意是指定一个颜色值:
http://www.example.com/?color=#fffccc
但是,浏览器实际发出的请求是:
GET /?color= HTTP/1.1
Host: www.example.com
可以看到,"#fffccc"被省略了。只有将#转码为%23,浏览器才会将其作为实义字符处理。也就是说,上面的网址应该被写成:
http://example.com/?color=%23fffccc
1.3.4、改变#不触发网页重载
单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载网页。
比如,从
http://www.example.com/index.html#location1
改成
http://www.example.com/index.html#location2
浏览器不会重新向服务器请求index.html。
1.3.5、改变#会改变浏览器的访问历史
每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用"后退"按钮,就可以回到上一个位置。
这对于ajax应用程序特别有用,可以用不同的#值,表示不同的访问状态,然后向用户给出可以访问某个状态的链接。
值得注意的是,上述规则对IE 6和IE 7不成立,它们不会因为#的改变而增加历史记录。
1.3.6、window.location.hash读取#值
window.location.hash这个属性可读可写。读取时,可以用来判断网页状态是否改变;写入时,则会在不重载网页的前提下,创造一条访问历史记录。
1.3.7、SEO抓取#的机制
默认情况下,Google的网络蜘蛛忽视URL的#部分。
但是,Google还规定,如果你希望Ajax生成的内容被浏览引擎读取,那么URL中可以使用"#!",Google会自动将其后面的内容转成查询字符串_escaped_fragment_的值。
比如,Google发现新版twitter的URL如下:
http://twitter.com/#!/username
就会自动抓取另一个URL:
http://twitter.com/?_escaped_fragment_=/username
通过这种机制,Google就可以索引动态的Ajax内容。
1.4、hashchange事件
这是一个HTML 5新增的事件,当#值发生变化时,就会触发这个事件。IE8+、Firefox 3.6+、Chrome 5+、Safari 4.0+支持该事件。
它的使用方法有三种:
window.onhashchange = func; <body onhashchange="func();"> window.addEventListener("hashchange", func, false);
对于不支持onhashchange的浏览器,可以用setInterval监控location.hash的变化。
示例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>hashchange</title> </head> <body> <a href="#p1">到第一段</a> | <a href="#p2">到第二段</a> | <a href="#p3">到第三段</a> <script> addEventListener("hashchange", function (e) { console.log(e); }, false); </script> </body> </html>
结果:
newURL是当前URL,oldURL是原来的URL。
示例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>history 测试</title> </head> <body> <p><input type="text" value="0" id="oTxt" /></p> <p><input type="button" value="+" id="oBtn" /></p> <script> var otxt = document.getElementById("oTxt"); var oBtn = document.getElementById("oBtn"); var n = 0; oBtn.addEventListener("click",function(){ n++; add(); },false); get(); function add(){ if("onhashchange" in window){ //如果浏览器的原生支持该事件 window.location.hash = "#"+n; } } function get(){ if("onhashchange" in window){ //如果浏览器的原生支持该事件 window.addEventListener("hashchange",function(e){ var hashVal = window.location.hash.substring(1); if(hashVal){ n = hashVal; otxt.value = n; } },false); } } </script> </body> </html>
1.5、Hjax实现与缓存
结合前面的内容,我们可以模拟一个简单的单页示例,为了提高性能这里做了缓存,示例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>模拟单页 - hash</title> </head> <body> <script src="../../js/jquery-1.12.4/jquery-1.12.4.js"></script> <a href="#page1.html" id="page1">页面一</a> | <a href="#page2.html" id="page2">页面二</a> <div id="container"> </div> <script> //缓存 var pages={}; //显示内容 function showContent(data) { var ctx = $("<html/>").html(data); var root = $("body", ctx); if (root.size() > 0) { $("#container").html(root); } else { $("#container").html(data); } } //监听hash的变化 window.addEventListener("hashchange",function (e) { console.log(location.href); var url = location.hash.substring(1); console.log(url); if(pages[url]){ showContent(pages[url]); return false; } if(url) { $.ajax({ url: url, type: "get", dataType: "html", success: function (data) { showContent(data); //缓存到对象中 if(!pages.url){ pages[url]=data; } } }); } },false); </script> </body> </html>
结果:
二、Pjax(PushState + Ajax)
虽然传统的ajax方式可以异步无刷新改变页面内容,但无法改变页面URL,因此有种方案是在内容发生改变后通过改变URL的hash的方式获得更好的可访问性(如 https://liyu365.github.io/BG-UI/tpl/#page/desktop.html),但是 hash 的方式有时候不能很好的处理浏览器的前进、后退,而且常规代码要切换到这种方式还要做不少额外的处理。而 pjax 的出现就是为了解决这些问题,简单的说就是对 ajax 的加强。
pjax结合pushState和ajax技术, 不需要重新加载整个页面就能从服务器加载Html到你当前页面,这个ajax请求会有永久链接、title并支持浏览器的回退/前进按钮。
pjax项目地址在 https://github.com/defunkt/jquery-pjax 。 实际的效果见: http://pjax.herokuapp.com 没有勾选 pjax 的时候点击链接是跳转的, 勾选了之后链接都是变成了 ajax 刷新(实际效果如下图的请求内容对比)。
2.0、History对象详解(pushState、replaceState、popstate)
在没有history ap之前,我们经常使用散列值来改变页面内容,特别是那些对页面特别重要的内容。因为没有刷新,所以对于单页面应用,改变其URL是不可能的。此外,当你改变URL的散列值,它对浏览器的历史记录没有任何影响。通过增加location.hash,并用onhashchange来达到目的。
现在对于HTML 5的History API来说,这些都是可以轻易实现的,但是由于单页面应用没必要使用散列值,它可能需要额外的开发脚本。它也允许我们用一种对SEO友好的方式建立新应用。
2.0.1、length属性
history.length属性保存着历史记录的URL数量。初始时,该值为1。由于IE10+浏览器在初始时返回2,存在兼容性问题,所以该值并不常用。
2.0.2、跳转方法go()、back()和forward()
history.go(n),进前或后退n步
history.back(),后退
history.forward(),前进
如果移动的位置超出了访问历史的边界,以上三个方法并不报错,而是静默失败
使用历史记录时,页面通常从浏览器缓存之中加载,而不是重新要求服务器发送新的网页,不触发onload事件。
2.0.3、pushState()
HTML5为history对象添加了两个新方法,history.pushState()和history.replaceState(),用来在浏览历史中添加和修改记录。state属性用来保存记录对象,而popstate事件用来监听history对象的变化(ie9不支持)。
history.pushState()方法向浏览器历史添加了一个状态。pushState()方法带有三个参数:一个状态对象、一个标题(现在被忽略了)以及一个可选的URL地址
history.pushState(state, title, url);
state object —— 状态对象是一个由pushState()方法创建的、与历史纪录相关的javascript对象。当用户定向到一个新的状态时,会触发popstate事件。事件的state属性包含了历史纪录的state对象。如果不需要这个对象,此处可以填null
title —— 新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填null
URL —— 这个参数提供了新历史纪录的地址。新URL必须和当前URL在同一个域,否则,pushState()将丢出异常。这个参数可选,如果它没有被特别标注,会被设置为文档的当前URL
假定当前网址是example.com/1.html,使用pushState方法在浏览记录(history对象)中添加一个新记录
var stateObj = { foo: 'bar' }; history.pushState(stateObj, 'page 2', '2.html');
添加上面这个新记录后,浏览器地址栏立刻显示example.com/2.html,但并不会跳转到2.html,甚至也不会检查2.html是否存在,它只是成为浏览历史中的最新记录。假如这时访问了google.com,然后点击了倒退按钮,页面的url将显示2.html,但是内容还是原来的1.html。再点击一次倒退按钮,url将显示1.html,内容不变
总之,pushState方法不会触发页面刷新,只是导致history对象发生变化,地址栏的显示地址发生变化
如果pushState的url参数,设置了一个新的锚点值(即hash),并不会触发hashchange事件,,即使新的URL和旧的只在hash上有区别
如果设置了一个跨域网址,则会报错。这样设计的目的是,防止恶意代码让用户以为他们是在另一个网站上
2.0.4、replaceState()
history.replaceState方法的参数与pushState方法一模一样,不同之处在于replaceState()方法会修改当前历史记录条目而并非创建新的条目
假定当前网页是example.com/example.html
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() // url显示为http://example.com/example.html?page=1 history.back() // url显示为http://example.com/example.html history.go(2) // url显示为http://example.com/example.html?page=3
示例:
2.0.5、state
history.state属性返回当前页面的state对象
history.pushState({page: 1}, 'title 1', '?page=1'); history.state// { page: 1 }
2.0.6、popstate事件
每当同一个文档的浏览历史(即history对象)出现变化时,就会触发popstate事件
[注意]需要注意的是,仅仅调用pushState方法或replaceState方法,并不会触发该事件,只有用户点击浏览器倒退按钮和前进按钮,或者使用javascript调用back()、forward()、go()方法时才会触发。另外,该事件只针对同一个文档,如果浏览历史的切换,导致加载不同的文档,该事件也不会触发
使用的时候,可以为popstate事件指定回调函数。这个回调函数的参数是一个event事件对象,它的state属性指向pushState和replaceState方法为当前URL所提供的状态对象(即这两个方法的第一个参数)
上面代码中的event.state,就是通过pushState和replaceState方法,为当前URL绑定的state对象
这个state对象也可以直接通过history对象读取
var currentState = history.state;
示例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>模拟单页 - hash</title> </head> <body> <script src="../../js/jquery-1.12.4/jquery-1.12.4.js"></script> <a href="#page1.html" id="page1">页面一</a> | <a href="#page2.html" id="page2">页面二</a> <div id="container"> </div> <script> //添加浏览历史变化事件 addEventListener("popstate",function (e) { console.log("popstate事件被引发,%o",e); },false); </script> </body> </html>
结果:
2.0.7、往返缓存
默认情况下,浏览器会在当前会话(session)缓存页面,当用户点击“前进”或“后退”按钮时,浏览器就会从缓存中加载页面
浏览器有一个特性叫“往返缓存”(back-forward cache或bfcache),可以在用户使用浏览器的“后退”和“前进”按钮时加快页面的转换速度。这个缓存中不仅保存着页面数据,还保存了DOM和javascript的状态;实际上是将整个页面都保存在了内存里。如果页面位于bfcache中,那么再次打开该页面时就不会触发load事件,IE10-浏览器不支持
2.0.8、pageshow
pageshow事件在页面加载时触发,包括第一次加载和从缓存加载两种情况。如果要指定页面每次加载(不管是不是从浏览器缓存)时都运行的代码,可以放在这个事件的监听函数
第一次加载时,它的触发顺序排在load事件后面。从缓存加载时,load事件不会触发,因为网页在缓存中的样子通常是load事件的监听函数运行后的样子,所以不必重复执行。同理,如果是从缓存中加载页面,网页内初始化的JavaScript脚本(比如DOMContentLoaded事件的监听函数)也不会执行
[注意]虽然这个事件的目标是document,但必须将其事件处理程序添加到window
pageshow事件有一个persisted属性,返回一个布尔值。页面第一次加载时或没有从缓存加载时,这个属性是false;当页面从缓存加载时,这个属性是true
[注意]上面的例子使用了私有作用域,以防止变量showCount进入全局作用域。如果单击了浏览器的“刷新”按钮,那么showCount的值就会被重置为0,因为页面已经完全重新加载了
2.0.9、pagehide
与pageshow事件对应的是pagehide事件,该事件会在浏览器卸载页面的时候触发,而且是在unload事件之前触发。与pageshow事件一样,pagehide在document上面触发,但其事件处理程序必须要添加到window对象
[注意]指定了onunload事件处理程序的页面会被自动排除在bfcache之外,即使事件处理程序是空的。原因在于,onunload最常用于撤销在onload中所执行的操作,而跳过onload后再次显示页面很可能就会导致页面不正常
pagehide事件的event对象也包含persisted属性,不过其用途稍有不同。如果页面是从bfcache中加载的,那么persisted的值就是true;如果页面在卸载之后会被保存在bfcache中,那么persisted的值也会被设置为true。因此,当第一次触发pageshow时,persisted的值一定是false,而在第一次触发pagehide时,persisted就会变成true(除非页面不会被保存在bfcache中)
window.onpagehide = function(e){ e = e || event; console.log(e.persisted); }
使用方法:
1、取消默认的返回操作
function pushHistory(){ var state = { title: "title", url: "#" } window.history.pushState(state, "title", "#"); } pushHistory()
2、history.js用于兼容html4,也可以监听pushState与replaceSatea
history在某些浏览器上支持还不是特别好,可以引用这个实现兼容的js
https://github.com/browserstate/history.js
2.1、原理
利用ajax请求替代了a标签的默认跳转,然后利用html5中的API修改了url
API: history.pushState 和 history.replaceState
两个 API 都会操作浏览器的历史记录,而不会引起页面的刷新,pushState会增加一条新的历史记录,而replaceState则会替换当前的历史记录。
window.history.pushState(null, null, "name/foo"); //url: https://best.cnblogs.com/name/foo window.history.pushState(null, null, "/name/bar"); //url: https://best.cnblogs.com/name/bar
url是统一资源定位符,对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。互联网上的每个文件都有一个唯一的URL,它包含的信息指出文件的位置以及浏览器应该怎么处理它。
2.2、jQuery-Pjax
2.2.1、pjax概要
pjax是jquery的一个插件,它使用ajax和pushState两个技术改善用户的网页浏览体验。具体来说,当用户使用a标签切换页面时,可以实现局部刷新的技术。
github源码:https://github.com/defunkt/jquery-pjax
pjax主要做两方面的事儿:
- 用户点击链接发送
ajax
请求,服务器得到请求返回需要填充的HTML片段,客户端得到HTML片段然后插入更新区域 - 页面填充完毕后,使用
pushState
更新当前的URL
这个过程能实现页面局部刷新,比传统的页面切换刷新的体验好一些,因为:
- 只下载需要的HTML页面片段,没有JS、CSS解析
- 如果服务端配置了正确的pjax请求,则只返回要更新的HTML片段,客户端只更新必要的内容,避免了页面重新渲染的过程。
优点:
减轻服务端压力
按需请求,每次只需加载页面的部分内容,而不用重复加载一些公共的资源文件和不变的页面结构,大大减小了数据请求量,以减轻对服务器的带宽和性能压力,还大大提升了页面的加载速度。
优化页面跳转体验
常规页面跳转需要重新加载画面上的内容,会有明显的闪烁,而且往往和跳转前的页面没有连贯性,用户体验不是很好。如果再遇上页面比较庞大、网速又不是很好的情况,用户体验就更加雪上加霜了。使用pjax后,由于只刷新部分页面,切换效果更加流畅,而且可以定制过度动画,在等待页面加载的时候体验就比较舒服了。
缺点:
不支持一些低版本的浏览器(如IE系列)
pjax使用了pushState来改变地址栏的url,这是html5中history的新特性,在某些旧版浏览器中可能不支持。不过pjax会进行判断,功能不适用的时候会执行默认的页面跳转操作。
使服务端处理变得复杂
要做到普通请求返回完整页面,而pjax请求只返回部分页面,服务端就需要做一些特殊处理,当然这对于设计良好的后端框架来说,添加一些统一处理还是比较容易的,自然也没太大问题。另外,即使后台不做处理,设置pjax的fragment参数来达到同样的效果。
综合来看,pajx的优点很强势,缺点也几乎可以忽略,还是非常值得推荐的,尤其是类似博客这种大部分情况下只有主体内容变化的网站。关键它使用简单、学习成本小,即时全站只有极个别页面能用得到,尝试下没什么损失。pjax的github主页介绍的已经很详细了,想了解更多可以看下源码。
2.2.2、使用方法
1. 客户端
客户端设置分两步:
- 下载插件,包括jquery1.8+,或者npm安装。https://github.com/defunkt/jquery-pjax
- 初始化pjax插件,并有条件的拦截
a
标签跳转。
初始化
$.fn.pjax
下面代码表示:当selector
被点击时,执行ajax请求,并将返回的HTML字符串填充在container
标记的位置。
$(document).pjax(selector, [container], options)
参数说明
- selector:click事件的选择器
- container:pjax容器id
- options :配置参数
pjax参数配置
key | default | description |
---|---|---|
timeout |
650 | ajax请求如果超时将触发强制刷新 |
push |
true | 使用 [pushState][] 在浏览器中添加导航记录 |
replace |
false | 是否使用replace方式改变URL |
maxCacheLength |
20 | 返回的HTML片段字符串最大缓存数 |
version |
当前pjax版本 | |
scrollTo |
0 | 当页面导航切换时滚动到的位置. 如果想页面切换不做滚动重置处理,请传入false . |
type |
"GET" |
使用ajax的模板请求方法,参考 $.ajax |
dataType |
"html" |
模板请求时的type,参考 $.ajax |
container |
内容替换的CSS选择器 | |
url |
link.href | 用于ajax请求的url,可以是字符串或者返回字符串的函数 |
target |
link | eventually the relatedTarget value for pjax events |
fragment |
从服务端返回的HTML字符串中子内容所在的CSS选择器,用于当服务端返回了整个HTML文档,但要求pjax局部刷新时使用。 |
可以使用下面的方式动态设置options:
$.pjax.defaults.timeout = 1200
事件
1. 点击链接后触发的一系列事件, 除了 pjax:click
和 pjax:clicked
的事件源是点击的按钮,其他事件的事件源都是要替换内容的容器。可以在 pjax:start
事件触发时开始过度动画,在 pjax:end
事件触发时结束过度动画。
事件名 | 支持取消 | 参数 | 说明 |
---|---|---|---|
pjax:click | ✔ |
options | 点击按钮时触发。可调用 e.preventDefault(); 取消pjax |
pjax:beforeSend | ✔ |
xhr, options | ajax 执行 beforeSend 函数时触发,可在回调函数中设置额外的请求头参数。可调用 e.preventDefault(); 取消 pjax |
pjax:start | xhr, options | pjax 开始(与服务器连接建立后触发) |
|
pjax:send | xhr, options | pjax:start 之后触发 |
|
pjax:clicked | options | ajax 请求开始后触发 |
|
pjax:beforeReplace | contents, options | ajax 请求成功,内容替换渲染前触发 |
|
pjax:success | data, status, xhr, options | 内容替换成功后触发 | |
pjax:timeout | ✔ |
xhr, options | ajax请求超时后触发。可调用 e.preventDefault(); 继续等待 ajax 请求结束 |
pjax:error | ✔ |
xhr, textStatus, error, options | ajax 请求失败后触发。默认失败后会跳转url,如要阻止跳转可调用 e.preventDefault(); |
pjax:complete | xhr, textStatus, options | ajax 请求结束后触发,不管成功还是失败 |
|
pjax:end | xhr, options | pjax 所有事件结束后触发 |
- 注意:
pjax:beforeReplace
事件前pjax
会调用extractContainer
函数处理页面内容,即以script[src]
的形式引入的js
脚本不会被重复加载,有必要可以改下源码。
2. 浏览器前进/后退导航时触发的事件
事件名 | 参数 | 说明 |
---|---|---|
pjax:popstate | 页面导航方向: 'forward'/'back'(前进/后退) | |
pjax:start | null, options |
pjax 开始 |
pjax:beforeReplace | contents, options | 内容替换渲染前触发,如果缓存了要导航页面的内容则使用缓存,否则使用 pjax 加载 |
pjax:end | null, options |
pjax 结束 |
初始化一般的做法是做好HTML结构,有条件的触发pjax跳转请求:
<div data-pjax> <a data-pjax href="/to/somewhere">ToSomewhere1</a> <a data-pjax href="/to/somewhere">ToSomewhere2/a> </div> <section id="pjax-container"> <!-- 在这里更新返回的HTML字符串 --> </section> $(document).pjax('[data-pjax] a, a[data-pjax]', '#pjax-container')
常用方法
/** * 方式一 按钮父节点监听事件 * * @param selector 触发点击事件的按钮 * @param container 展示刷新内容的容器,也就是会被替换的部分 * @param options 参数 */ $(document).pjax(selector, [container], options); // 方式二 直接对按钮监听,可以不用指定容器,使用按钮的data-pjax属性值查找容器 $("a[data-pjax]").pjax(); // 方式三 常规的点击事件监听方式 $(document).on('click', 'a', $.pjax.click); $(document).on('click', 'a', function(event) { var container = $(this).closest('[data-pjax-container]'); $.pjax.click(event, container); }); // 下列是源码中介绍的其他用法,由于本人暂时没有那些需求暂时没深究,有兴趣的各位自己试试看哈 // 表单提交 $(document).on('submit', 'form', function(event) { var container = $(this).closest('[data-pjax-container]'); $.pjax.submit(event, container); }); // 加载内容到指定容器 $.pjax({ url: this.href, container: '#main' }); // 重新当前页面容器的内容 $.pjax.reload('#container');
2. 服务端
服务端也比较简单,监听HTTP的header中有X-PJAX
的ajax请求,如果有则返回HTML片段,而不是整个HTML。
示例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Pjax</title> </head> <body> <a href="page3.html" data-pjax>页面三</a> | <a href="page4.html">页面四</a> | <div id="container"> </div> <script src="../../js/jquery-1.12.4/jquery-1.12.4.js"></script> <script src="../../js/pjax/jquery.pjax.js"></script> <script> $(document).pjax("a","#container",{}); </script> </body> </html>
模拟服务器端:
page3.html:
<div style="background:lightcoral"> <h2>这是页面三</h2> </div>
page4.html:
<div style="background:lightseagreen"> <h2>这是页面四</h2> </div>
运行结果:
解释:$(document).pjax('a', '#container')其中a是触发元素,#container是装载pjax返回内容的容器,下面也是这样。
客户端:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Pjax</title> </head> <body> <button data-href="page3.html">页面三</button> <button data-href="page4.html">页面四</button> <div id="container"> </div> <script src="../../js/jquery-1.12.4/jquery-1.12.4.js"></script> <script src="../../js/pjax/jquery.pjax.js"></script> <script> $("button[data-href]").click(function () { $.pjax({ container: "#container", url: $(this).data("href") }); }); </script> </body> </html>
运行结果:
2.2.3、API介绍
这部分用于更细粒度的控制。
$.pjax.click
示例:
// 确定能使用pjax时 if ($.support.pjax) { $(document).on('click', 'a[data-pjax]', function(event) { var container = $(this).closest('[data-pjax-container]') var containerSelector = '#' + container.id $.pjax.click(event, {container: containerSelector}) }) }
$.pjax.submit
用pjax提交表单
$(document).on('submit', 'form[data-pjax]', function(event) { $.pjax.submit(event, '#pjax-container') })
$.pjax.reload
对当前URL使用pjax的方式重新获取HTML代码片段,并且在指定容器替换,这个过程不添加新的历史记录。(子片段重刷新)
$.pjax.reload('#pjax-container', options)
$.pjax
不是通过click
触发pjax的时候使用。比如某些操作后自动触发pjax的过程。如果能获取到click
的event
事件时,建议使用$.pjax-click(event)
替换。
function applyFilters() { var url = urlForFilters() $.pjax({url: url, container: '#pjax-container'}) }
2.2.4、pjax生命周期
pjax生命周期简单的说:
生命周期和Loading组件使用密切:
$(document).on('pjax:send', function() { $('#loading').show() }) $(document).on('pjax:complete', function() { $('#loading').hide() })
2.2.5、高级技巧
子页面加载完毕初始化其中的插件/组件
pjax只是请求HTML片段之后插入指定位置,因此片段内的JS插件/组件初始化需要在pjax:end
事件后执行。
$(document).on('ready pjax:end', function(event) { $(event.target).initializeMyPlugin() })
这段代码会在document ready或者container ready后执行initializeMyPlugin
初始化方法(包括前进后退)。
强制reload
当使用pjax导致整个页面被强制刷时,可能的原因是:
- 当返回的HTML片段包含
<html>
标签且fragment
选择器没有指定时。如果指定了fragment
选择器,pjax将从HTML文档中提取需要局部刷新的子片段。 - 服务端返回的内容为空时。
- HTTP响应的code是 4xx 或者 5xx。
浏览器重定向
在响应头中设置X-PJAX-URL
,例如:
request.headers['X-PJAX-URL'] = "http://example.com/hello"
Layout重新加载
当客户端页面的pjax版本和服务器返回的pjax版本不一致时,页面会重新刷新。
客户端页面的pjax版本:
<meta http-equiv="x-pjax-version" content="v123">
如果服务器修改了版本则重新刷新:
response.headers['X-PJAX-Version'] = "xxxx修改版本名称xxxx"
2.2.5、使用建议
这货需要服务端密切配合,如果服务端没设置好,要不就是请求只返回HTML片段,要不每次页面切换都是重新加载页面。
如果服务端无法完成这些配置,只能ajax异步由前端自己拼接HTML来做,建议使用MV*的库来做这部分。
插件伴侣——NProgress
官网:http://ricostacruz.com/nprogress/
github:https://github.com/rstacruz/nprogress/
比较漂亮的一款进度条插件,用法十分简单,很适合做pjax的过度动画,详细用法在该项目github上有介绍
三、Vue Router
3.1、概要
Vue Router是一个Vue核心插件,是Vue.js官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。vue的单页面应用是基于路由和组件的,路由用于设定访问路径,并将路径和组件映射起来。传统的页面应用,是用一些超链接来实现页面切换和跳转的。在vue router单页面应用中,则是路径之间的切换,也就是组件的切换。包含的功能有:
- 嵌套的路由/视图表
- 模块化的、基于组件的路由配置
- 路由参数、查询、通配符
- 基于 Vue.js 过渡系统的视图过渡效果
- 细粒度的导航控制
- 带有自动激活的 CSS class 的链接
- HTML5 历史模式或 hash 模式,在 IE9 中自动降级
- 自定义的滚动条行为
3.1.1、资源
中文帮助:https://router.vuejs.org/zh/
英文帮助:https://router.vuejs.org/
Git源码:https://github.com/vuejs/vue-router
3.1.2、概念
路由中有三个基本的概念 route, routes, router。
1、 route,它是一条路由,由这个英文单词也可以看出来,它是单数, Home按钮 => home内容, 这是一条route, about按钮 => about 内容, 这是另一条路由。
2、 routes 是一组路由,把上面的每一条路由组合起来,形成一个数组。[{home 按钮 =>home内容 }, { about按钮 => about 内容}]
3、router 是一个机制,相当于一个管理者,它来管理路由。因为routes 只是定义了一组路由,它放在哪里是静止的,当真正来了请求,怎么办? 就是当用户点击home 按钮的时候,怎么办?这时router 就起作用了,它到routes 中去查找,去找到对应的 home 内容,所以页面中就显示了 home 内容。
4、客户端中的路由,实际上就是dom 元素的显示和隐藏。当页面中显示home 内容的时候,about 中的内容全部隐藏,反之也是一样。客户端路由有两种实现方式:基于hash 和基于html5 history api。
3.1.3、底层实现
SPA(single page application):单一页面应用程序,有且只有一个完整的页面;当它在加载页面的时候,不会加载整个页面的内容,而只更新某个指定的容器中内容。
单页面应用(SPA)的核心之一是:
1.更新视图而不重新请求页面;
2.vue-router在实现单页面前端路由时,提供了三种方式:Hash模式、History模式、abstract模式,根据mode参数来决定采用哪一种方式。
路由模式
vue-router 提供了三种运行模式:
● hash: 使用 URL hash 值来作路由,默认模式。
● history: 依赖 HTML5 History API 和服务器配置。查看 HTML5 History 模式。
● abstract: 支持所有 JavaScript 运行环境,如 Node.js 服务器端。
3.2、安装
3.2.1、直接下载 / CDN
https://unpkg.com/vue-router/dist/vue-router.js
Unpkg.com 提供了基于 NPM 的 CDN 链接。上面的链接会一直指向在 NPM 发布的最新版本。你也可以像 https://unpkg.com/vue-router@2.0.0/dist/vue-router.js 这样指定 版本号 或者 Tag。
在 Vue 后面加载 vue-router,它会自动安装的:
<script src="/path/to/vue.js"></script> <script src="/path/to/vue-router.js"></script>
页面中直接引用CDN
<script src="https://unpkg.com/vue/dist/vue.js"></script> <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
3.2.2、NPM
使用nodejs包管理器安装
npm install vue-router
如果在一个模块化工程中使用它,必须要通过 Vue.use() 明确地安装路由功能:
import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter)
如果使用全局的 script 标签,则无须如此 (手动安装)。
3.2.3、构建开发版
如果你想使用最新的开发版,就得从 GitHub 上直接 clone,然后自己 build 一个 vue-router。
git clone https://github.com/vuejs/vue-router.git node_modules/vue-router cd node_modules/vue-router npm install npm run build
3.3、第一个路由示例
3.3.1、网页版
用 Vue.js + Vue Router 创建单页应用,是非常简单的。使用 Vue.js ,我们已经可以通过组合组件来组成应用程序,当你要把 Vue Router 添加进来,我们需要做的是,将组件 (components) 映射到路由 (routes),然后告诉 Vue Router 在哪里渲染它们。下面是个基本例子:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Hello Router App</title> </head> <body> <div id="app"> <h1>Hello Router App!</h1> <p> <!-- 使用 router-link 组件来导航. --> <!-- 通过传入 `to` 属性指定链接. --> <!-- <router-link> 默认会被渲染成一个 `<a>` 标签 --> <router-link to="/foo">Go to Foo</router-link> <router-link to="/bar">Go to Bar</router-link> <p> <a href="#/foo">foo</a> | <a href="#/bar">bar</a> </p> </p> <!-- 路由出口 --> <!-- 路由匹配到的组件将渲染在这里 --> <router-view></router-view> </div> <script src="https://unpkg.com/vue/dist/vue.js"></script> <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script> <script> // 0. 如果使用模块化机制编程,导入Vue和VueRouter,要调用 Vue.use(VueRouter) // 1. 定义 (路由) 组件。 // 可以从其他文件 import 进来 const Foo = {template: '<div>foo</div>'} const Bar = {template: '<div>bar</div>'} // 2. 定义路由 // 每个路由应该映射一个组件。 其中"component" 可以是 // 通过 Vue.extend() 创建的组件构造器, // 或者,只是一个组件配置对象。 // 我们晚点再讨论嵌套路由。 const routes = [ {path: '/foo', component: Foo}, {path: '/bar', component: Bar} ] // 3. 创建 router 实例,然后传 `routes` 配置 // 你还可以传别的配置参数, 不过先这么简单着吧。 const router = new VueRouter({ routes // (缩写) 相当于 routes: routes }) // 4. 创建和挂载根实例。 // 记得要通过 router 配置参数注入路由, // 从而让整个应用都有路由功能 const app = new Vue({ el: "#app", router: router, }); </script> </body> </html>
运行结果:
3.3.2、Vue-cli版
安装:
npm install vue-router / yarn add vue-router
若在构建vue-cli的时候,在询问“nstall vue-router”(是否安装vue-router)时,选择“Y”,这里就不用重复安装vue-router。使用WebStorm创建一个vue-cli项目,选择使用router:
在src/components下创建三个组件:
Default.vue
<template> <div> <h2>Default</h2> <p>{{msg}}</p> </div> </template> <script> export default { data() { return { msg: "这是默认组件" } } } </script> <style scoped> h2 { color:crimson; } </style>
Home.vue
<template> <div> <h2>Home</h2> <p>{{msg}}</p> </div> </template> <script> export default { data() { return { msg: "我是Home 组件" } } } </script> <style scoped> h2 { color: dodgerblue; } </style>
About.Vue
<template> <div> <h2>About</h2> <p>{{msg}}</p> </div> </template> <script> export default { data() { return { msg: "我是About 组件" } } } </script> <style scoped> h2 { color:springgreen; } </style>
在 App.vue中 定义<router-link > 和 </router-view>
<template> <div> <img src="./assets/logo.png"> <header> <!-- router-link 定义点击后导航到哪个路径下 --> <router-link to="/">Default</router-link> <router-link to="/home">Home</router-link> <router-link to="/about">About</router-link> </header> <!-- 对应的组件内容渲染到router-view中 --> <router-view></router-view> </div> </template> <script> export default { } </script>
在src/router目录下定义hello.js路由配置文件:
import Vue from 'vue' import Router from 'vue-router' import Home from '@/components/Home' import About from '@/components/About' import Default from '@/components/Default' Vue.use(Router); export default new Router({ routes: [ { path: '/', name: 'Default', component: Default }, { path: '/home', name: 'Home', component: Home }, { path: '/about', name: 'About', component: About } ] })
修改main.js,使用路由:
// The Vue build version to load with the `import` command // (runtime-only or standalone) has been set in webpack.base.conf with an alias. import Vue from 'vue' import App from './App' import router from './router/hello' Vue.config.productionTip = false /* eslint-disable no-new */ new Vue({ el: '#app', router, components: { App }, render:r=>r(App) })
index.html页面如下:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <title>Hello Router</title> </head> <body> <div id="app"> </div> </body> </html>
目录结构如下:
运行结果:
单页切换
3.4、路由模式
vue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载
http://localhost:8080/#/home
如果不想要很hash,可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面
const router = new VueRouter({ mode: 'history', routes: [...] }
当使用 history 模式时,URL 就像正常的 url
http://localhost:8080/home
不过这种模式需要后台配置支持。如果后台没有正确的配置,当用户在浏览器直接访问 http://site.com/user/id 就会返回 404,详细请参考:https://router.vuejs.org/zh/guide/essentials/history-mode.html
3.5、重定向
重定向通过 routes
配置来完成,下面例子是从 /a
重定向到 /b
const router = new VueRouter({ routes: [ { path: '/a', redirect: '/b' } ] })
重定向的目标也可以是一个命名的路由:
const router = new VueRouter({ routes: [ { path: '/a', redirect: { name: 'foo' }} ] })
甚至是一个方法,动态返回重定向目标:
const router = new VueRouter({ routes: [ { path: '/a', redirect: to => { // 方法接收 目标路由 作为参数 // return 重定向的 字符串路径/路径对象
return '/home' }} ] })
对于不识别的URL地址来说,常常使用重定向功能,将页面定向到首页显示
const Foo = { template: '<div>foo</div>' } const Bar = { template: '<div>bar</div>' } const routes = [ { path: '/foo', component: Foo }, { path: '/bar', component: Bar }, { path: '*', redirect: "/foo"}, ]
示例:
import Vue from 'vue' import Router from 'vue-router' import Home from '@/components/Home' import About from '@/components/About' import Default from '@/components/Default' Vue.use(Router); export default new Router({ mode: "history", routes: [ { path: '/', name: 'Default', component: Default }, { path: '/home', name: 'Home', component: Home }, { path: '/about', name: 'About', component: About }, { path: '/gohome', redirect:'/home' /*路径*/ }, { path: '/goabout', redirect:'About' /*命名*/ } ] })
结果:
3.6、别名
重定向是指,当用户访问 /a
时,URL 将会被替换成 /b
,然后匹配路由为 /b
,那么别名是什么呢?/a
的别名是 /b
,意味着,当用户访问 /b
时,URL 会保持为 /b
,但是路由匹配则为 /a
,就像用户访问 /a
一样
上面对应的路由配置为
const router = new VueRouter({ routes: [ { path: '/a', component: A, alias: '/b' } ] })
『别名』的功能可以自由地将 UI 结构映射到任意的 URL,而不是受限于配置的嵌套路由结构
处理首页访问时,常常将index设置为别名,比如将'/home'的别名设置为'/index'。但是,要注意的是,<router-link to="/home">的样式在URL为/index时并不会显示。因为,router-link只识别出了home,而无法识别index
示例:
配置
import Vue from 'vue' import Router from 'vue-router' import Home from '@/components/Home' import About from '@/components/About' import Default from '@/components/Default' Vue.use(Router); export default new Router({ mode: "history", routes: [ { path: '/', name: 'Default', component: Default }, { path: '/home', name: 'Home', component: Home, alias:'/h' /*别名*/ }, { path: '/about', name: 'About', component: About }, { path: '/gohome', redirect:'/home' /*路径,重定向*/ }, { path: '/goabout', redirect:'About' /*命名,重定向*/ } ] })
链接:
<template> <div> <img src="./assets/logo.png"> <header> <!-- router-link 定义点击后导航到哪个路径下 --> <router-link to="/">Default</router-link> | <router-link to="/home">Home</router-link> | <router-link to="/about">About</router-link> | <router-link to="/gohome">GoHome</router-link> | <router-link to="/goabout">GoAbout</router-link> | <router-link to="/h">H</router-link> </header> <!-- 对应的组件内容渲染到router-view中 --> <router-view></router-view> </div> </template> <script> export default { } </script> <style scoped> a{ color: #777; } a:hover{ color:orangered; } </style>
结果:
3.7、根路径
设置根路径,需要将path设置为'/'
<p> <router-link to="/">index</router-link> <router-link to="/foo">Go to Foo</router-link> <router-link to="/bar">Go to Bar</router-link> </p> const routes = [ { path: '/', component: Home }, { path: '/foo', component: Foo }, { path: '/bar', component: Bar }, ]
默认运行效果:
点击Go to Bar
但是,由于默认使用的是全包含匹配,即'/foo'、'/bar'也可以匹配到'/',如果需要精确匹配,仅仅匹配'/',则需要在router-link中设置exact属性
<p> <router-link to="/" exact>index</router-link> <router-link to="/foo">Go to Foo</router-link> <router-link to="/bar">Go to Bar</router-link> </p> const routes = [ { path: '/', component: Home }, { path: '/foo', component: Foo }, { path: '/bar', component: Bar }, ]
运行结果:
3.8、嵌套路由
实际项目中的应用界面,通常由多层嵌套的组件组合而成。同样地,URL中各段动态路径也按某种结构对应嵌套的各层组件
借助 vue-router
,使用嵌套路由配置,就可以很简单地表达这种关系
<div id="app"> <p> <router-link to="/" exact>index</router-link> <router-link to="/foo">Go to Foo</router-link> <router-link to="/bar">Go to Bar</router-link> </p> <router-view></router-view> </div>
const Home = { template: '<div>home</div>' } const Foo = { template: ` <div> <p> <router-link to="/foo/foo1">to Foo1</router-link> <router-link to="/foo/foo2">to Foo2</router-link> <router-link to="/foo/foo3">to Foo3</router-link> </p> <router-view></router-view> </div> ` } const Bar = { template: '<div>bar</div>' } const Foo1 = { template: '<div>Foo1</div>' } const Foo2 = { template: '<div>Foo2</div>' } const Foo3 = { template: '<div>Foo3</div>' }
const routes = [ { path: '/', component: Home }, { path: '/foo', component: Foo ,children:[ {path:'foo1',component:Foo1}, {path:'foo2',component:Foo2}, {path:'foo3',component:Foo3}, ]}, { path: '/bar', component: Bar }, ]
要特别注意的是,router的构造配置中,children属性里的path属性只设置为当前路径,因为其会依据层级关系;而在router-link的to属性则需要设置为完全路径
如果要设置默认子路由,即点击foo时,自动触发foo1,则需要进行如下修改。将router配置对象中children属性的path属性设置为'',并将对应的router-link的to属性设置为'/foo'
const Foo = { template: `
<div>
<p>
<router-link to="/foo" exact>to Foo1</router-link>
<router-link to="/foo/foo2">to Foo2</router-link>
<router-link to="/foo/foo3">to Foo3</router-link>
</p>
<router-view></router-view>
</div>
` }
const routes = [ { path: '/', component: Home }, { path: '/foo', component: Foo ,children:[ {path:'',component:Foo1}, {path:'foo2',component:Foo2}, {path:'foo3',component:Foo3}, ]}, { path: '/bar', component: Bar }, ]
Foo1.Vue
<template> <div> <h2>这是Foo1</h2> </div> </template> <script> export default { name: "Foo1" } </script> <style scoped> h2 { color: purple; } </style>
Foo2.Vue
<template> <div> <h2>这是Foo2</h2> </div> </template> <script> export default { name: "Foo2" } </script> <style scoped> h2 { color: orange; } </style>
Foo3.Vue
<template> <div> <h2>这是Foo3</h2> </div> </template> <script> export default { name: "Foo3" } </script> <style scoped> h2 { color:springgreen; } </style>
Foo.Vue
<template> <div> <h2>Foo</h2> <p>{{msg}}</p> <div> <p> <router-link to="/foo/foo1">to Foo1</router-link> <router-link to="/gofoo2">to Foo2</router-link> <router-link to="/foo/foo3">to Foo3</router-link> </p> <div> <router-view></router-view> </div> </div> </div> </template> <script> export default { data() { return { msg: "我是Foo组件" } } } </script> <style scoped> h2 { color: dodgerblue; } </style>
hello.js路由配置
import Vue from 'vue' import Router from 'vue-router' import Foo from '@/components/Foo' import Bar from '@/components/Bar' import Home from '@/components/Home' import Foo1 from '@/components/Foo1' import Foo2 from '@/components/Foo2' import Foo3 from '@/components/Foo3' Vue.use(Router); export default new Router({ mode: "history", routes: [ { path: '/', name: 'Home', component: Home }, { path: '/foo', name: 'Foo', component: Foo, children:[ {path: '/foo/foo1', component: Foo1}, {path: '/gofoo2', component: Foo2}, {path: 'foo3', component: Foo3}, ] }, { path: '/bar', name: 'Bar', component: Bar } ] })
运行结果:
默认
Foo3
3.9、命名路由
有时,通过一个名称来标识一个路由显得更方便,特别是在链接一个路由,或者是执行一些跳转时。可以在创建Router实例时,在routes
配置中给某个路由设置名称
const router = new VueRouter({
routes: [
{
path: '/user/:userId',
name: 'user',
component: User
}
]
})
要链接到一个命名路由,可以给 router-link
的 to
属性传一个对象:
<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>
这跟代码调用 router.push()
是一回事
router.push({ name: 'user', params: { userId: 123 }})
这两种方式都会把路由导航到 /user/123
路径
命名路由的常见用途是替换router-link中的to属性,如果不使用命名路由,由router-link中的to属性需要设置全路径,不够灵活,且修改时较麻烦。使用命名路由,只需要使用包含name属性的对象即可
[注意]如果设置了默认子路由,则不要在父级路由上设置name属性
<div id="app"> <p> <router-link to="/" exact>index</router-link> <router-link :to="{ name: 'foo1' }">Go to Foo</router-link> <router-link :to="{ name: 'bar' }">Go to Bar</router-link> </p> <router-view></router-view> </div>
const Home = { template: '<div>home</div>' } const Foo = { template: ` <div> <p> <router-link :to="{ name: 'foo1' }" exact>to Foo1</router-link> <router-link :to="{ name: 'foo2' }" >to Foo2</router-link> <router-link :to="{ name: 'foo3' }" >to Foo3</router-link> </p> <router-view></router-view> </div> ` } const Bar = { template: '<div>bar</div>' } const Foo1 = { template: '<div>Foo1</div>' } const Foo2 = { template: '<div>Foo2</div>' } const Foo3 = { template: '<div>Foo3</div>' }
const routes = [
{ path: '/', name:'home', component: Home },
{ path: '/foo', component: Foo ,children:[
{path:'',name:'foo1', component:Foo1},
{path:'foo2',name:'foo2', component:Foo2},
{path:'foo3',name:'foo3', component:Foo3},
]},
{ path: '/bar', name:'bar', component: Bar },
]
hello.js
children:[ {name:'f0',path: '', component: Foo1}, {name:'f1',path: '/foo/foo1', component: Foo1}, {name:'f2',path: '/gofoo2', component: Foo2}, {name:'f3',path: 'foo3', component: Foo3} ]
Foo.vue
<p> <router-link to="/foo" exact>to Default</router-link> <router-link :to="{name:'f1'}">to Foo1</router-link> <router-link to="/gofoo2">to Foo2</router-link> <router-link to="/foo/foo3">to Foo3</router-link> </p>
结果如下所示
3.10、命名视图
有时候想同时(同级)展示多个视图,而不是嵌套展示,例如创建一个布局,有 sidebar
(侧导航) 和 main
(主内容) 两个视图,这个时候命名视图就派上用场了。可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。如果 router-view
没有设置名字,那么默认为 default
<router-view class="view one"></router-view> <router-view class="view two" name="a"></router-view> <router-view class="view three" name="b"></router-view>
一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件。确保正确使用components
配置
const router = new VueRouter({
routes: [
{
path: '/',
components: {
default: Foo,
a: Bar,
b: Baz
}
}
]
})
下面是一个实例
<div id="app"> <p> <router-link to="/" exact>index</router-link> <router-link :to="{ name: 'foo' }">Go to Foo</router-link> <router-link :to="{ name: 'bar' }">Go to Bar</router-link> </p> <router-view></router-view> <router-view name="side"></router-view> </div>
const Home = { template: '<div>home</div>' } const Foo = { template: '<div>Foo</div>'} const MainBar = { template: '<div>mainBar</div>' } const SideBar = { template: '<div>sideBar</div>' } const routes = [ { path: '/', name:'home', component: Home }, { path: '/foo', name:'foo', component: Foo}, { path: '/bar', name:'bar', components: { default: MainBar, side:SideBar } }, ]
结果如下所示
3.11、动态路径
经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,有一个 User
组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么,可以在 vue-router
的路由路径中使用动态路径参数(dynamic segment)来达到这个效果
const User = { template: '<div>User</div>' } const router = new VueRouter({ routes: [ // 动态路径参数以冒号开头 { path: '/user/:id', component: User } ] })
现在,像 /user/foo
和 /user/bar
都将映射到相同的路由
下面是一个比较完整的实例,path:'/user/:id?'表示有没有子路径都可以匹配
<div id="app"> <router-view></router-view> <br> <p> <router-link to="/" exact>index</router-link> <router-link :to="{name:'user'}">User</router-link> <router-link :to="{name:'bar'}">Go to Bar</router-link> </p> </div> const home = { template: '<div>home</div>'}; const bar = { template: '<div>bar</div>'}; const user = {template: `<div> <p>user</p> <router-link style="margin: 0 10px" :to="'/user/' + item.id" v-for="item in userList" key="item.id">{{item.userName}}</router-link> </div>`, data(){ return{userList:[{id:1,userName:'u1'},{id:2,userName:'u2'},{id:3,userName:'u3'}]} } }; const app = new Vue({ el:'#app', router:new VueRouter({ routes: [ { path: '/', name:'home', component:home }, { path: '/user/:id?', name:'user', component:user}, { path: '/bar', name:'bar', component:bar}, ], }), })
一个路径参数使用冒号 :
标记。当匹配到一个路由时,参数值会被设置到 this.$route.params
,可以在每个组件内使用。于是,可以更新 User
的模板,输出当前用户的 ID:
const User = { template: '<div>User {{ $route.params.id }}</div>' }
下面是一个实例
<div id="app"> <p> <router-link to="/user/foo">/user/foo</router-link> <router-link to="/user/bar">/user/bar</router-link> </p> <router-view></router-view> </div> <script src="vue.js"></script> <script src="vue-router.js"></script> <script> const User = { template: `<div>User {{ $route.params.id }}</div>` } const router = new VueRouter({ routes: [ { path: '/user/:id', component: User } ] }) const app = new Vue({ router }).$mount('#app') </script>
可以在一个路由中设置多段『路径参数』,对应的值都会设置到 $route.params
中。例如:
模式 匹配路径 $route.params /user/:username /user/evan { username: 'evan' } /user/:username/post/:post_id /user/evan/post/123 { username: 'evan', post_id: 123 }
除了 $route.params
外,$route
对象还提供了其它有用的信息,例如,$route.query
(如果 URL 中有查询参数)、$route.hash
等等
【响应路由参数的变化】
使用路由参数时,例如从 /user/foo
导航到 user/bar
,原来的组件实例会被复用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会再被调用
复用组件时,想对路由参数的变化作出响应的话,可以简单地 watch(监测变化) $route
对象:
const User = { template: '...', watch: { '$route' (to, from) { // 对路由变化作出响应... } } }
[注意]有时同一个路径可以匹配多个路由,此时,匹配的优先级就按照路由的定义顺序:谁先定义的,谁的优先级就最高
下面是一个实例
const home = { template: '<div>home</div>'}; const bar = { template: '<div>bar</div>'}; const user = {template: `<div> <p>user</p> <router-link style="margin: 0 10px" :to="'/user/' +item.type + '/'+ item.id" v-for="item in userList" key="item.id">{{item.userName}}</router-link> <div v-if="$route.params.id"> <div>id:{{userInfo.id}};userName:{{userInfo.userName}} ;type:{{userInfo.type}};</div> </div> </div>`, data(){ return{ userList:[{id:1,type:'vip',userName:'u1'},{id:2,type:'common',userName:'u2'},{id:3,type:'vip',userName:'u3'}], userInfo:null, } }, methods:{ getData(){ let id = this.$route.params.id; if(id){ this.userInfo = this.userList.filter((item)=>{ return item.id == id; })[0] }else{ this.userInfo = {}; } } }, created(){ this.getData(); }, watch:{ $route(){ this.getData(); }, } }; const app = new Vue({ el:'#app', router:new VueRouter({ routes: [ { path: '/', name:'home', component:home }, { path: '/user/:type?/:id?', name:'user', component:user}, { path: '/bar', name:'bar', component:bar}, ], }), })
3.12、查询字符串
实现子路由,除了使用动态参数,也可以使用查询字符串
const home = { template: '<div>home</div>'}; const bar = { template: '<div>bar</div>'}; const user = {template: `<div> <p>user</p> <router-link style="margin: 0 10px" :to="'/user/' +item.type + '/'+ item.id" v-for="item in userList" key="item.id">{{item.userName}}</router-link> <div v-if="$route.params.id"> <div>id:{{userInfo.id}};userName:{{userInfo.userName}} ;type:{{userInfo.type}};</div> <router-link to="?info=follow" exact>关注</router-link> <router-link to="?info=share" exact>分享</router-link> </div> </div>`, data(){ return{ userList:[{id:1,type:'vip',userName:'u1'},{id:2,type:'common',userName:'u2'},{id:3,type:'vip',userName:'u3'}], userInfo:null, } }, methods:{ getData(){ let id = this.$route.params.id; if(id){ this.userInfo = this.userList.filter((item)=>{ return item.id == id; })[0] }else{ this.userInfo = {}; } } }, created(){ this.getData(); }, watch:{ $route(){ this.getData(); }, } }; const app = new Vue({ el:'#app', router:new VueRouter({ routes: [ { path: '/', name:'home', component:home }, { path: '/user/:type?/:id?', name:'user', component:user}, { path: '/bar', name:'bar', component:bar}, ], }), })
当需要设置默认查询字符串时,进行如下设置
const user = {template: `<div> <p>user</p> <router-link style="margin: 0 10px" :to="{path:'/user/' +item.type + '/'+ item.id,query:{info:'follow'}}" v-for="item in userList" key="item.id">{{item.userName}}</router-link> <div v-if="$route.params.id"> <div>id:{{userInfo.id}};userName:{{userInfo.userName}} ;type:{{userInfo.type}};</div> <router-link to="?info=follow" exact>关注</router-link> <router-link to="?info=share" exact>分享</router-link> {{$route.query}} </div> </div>`, data(){ return{ userList:[{id:1,type:'vip',userName:'u1'},{id:2,type:'common',userName:'u2'},{id:3,type:'vip',userName:'u3'}], userInfo:null, } }, methods:{ getData(){ let id = this.$route.params.id; if(id){ this.userInfo = this.userList.filter((item)=>{ return item.id == id; })[0] }else{ this.userInfo = {}; } } }, created(){ this.getData(); }, watch:{ $route(){ this.getData(); }, } };
3.13、滚动行为
使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。 vue-router
能做到,而且更好,它可以自定义路由切换时页面如何滚动
[注意]这个功能只在 HTML5 history 模式下可用
当创建一个 Router 实例,可以提供一个 scrollBehavior
方法。该方法在前进、后退或切换导航时触发
const router = new VueRouter({
routes: [...],
scrollBehavior (to, from, savedPosition) {
// return 期望滚动到哪个的位置
}
})
scrollBehavior
方法返回 to
和 from
路由对象。第三个参数 savedPosition
当且仅当 popstate
导航 (通过浏览器的 前进/后退 按钮触发) 时才可用,返回滚动条的坐标{x:number,y:number}
如果返回一个布尔假的值,或者是一个空对象,那么不会发生滚动
scrollBehavior (to, from, savedPosition) {
return { x: 0, y: 0 }
}
对于所有路由导航,简单地让页面滚动到顶部。返回 savedPosition
,在按下 后退/前进 按钮时,就会像浏览器的原生表现那样:
scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { x: 0, y: 0 }
}
}
下面是一个实例,点击导航进行切换时,滚动到页面顶部;通过前进、后退按钮进行切换时,保持坐标位置
const router = new VueRouter({ mode:'history', routes , scrollBehavior (to, from, savedPosition){ if(savedPosition){ return savedPosition; }else{ return {x:0,y:0} } } })
还可以模拟『滚动到锚点』的行为:
scrollBehavior (to, from, savedPosition) {
if (to.hash) {
return {
selector: to.hash
}
}
}
下面是一个实例
<div id="app"> <router-view></router-view> <br> <p> <router-link to="/" exact>index</router-link> <router-link :to="{name:'foo' ,hash:'#abc'}">Go to Foo</router-link> <router-link :to="{ name: 'bar' }">Go to Bar</router-link> </p> </div>
const router = new VueRouter({ mode:'history', routes , scrollBehavior (to, from, savedPosition){ if(to.hash){ return { selector: to.hash } } if(savedPosition){ return savedPosition; }else{ return {x:0,y:0} } } })
3.14、过渡动效
<router-view>
是基本的动态组件,所以可以用 <transition>
组件给它添加一些过渡效果:
<transition> <router-view></router-view> </transition>
下面是一个实例
.router-link-active{background:pink;} .v-enter,.v-leave-to{ opacity:0; } .v-enter-active,.v-leave-active{ transition:opacity .5s; }
<div id="app"> <p> <router-link to="/" exact>index</router-link> <router-link :to="{name:'foo'}">Go to Foo</router-link> <router-link :to="{ name: 'bar' }">Go to Bar</router-link> <transition> <router-view></router-view> </transition> </p> </div>
【单个路由过渡】
上面的用法会给所有路由设置一样的过渡效果,如果想让每个路由组件有各自的过渡效果,可以在各路由组件内使用 <transition>
并设置不同的 name
const Foo = {
template: `
<transition name="slide">
<div class="foo">...</div>
</transition>
`
}
const Bar = {
template: `
<transition name="fade">
<div class="bar">...</div>
</transition>
`
}
3.15、路由元信息
定义路由的时候可以配置 meta
字段:
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
children: [
{
path: 'bar',
component: Bar,
meta: { requiresAuth: true }
}
]
}
]
})
routes
配置中的每个路由对象被称为路由记录。路由记录可以是嵌套的,因此,当一个路由匹配成功后,它可能匹配多个路由记录。例如,根据上面的路由配置,/foo/bar
这个URL将会匹配父路由记录以及子路由记录
一个路由匹配到的所有路由记录会暴露为 $route
对象(还有在导航钩子中的 route 对象)的 $route.matched
数组。因此,需要遍历 $route.matched
来检查路由记录中的 meta
字段
下面例子展示在全局导航钩子中检查 meta 字段:
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!auth.loggedIn()) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
} else {
next()
}
} else {
next()
}
})
【基于路由的动态过渡】
可以基于当前路由与目标路由的变化关系,动态设置过渡效果。通过使用路由元信息,在每个路由对象上设置一个index属性保存其索引值
<style> .router-link-active{background:pink;} .left-enter{ transform:translateX(100%); } .left-leave-to{ transform:translateX(-100%); } .left-enter-active,.left-leave-active{ transition:transform .5s; } .right-enter{ transform:translateX(-100%); } .right-leave-to{ transform:translateX(100%); } .right-enter-active,.right-leave-active{ transition:transform .5s; } </style>
<div id="app"> <p> <router-link to="/" exact>index</router-link> <router-link :to="{name:'foo'}">Go to Foo</router-link> <router-link :to="{ name: 'bar' }">Go to Bar</router-link> <transition :name="transitionName"> <router-view></router-view> </transition> </p> </div>
const app = new Vue({
el:'#app',
router,
data () {
return {
'transitionName': 'left'
}
},
watch: {
'$route' (to, from) {
this['transitionName'] = to.meta.index > from.meta.index ? 'right' : 'left';
}
},
})
3.16、编程式导航
除了使用<router-link>
创建a标签来定义导航链接,还可以借助router的实例方法,通过编写代码来实现
【router.push(location)】
想要导航到不同的 URL,则使用 router.push
方法。这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL。
当点击 <router-link>
时,这个方法会在内部调用,所以说,点击 <router-link :to="...">
等同于调用 router.push(...)
声明式 编程式
<router-link :to="..."> router.push(...)
在@click中,用$router表示路由对象,在methods方法中,用this.$router表示路由对象
该方法的参数可以是一个字符串路径,或者一个描述地址的对象。例如:
// 字符串 router.push('home') // 对象 router.push({ path: 'home' }) // 命名的路由 router.push({ name: 'user', params: { userId: 123 }}) // 带查询参数,变成 /register?plan=private router.push({ path: 'register', query: { plan: 'private' }})
【router.replace(location)
】
跟 router.push
很像,唯一的不同就是,它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录
声明式 编程式
<router-link :to="..." replace> router.replace(...)
【router.go(n)
】
这个方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)
// 在浏览器记录中前进一步,等同于 history.forward() router.go(1) // 后退一步记录,等同于 history.back() router.go(-1) // 前进 3 步记录 router.go(3) // 如果 history 记录不够用,就静默失败 router.go(-100) router.go(100)
【操作history】
router.push
、router.replace
和router.go
跟history.pushState、
history.replaceState
和history.go类似
, 实际上它们确实是效仿window.history
API的。vue-router的导航方法(push
、replace
、go
)在各类路由模式(history
、 hash
和abstract
)下表现一致
3.17、导航钩子
vue-router
提供的导航钩子主要用来拦截导航,让它完成跳转或取消。有多种方式可以在路由导航发生时执行钩子:全局的、单个路由独享的或者组件级的
【全局钩子】
可以使用 router.beforeEach
注册一个全局的 before
钩子
const router = new VueRouter({ ... }) router.beforeEach((to, from, next) => { // ... })
当一个导航触发时,全局的 before
钩子按照创建顺序调用。钩子是异步解析执行,此时导航在所有钩子 resolve 完之前一直处于 等待中。
每个钩子方法接收三个参数:
to: Route: 即将要进入的目标路由对象
from: Route: 当前导航正要离开的路由
next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。
下面是next()函数传递不同参数的情况
next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。 next(false): 中断当前的导航。如果浏览器的 URL 改变了(可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。 next('/') 或者 next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。
[注意]确保要调用 next
方法,否则钩子就不会被 resolved。
同样可以注册一个全局的 after
钩子,不过它不像 before
钩子那样,after
钩子没有 next
方法,不能改变导航:
router.afterEach(route => { // ... })
下面是一个实例
const Home = { template: '<div>home</div>' } const Foo = { template: '<div>Foo</div>'} const Bar = { template: '<div>bar</div>' } const Login = { template: '<div>请登录</div>' } const routes = [ { path: '/', name:'home', component: Home,meta:{index:0}}, { path: '/foo', name:'foo', component:Foo,meta:{index:1,login:true}}, { path: '/bar', name:'bar', component:Bar,meta:{index:2}}, { path: '/login', name:'login', component:Login,}, ] const router = new VueRouter({ routes , }) router.beforeEach((to, from, next) => { if(to.meta.login){ next('/login'); } next(); }); router.afterEach((to, from)=>{ document.title = to.name; }) const app = new Vue({ el:'#app', router, })
【单个路由独享】
可以在路由配置上直接定义 beforeEnter
钩子
const router = new VueRouter({ routes: [ { path: '/foo', component: Foo, beforeEnter: (to, from, next) => { // ... } } ] })
这些钩子与全局 before
钩子的方法参数是一样的
【组件内钩子】
可以在路由组件内直接定义以下路由导航钩子
beforeRouteEnter beforeRouteUpdate (2.2 新增) beforeRouteLeave
const Foo = { template: `...`, beforeRouteEnter (to, from, next) { // 在渲染该组件的对应路由被 confirm 前调用,不能获取组件实例 `this`,因为当钩子执行前,组件实例还没被创建 }, beforeRouteUpdate (to, from, next) { // 在当前路由改变,但是该组件被复用时调用。举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转时,由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。可以访问组件实例 `this` }, beforeRouteLeave (to, from, next) { // 导航离开该组件的对应路由时调用,可以访问组件实例 `this` } }
beforeRouteEnter
钩子不能访问this
,因为钩子在导航确认前被调用,因此即将登场的新组件还没被创建
不过,可以通过传一个回调给 next
来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数
beforeRouteEnter (to, from, next) { next(vm => { // 通过 `vm` 访问组件实例 }) }
可以在 beforeRouteLeave
中直接访问 this
。这个 leave
钩子通常用来禁止用户在还未保存修改前突然离开。可以通过 next(false)
来取消导航
3.18、数据获取
有时候,进入某个路由后,需要从服务器获取数据。例如,在渲染用户信息时,需要从服务器获取用户的数据。可以通过两种方式来实现:
1、导航完成之后获取:先完成导航,然后在接下来的组件生命周期钩子中获取数据。在数据获取期间显示『加载中』之类的指示
2、导航完成之前获取:导航完成前,在路由的 enter
钩子中获取数据,在数据获取成功后执行导航。从技术角度讲,两种方式都不错 —— 就看想要的用户体验是哪种
【导航完成后获取】
当使用这种方式时,会马上导航和渲染组件,然后在组件的 created
钩子中获取数据。有机会在数据获取期间展示一个 loading 状态,还可以在不同视图间展示不同的 loading 状态。
假设有一个 Post
组件,需要基于 $route.params.id
获取文章数据:
<template> <div class="post"> <div class="loading" v-if="loading"> Loading... </div> <div v-if="error" class="error"> {{ error }} </div> <div v-if="post" class="content"> <h2>{{ post.title }}</h2> <p>{{ post.body }}</p> </div> </div> </template> export default { data () { return { loading: false, post: null, error: null } }, created () { // 组件创建完后获取数据, // 此时 data 已经被 observed 了 this.fetchData() }, watch: { // 如果路由有变化,会再次执行该方法 '$route': 'fetchData' }, methods: { fetchData () { this.error = this.post = null this.loading = true // replace getPost with your data fetching util / API wrapper getPost(this.$route.params.id, (err, post) => { this.loading = false if (err) { this.error = err.toString() } else { this.post = post } }) } } }
【导航完成前获取数据】
通过这种方式,在导航转入新的路由前获取数据。可以在接下来的组件的 beforeRouteEnter
钩子中获取数据,当数据获取成功后只调用 next
方法
export default {
data () {
return {
post: null,
error: null
}
},
beforeRouteEnter (to, from, next) {
getPost(to.params.id, (err, post) => {
if (err) {
// display some global error message
next(false)
} else {
next(vm => {
vm.post = post
})
}
})
},
// 路由改变前,组件就已经渲染完了
// 逻辑稍稍不同
watch: {
$route () {
this.post = null
getPost(this.$route.params.id, (err, post) => {
if (err) {
this.error = err.toString()
} else {
this.post = post
}
})
}
}
}
在为后面的视图获取数据时,用户会停留在当前的界面,因此建议在数据获取期间,显示一些进度条或者别的指示。如果数据获取失败,同样有必要展示一些全局的错误提醒
3.19、懒加载
当打包构建应用时,JS包会变得非常大,影响页面加载。如果能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了
结合 Vue 的 异步组件 和 Webpack 的代码分割功能,轻松实现路由组件的懒加载。
首先,可以将异步组件定义为返回一个 Promise 的工厂函数(该函数返回的Promise应该 resolve 组件本身)
const Foo = () => Promise.resolve({ /* 组件定义对象 */ })
在 webpack 2中,使用动态 import语法来定义代码分块点(split point):
import('./Foo.vue') // returns a Promise
[注意]如果使用的是 babel,需要添加 syntax-dynamic-import插件,才能使 babel 可以正确地解析语法
结合这两者,这就是如何定义一个能够被 webpack自动代码分割的异步组件
const Foo = () => import('./Foo.vue')
在路由配置中什么都不需要改变,只需要像往常一样使用 Foo
:
const router = new VueRouter({ routes: [ { path: '/foo', component: Foo } ] })
【把组件按组分块】
有时候想把某个路由下的所有组件都打包在同个异步块(chunk)中。只需要使用 命名 chunk,一个特殊的注释语法来提供chunk name(需要webpack > 2.4)
const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue') const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue') const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')
webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中