路由是根據不同的 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 會將任何一個異步模塊與相同的塊名稱組合到相同的異步塊中