|
|
买醉的花卷 · [Day 21] 媽! Keras 和 ...· 1 年前 · |
|
|
绅士的斑马 · CSV无法连接到ArcGIS属性表_arcg ...· 1 年前 · |
|
|
宽容的橡皮擦 · 电脑快速删除文件的快捷键方法-百度经验· 1 年前 · |
|
|
爱跑步的松鼠 · 【MQTT】MQTT简介+安装+使用pyth ...· 2 年前 · |
路由是根据不同的 url 地址展示不同的内容或页面,早期的路由都是后端直接根据 url 来 reload 页面实现的,即后端控制路由。
后来页面越来越复杂,服务器压力越来越大,随着AJAX(异步刷新技术) 的出现,页面实现非 reload 就能刷新数据,让前端也可以控制 url 自行管理,前端路由因此而生。
如果直接使用AJAX加载页面片段是可以实现单页效果的,但这样会破坏浏览器的前进与后退功能,使用Hjax或Pjax技术后即可以实现单页无刷新效果又不会影响前进与后退功能。
<!DOCTYPE html> <html lang="en"> <meta charset="UTF-8"> <title>模拟单页</title> </head> <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"> <meta charset="UTF-8"> <title>这是页面二</title> </head> <div style="background: dodgerblue"> <h2>这是页面二</h2> </div> </body> </html>
运行结果:
单页面应用的实现,就是因为前端路由,前端路由实现主要有下面几种方法:
原理:url 中常会出现 #,一可以表示锚点(如回到顶部按钮的原理),二是路由里的锚点(hash)。Web 服务并不会解析 hash,也就是说 # 后的内容 Web 服务都会自动忽略,但是 JavaScript 是可以通过 window.location.hash 读取到的,读取到路径加以解析之后就可以响应不同路径的逻辑处理。
hashchange 事件(监听 hash 变化触发的事件),当用 window.location 处理哈希的改变时不会重新渲染页面,而是当作新页面加到历史记录中,这样我们跳转页面就可以在 hashchange 事件中注册 ajax 从而改变页面内容。
<!DOCTYPE html> <html lang="en"> <meta charset="UTF-8"> <title>模拟单页 - hash</title> </head> <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>View Code
运行结果:
这里并没有解决前进与后退失效的问题。
URL是统一资源定位符,对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。互联网上的每个文件都有一个唯一的URL,它包含的信息指出文件的位置以及浏览器应该怎么处理它。URI是统一资源标识符,而URL是统一资源定位符,我们把URL理解为是URI的一个子类,而另一种子类是URN。url是统一资源定位符,对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。互联网上的每个文件都有一个唯一的URL,它包含的信息指出文件的位置以及浏览器应该怎么处理它。URL中所有的字符都是ASCII字符集,如果出现非ASCII字符集,比如中文,浏览器会先进行编码再进行传输。
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
拆解如下:
http:// zhangguo:123456@ www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#SomewhereInTheDocument
这里用户名是zhangguo,密码是123456,如果带@符用户必须填写,密码选填,带用户名与密码的情况很少见,很多时候都放到了参数中了。
http:// zhangguo:123456@www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#SomewhereInTheDocument
http://为协议名,标明了请求需要使用的协议,通常使用的是HTTP协议或者安全协议 HTTPS.其他协议还有mailto:用户打开邮箱的客户端,和ftp:用来做文件的转换, file用来获取文件,data获取外部资源等
http:// zhangguo:123456@ www.example.com :80/path/to/myfile.html?key1=value1&key2=value2#SomewhereInTheDocument
www.example.com为域名,标明了需要请求的服务器的地址,www是主机名,example是单位名称,.com是机构类型
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地址+端口号”来区分不同的服务.
如果把服务器比作房子,端口号可以看做是通向不同服务的门,
http:// zhangguo:123456@www.example.com:80 /path/to/myfile.html ?key1=value1&key2=value2#SomewhereInTheDocument
/path/to/myfile.html表示服务器上资源的路径,过去这样的路径标记的是服务器上文件的物理路径,但是现在,路径表示的只是一个抽象地址,并不指代任何物理地址.
http:// zhangguo:123456@www.example.com:80/path/to/myfile.html ?key1=value1&key2=value2 #SomewhereInTheDocument
?key1=value1&key2=value2是请求里提供的额外参数.这些参数是以键值对的形式,通过&符号分隔开来,服务器可以通过这些参数进行相应的个性化处理
http:// zhangguo:123456@www.example.com:80/path/to/myfile.html?key1=value1&key2=value2 #SomewhereInTheDocument
#SomewhereInTheDocument是对资源的部分补充.fragment可以理解为资源内部的书签.用来想服务器指明展示的内容所在的书签的点.例如对于HTML文件来说,浏览器会滚动到特定的或者上次浏览过的位置.对于音频或者视频资源来说,浏览器又会跳转到对应的时间节点。
锚记连接又叫命名锚记,命名锚记像一个迅速定位器一样是一种页面内的超级链接。
我们上面所说的都是绝对路径,但是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>
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 的锚部分(从 # 号开始的部分)
#代表网页中的一个位置。其右面的字符,就是该位置的标识符。比如,
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"> <meta charset="UTF-8"> <title>hash</title> </head> <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> <button>提交,是否带了hash</button> </form> </body> </html>
#是用来指导浏览器动作的,对服务器端完全无用。所以,HTTP请求中不包括#。
比如,访问下面的网址,
http://www.example.com/index.html#print
浏览器实际发出的请求是这样的:
GET /index.html HTTP/1.1
Host: www.example.com
可以看到,只是请求index.html,根本没有"#print"的部分。
在第一个#后面出现的任何字符,都会被浏览器解读为位置标识符。这意味着,这些字符都不会被发送到服务器端。
比如,下面URL的原意是指定一个颜色值:
http://www.example.com/?color=#fffccc
但是,浏览器实际发出的请求是:
GET /?color= HTTP/1.1
Host: www.example.com
可以看到,"#fffccc"被省略了。只有将#转码为%23,浏览器才会将其作为实义字符处理。也就是说,上面的网址应该被写成:
http://example.com/?color=%23fffccc
单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载网页。
http://www.example.com/index.html#location1
http://www.example.com/index.html#location2
浏览器不会重新向服务器请求index.html。
每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用"后退"按钮,就可以回到上一个位置。
这对于ajax应用程序特别有用,可以用不同的#值,表示不同的访问状态,然后向用户给出可以访问某个状态的链接。
值得注意的是,上述规则对IE 6和IE 7不成立,它们不会因为#的改变而增加历史记录。
window.location.hash这个属性可读可写。读取时,可以用来判断网页状态是否改变;写入时,则会在不重载网页的前提下,创造一条访问历史记录。
默认情况下,Google的网络蜘蛛忽视URL的#部分。
但是,Google还规定,如果你希望Ajax生成的内容被浏览引擎读取,那么URL中可以使用"#!",Google会自动将其后面的内容转成查询字符串_escaped_fragment_的值。
比如,Google发现新版twitter的URL如下:
http://twitter.com/#!/username
就会自动抓取另一个URL:
http://twitter.com/?_escaped_fragment_=/username
通过这种机制,Google就可以索引动态的Ajax内容。
这是一个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"> <meta charset="UTF-8"> <title>hashchange</title> </head> <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"> <meta charset="UTF-8"> <title>history 测试</title> </head> <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>
结合前面的内容,我们可以模拟一个简单的单页示例,为了提高性能这里做了缓存,示例:
<!DOCTYPE html> <html lang="en"> <meta charset="UTF-8"> <title>模拟单页 - hash</title> </head> <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>
虽然传统的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 刷新(实际效果如下图的请求内容对比)。
在没有history ap之前,我们经常使用散列值来改变页面内容,特别是那些对页面特别重要的内容。因为没有刷新,所以对于单页面应用,改变其URL是不可能的。此外,当你改变URL的散列值,它对浏览器的历史记录没有任何影响。通过增加location.hash,并用onhashchange来达到目的。
现在对于HTML 5的History API来说,这些都是可以轻易实现的,但是由于单页面应用没必要使用散列值,它可能需要额外的开发脚本。它也允许我们用一种对SEO友好的方式建立新应用。
history.length属性保存着历史记录的URL数量。初始时,该值为1。由于IE10+浏览器在初始时返回2,存在兼容性问题,所以该值并不常用。
history.go(n),进前或后退n步
history.back(),后退
history.forward(),前进
如果移动的位置超出了访问历史的边界,以上三个方法并不报错,而是静默失败
使用历史记录时,页面通常从浏览器缓存之中加载,而不是重新要求服务器发送新的网页,不触发onload事件。
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上有区别
如果设置了一个跨域网址,则会报错。这样设计的目的是,防止恶意代码让用户以为他们是在另一个网站上
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
history.state属性返回当前页面的state对象
history.pushState({page: 1}, 'title 1', '?page=1');
history.state// { page: 1 }
每当同一个文档的浏览历史(即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"> <meta charset="UTF-8"> <title>模拟单页 - hash</title> </head> <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>
默认情况下,浏览器会在当前会话(session)缓存页面,当用户点击“前进”或“后退”按钮时,浏览器就会从缓存中加载页面
浏览器有一个特性叫“往返缓存”(back-forward cache或bfcache),可以在用户使用浏览器的“后退”和“前进”按钮时加快页面的转换速度。这个缓存中不仅保存着页面数据,还保存了DOM和javascript的状态;实际上是将整个页面都保存在了内存里。如果页面位于bfcache中,那么再次打开该页面时就不会触发load事件,IE10-浏览器不支持
pageshow事件在页面加载时触发,包括第一次加载和从缓存加载两种情况。如果要指定页面每次加载(不管是不是从浏览器缓存)时都运行的代码,可以放在这个事件的监听函数
第一次加载时,它的触发顺序排在load事件后面。从缓存加载时,load事件不会触发,因为网页在缓存中的样子通常是load事件的监听函数运行后的样子,所以不必重复执行。同理,如果是从缓存中加载页面,网页内初始化的JavaScript脚本(比如DOMContentLoaded事件的监听函数)也不会执行
[注意]虽然这个事件的目标是document,但必须将其事件处理程序添加到window
pageshow事件有一个persisted属性,返回一个布尔值。页面第一次加载时或没有从缓存加载时,这个属性是false;当页面从缓存加载时,这个属性是true
[注意]上面的例子使用了私有作用域,以防止变量showCount进入全局作用域。如果单击了浏览器的“刷新”按钮,那么showCount的值就会被重置为0,因为页面已经完全重新加载了
与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/barurl是统一资源定位符,对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。互联网上的每个文件都有一个唯一的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参数配置
keydefaultdescriptiontimeoutajax请求如果超时将触发强制刷新 使用 [pushState][] 在浏览器中添加导航记录replacefalse 是否使用replace方式改变URLmaxCacheLength返回的HTML片段字符串最大缓存数version当前pjax版本scrollTo当页面导航切换时滚动到的位置. 如果想页面切换不做滚动重置处理,请传入false."GET"使用ajax的模板请求方法,参考 $.ajaxdataType"html"模板请求时的type,参考 $.ajaxcontainer内容替换的CSS选择器 link.href 用于ajax请求的url,可以是字符串或者返回字符串的函数targeteventually therelatedTargetvalue for pjax eventsfragment从服务端返回的HTML字符串中子内容所在的CSS选择器,用于当服务端返回了整个HTML文档,但要求pjax局部刷新时使用。可以使用下面的方式动态设置options:
$.pjax.defaults.timeout = 12001. 点击链接后触发的一系列事件, 除了
事件名支持取消参数说明 pjax:click options 点击按钮时触发。可调用pjax:click和pjax:clicked的事件源是点击的按钮,其他事件的事件源都是要替换内容的容器。可以在pjax:start事件触发时开始过度动画,在pjax:end事件触发时结束过度动画。e.preventDefault();取消pjax pjax:beforeSend xhr, optionsajax执行beforeSend函数时触发,可在回调函数中设置额外的请求头参数。可调用e.preventDefault();取消pjaxpjax:start xhr, optionspjax开始(与服务器连接建立后触发) pjax:send xhr, optionspjax:start之后触发 pjax:clicked optionsajax请求开始后触发 pjax:beforeReplace contents, optionsajax请求成功,内容替换渲染前触发 pjax:success data, status, xhr, options 内容替换成功后触发 pjax:timeout xhr, options ajax请求超时后触发。可调用e.preventDefault();继续等待ajax请求结束 pjax:error xhr, textStatus, error, optionsajax请求失败后触发。默认失败后会跳转url,如要阻止跳转可调用e.preventDefault();pjax:complete xhr, textStatus, optionsajax请求结束后触发,不管成功还是失败 pjax:end xhr, optionspjax所有事件结束后触发<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');
服务端也比较简单,监听HTTP的header中有
X-PJAX
的ajax请求,如果有则返回HTML片段,而不是整个HTML。
<!DOCTYPE html> <html lang="en"> <meta charset="UTF-8"> <title>Pjax</title> </head> <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"> <meta charset="UTF-8"> <title>Pjax</title> </head> <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>
运行结果:
这部分用于更细粒度的控制。
// 确定能使用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生命周期简单的说:
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.jsUnpkg.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 build3.3、第一个路由示例
3.3.1、网页版
用 Vue.js + Vue Router 创建单页应用,是非常简单的。使用 Vue.js ,我们已经可以通过组合组件来组成应用程序,当你要把 Vue Router 添加进来,我们需要做的是,将组件 (components) 映射到路由 (routes),然后告诉 Vue Router 在哪里渲染它们。下面是个基本例子:
<!DOCTYPE html> <html lang="en"> <meta charset="UTF-8"> <title>Hello Router App</title> </head> <div id="app"> <h1>Hello Router App!</h1> <!-- 使用 router-link 组件来导航. --> <!-- 通过传入 `to` 属性指定链接. --> <!-- <router-link> 默认会被渲染成一个 `<a>` 标签 --> < router-link to="/foo">Go to Foo</router-link> <router-link to="/bar">Go to Bar</router-link> <a href="#/foo">foo</a> | <a href="#/bar">bar</a> <!-- 路由出口 --> <!-- 路由匹配到的组件将渲染在这里 --> <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> <h2>Default</h2> <p>{{msg}}</p> </div> </template> <script> export default { data() { return { msg: "这是默认组件" </script> <style scoped> color:crimson; </style>Home.vue
<template> <h2>Home</h2> <p>{{msg}}</p> </div> </template> <script> export default { data() { return { msg: "我是Home 组件" </script> <style scoped> color: dodgerblue; </style>About.Vue
<template> <h2>About</h2> <p>{{msg}}</p> </div> </template> <script> export default { data() { return { msg: "我是About 组件" </script > <style scoped> color:springgreen; </style>在 App.vue中 定义<router-link > 和 </router-view>
<template> <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> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <title>Hello Router</title> </head> <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重定向到/bconst 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' /*命名,重定向*/ View Code<template> <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> color: #777; a:hover{ color:orangered; </style>View Code3.7、根路径
设置根路径,需要将path设置为'/'
<router-link to="/">index</router-link> <router-link to="/foo">Go to Foo</router-link> <router-link to="/bar">Go to Bar</router-link> const routes = [ { path: '/', component: Home }, { path: '/foo', component: Foo }, { path: '/bar', component: Bar },默认运行效果:
点击Go to Bar
但是,由于默认使用的是全包含匹配,即'/foo'、'/bar'也可以匹配到'/',如果需要精确匹配,仅仅匹配'/',则需要在router-link中设置exact属性
<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> const routes = [ { path: '/', component: Home }, { path: '/foo', component: Foo }, { path: '/bar', component: Bar },运行结果:
3.8、嵌套路由
实际项目中的应用界面,通常由多层嵌套的组件组合而成。同样地,URL中各段动态路径也按某种结构对应嵌套的各层组件
借助
vue-router,使用嵌套路由配置,就可以很简单地表达这种关系<div id="app"> <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> <router-view></router-view> </div>const Home = { template: '<div>home</div>' } const Foo = { template: ` <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> <router-view></router-view> 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: ` <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> <router-view></router-view>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> <h2>这是Foo1</h2> </div> </template> <script> export default { name: "Foo1" </script> <style scoped> color: purple; </style>View CodeFoo2.Vue
<template> <h2>这是Foo2</h2> </div> </template> <script> export default { name: "Foo2" </script> <style scoped> color: orange; </style>View CodeFoo3.Vue
<template> <h2>这是Foo3</h2> </div> </template> <script> export default { name: "Foo3" </script> <style scoped> color:springgreen; </style>View CodeFoo.Vue
<template> <h2>Foo</h2 > <p>{{msg}}</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> <router-view></router-view> </div> </div> </div> </template> <script> export default { data() { return { msg: "我是Foo组件" </script> <style scoped> color: dodgerblue; </style>View Codehello.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运行结果:
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"> <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> <router-view></router-view> </div><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> <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
<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>结果如下所示
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"> <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> <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> <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> 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"> <router-link to="/user/foo">/user/foo</router-link> <router-link to="/user/bar">/user/bar</router-link> <router-view></router-view> <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>`, 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>`, 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>`, 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> <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> <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> </div>【单个路由过渡】
上面的用法会给所有路由设置一样的过渡效果,如果想让每个路由组件有各自的过渡效果,可以在各路由组件内使用
<transition>并设置不同的 nameconst 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()watch: { '$route' (to, from) { this['transitionName'] = to.meta.index > from.meta.index ? 'right' : 'left';【基于路由的动态过渡】
可以基于当前路由与目标路由的变化关系,动态设置过渡效果。通过使用路由元信息,在每个路由对象上设置一个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><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> </div>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.historyAPI的。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 新增) beforeRouteLeaveconst 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 会将任何一个异步模块与相同的块名称组合到相同的异步块中
|
|
宽容的橡皮擦 · 电脑快速删除文件的快捷键方法-百度经验 1 年前 |