前端MVC Vue2學習總結(八)——前端路由


路由是根據不同的 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>
View Code

運行結果:

這里並沒有解決前進與后退失效的問題。

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

官網:https://pjax.herokuapp.com

pjax主要做兩方面的事兒:

  1. 用戶點擊鏈接發送ajax請求,服務器得到請求返回需要填充的HTML片段,客戶端得到HTML片段然后插入更新區域
  2. 頁面填充完畢后,使用pushState更新當前的URL

這個過程能實現頁面局部刷新,比傳統的頁面切換刷新的體驗好一些,因為:

  1. 只下載需要的HTML頁面片段,沒有JS、CSS解析
  2. 如果服務端配置了正確的pjax請求,則只返回要更新的HTML片段,客戶端只更新必要的內容,避免了頁面重新渲染的過程。

優點:

減輕服務端壓力

按需請求,每次只需加載頁面的部分內容,而不用重復加載一些公共的資源文件和不變的頁面結構,大大減小了數據請求量,以減輕對服務器的帶寬和性能壓力,還大大提升了頁面的加載速度。

優化頁面跳轉體驗

常規頁面跳轉需要重新加載畫面上的內容,會有明顯的閃爍,而且往往和跳轉前的頁面沒有連貫性,用戶體驗不是很好。如果再遇上頁面比較龐大、網速又不是很好的情況,用戶體驗就更加雪上加霜了。使用pjax后,由於只刷新部分頁面,切換效果更加流暢,而且可以定制過度動畫,在等待頁面加載的時候體驗就比較舒服了。

缺點:

不支持一些低版本的瀏覽器(如IE系列)

pjax使用了pushState來改變地址欄的url,這是html5中history的新特性,在某些舊版瀏覽器中可能不支持。不過pjax會進行判斷,功能不適用的時候會執行默認的頁面跳轉操作。

使服務端處理變得復雜

要做到普通請求返回完整頁面,而pjax請求只返回部分頁面,服務端就需要做一些特殊處理,當然這對於設計良好的后端框架來說,添加一些統一處理還是比較容易的,自然也沒太大問題。另外,即使后台不做處理,設置pjax的fragment參數來達到同樣的效果。

綜合來看,pajx的優點很強勢,缺點也幾乎可以忽略,還是非常值得推薦的,尤其是類似博客這種大部分情況下只有主體內容變化的網站。關鍵它使用簡單、學習成本小,即時全站只有極個別頁面能用得到,嘗試下沒什么損失。pjax的github主頁介紹的已經很詳細了,想了解更多可以看下源碼。

2.2.2、使用方法

1. 客戶端

客戶端設置分兩步:

  1. 下載插件,包括jquery1.8+,或者npm安裝。https://github.com/defunkt/jquery-pjax
  2. 初始化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:clickpjax: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的過程。如果能獲取到clickevent事件時,建議使用$.pjax-click(event)替換。

function applyFilters() {
  var url = urlForFilters()
  $.pjax({url: url, container: '#pjax-container'})
}

2.2.4、pjax生命周期

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' /*命名,重定向*/
    }
  ]
})
View Code

鏈接:

<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>
View Code

結果:

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>
View Code

Foo2.Vue

<template>
  <div>
    <h2>這是Foo2</h2>
  </div>
</template>

<script>
  export default {
    name: "Foo2"
  }
</script>

<style scoped>
  h2 {
    color: orange;
  }
</style>
View Code

Foo3.Vue

<template>
  <div>
    <h2>這是Foo3</h2>
  </div>
</template>

<script>
  export default {
    name: "Foo3"
  }
</script>

<style scoped>
  h2 {
    color:springgreen;
  }
</style>
View Code

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>
View Code

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.pushrouter.replacerouter.gohistory.pushState、history.replaceStatehistory.go類似, 實際上它們確實是效仿window.historyAPI的。vue-router的導航方法(pushreplacego)在各類路由模式(historyhashabstract)下表現一致

 

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM