前端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参数配置

keydefaultdescription
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 会将任何一个异步模块与相同的块名称组合到相同的异步块中

posted @ 2019-01-07 10:01  张果  阅读(1720)  评论(0编辑  收藏  举报
AmazingCounters.com