理解瀏覽器歷史記錄(2)-hashchange、pushState

本文也是一篇基礎文章。繼上文以後,本打算去研究pushState,偶然在一些信息中發現了錨點變化對瀏覽器的歷史記錄也會影響,同時錨點的變化跟pushState也有一些關聯。因此就花了點時間,把這兩個東西儘可能都琢磨清楚。本文記錄相關的一些要點及研究過程。css

1. hashchange

這個部分的內容也已經補充到上文的最後了,這裏只是細化一下。總的結論是:若是一個網頁只是錨點,也就是location.hash發生變化,也會致使歷史記錄棧的變化;且變化相關的全部特性,都與上文描述的整個頁面變化的特性相同。常見的改變網頁錨點的方式有:html

1)直接更改瀏覽器地址,在最後面增長或改變#hash;
2)經過改變location.href或location.hash的值;
3)經過觸發點擊帶錨點的連接;
4)瀏覽器前進後退可能致使hash的變化,前提是兩個網頁地址中的hash值不一樣。前端

假如咱們還用上文的demo來測試,並按照如下步驟操做的話:
打開新選項卡;輸入demo1.html;在地址欄後面加#1;將地址欄#1改爲#2;將地址欄#2改爲#3;將地址欄#3改爲#1。
那麼歷史記錄棧的存儲狀態就應該相似下面這個形式:jquery

image

因爲錨點變化也會在歷史記錄棧添加新的記錄,因此history.length也會在錨點變化以後改變。每當錨點發生變化的時候,主流瀏覽器還會觸發window對象的onhashchange事件,在這個事件回調裏面,咱們經過事件對象和location可以拿到頗有用三個參數:git

window.onhashchange = function(event) {
    console.log(event.oldURL);
    console.log(event.newURL);
    console.log(location.hash);
};

event.oldURL返回錨點變化前的完整瀏覽器地址;
event.newURL返回錨點變化後的完整瀏覽器地址;
location.hash返回錨點變化後頁面地址中的錨點值。github

藉助於這三個信息,能夠在hashchange回調內加一些控制器的邏輯,來實現單頁程序開發裏面關鍵的路由功能。現簡單實現舉例以下:ajax

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <link rel="stylesheet" href="./css/quick_layout.css"/>
    <script src="./js/jquery.js"></script>
    <script src="./js/demo.js"></script>
    <style type="text/css">
        ul {
            list-style: none;
        }

        * {
            padding: 0;
            margin: 0;
        }

        .menu {
            width: 320px;
            margin: 10px auto;
            text-align: center;
        }

        .menu li,
        .menu a {
            float: left;
            width: 100px;
        }

        .menu > .active > a {
            font-weight: bold;
        }

        .menu > li + li {
            margin-left: 10px;
        }
    </style>
</head>
<body>
<div id="container" class="container"></div>
<script>
    //容器
    var Container = {
        $element: $('#container'),
        actions: {}
    };

    //action實例配置定義
    var Actions = {
        'index': {
            destroy: function () {
                this.$content.remove();
            },
            doAction: function () {
                var $content = this.$content = $('<div class="content">這是首頁的內容</div>');
                $content.appendTo(Container.$element);
            }
        },
        'list': {
            destroy: function () {
                this.$content.remove();
            },
            doAction: function () {
                var $content = this.$content = $('<div class="content">這是列表頁的內容</div>');
                $content.appendTo(Container.$element);
            }
        },
        'about': {
            destroy: function () {
                this.$content.remove();
            },
            doAction: function () {
                var $content = this.$content = $('<div class="content">這是關於頁的內容</div>');
                $content.appendTo(Container.$element);
            }
        }
    };

    //公共方法,渲染菜單
    var getMenu = function (actionName) {
        return ['<ul class="menu fix">',
            '        <li class="' + (actionName == 'index' ? 'active' : '') + '"><a href="#index">首頁</a></li>',
            '        <li class="' + (actionName == 'list' ? 'active' : '') + '"><a href="#list">列表頁</a></li>',
            '        <li class="' + (actionName == 'about' ? 'active' : '') + '"><a href="#about">關於頁</a></li>',
            '    </ul>'].join("");
    };

    function hashchange(event) {
        var actionName = (location.hash || '#index').substring(1);

        //重複
        if (Container._current && Container._current.actionName == actionName) {
            return;
        }

        //未定義
        if (!Actions[actionName]) {
            return;
        }

        //已定義的action
        var action = Container.actions[actionName];

        //銷燬以前的action
        Container._current && Container._current.destroy();

        if (!action) {
            //未定義則當即建立
            action = (function () {
                //action實例
                var ret = $.extend(true, {
                    destory: $.noop,
                    doAction: $.noop
                }, Actions[actionName]);

                //添加actionName屬性
                ret.actionName = actionName;

                //代理destroy方法,封裝公共邏輯
                ret.destroy = (function () {
                    var _destroy = ret.destroy;

                    return function () {
                        //移除菜單
                        ret.$menu.remove();

                        //調用Actions中定義的destroy方法
                        _destroy.apply(ret, arguments);
                    };
                })();

                //代理doAction方法,封裝公共邏輯
                ret.doAction = (function () {
                    var _doAction = ret.doAction;
                    return function () {
                        //添加菜單
                        var $menu = ret.$menu = $(getMenu(ret.actionName));
                        $menu.appendTo(Container.$element);

                        //調用Actions中定義的doAction方法
                        _doAction.apply(ret, arguments);
                    }
                })();

                return ret;
            })();
        }

        Container._current = action;
        action.doAction();
    }

    //初始化調用
    hashchange();
    //用hashchange當頁面切換的控制器
    window.onhashchange = hashchange;

</script>
</body>
</html>

本代碼demo可經過如下地址訪問測試:http://liuyunzhuge.github.io/blog/pushState/demo1.html。這個demo中,瀏覽器前進後退,頁面刷新,連接跳轉,都能保證內容正確顯示。固然這只是一個極爲簡單的舉例,真正的SPA的路由功能遠比此複雜,下一步我會花時間研究一個較爲流行的路由實現,到時再寫文來總結單頁路由的實現思路。chrome

window.onhashchange的mdn參考:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/onhashchange後端

以上是我瞭解到hashchange的絕大部分用得着的內容,下面要介紹的pushState,還會有一點跟它相關的東西。在SPA的路由實現中,hashchange與pushState是搭配在一塊兒使用的,因此在真正瞭解路由實現前,把這2個東西的基礎知識瞭解透徹也是很是有必要的。api

2 . pushState

有了以前對歷史記錄棧的認識,再來了解pushState就會比較容易。pushState相關的內容包含三個東西:2個api和一個事件。2個api分別是history.pushState和history.replaceState,1個事件是指window.onpopstate事件。pushState提供給咱們的是一種在不改變網頁內容的前提下,操做瀏覽器歷史記錄的能力。

下面詳細看看這2個api和1個事件的內容:

1)history.pushState(stateObj,title,url)

這個方法用來在瀏覽器歷史記錄棧中當前指針後面壓入一條新的條目,而後將當前指針移到這條最新的條目;若是在壓入新條目的時候,當前指針的後面還有舊的條目,在壓入新的以後也會被廢棄掉。總體特性其實跟上一篇博客介紹的,在同一個窗口打開另一個頁面對歷史記錄棧的做用徹底類似,只不過history.pushState僅僅是添加新的條目,而且激活它,而後改變瀏覽器的地址,可是不會改變網頁內容,它也不會去驗證這個新條目對應的網頁是否存在。

這個api有三個參數,第二個參數目前瀏覽器都是忽略它的,在使用的時候通常傳入空字符串便可;第三個參數對應的是新條目的地址,若是沒有,默認就是當前文檔的地址;第一個參數是一個object對象,它會與新條目綁定在一塊兒,能夠用來存儲一些簡單的數據,不過不能存太多,firefox對它的限制是640K,這個對象能夠經過onpopstate事件對象的state屬性來訪問。

爲了驗證前面這部分的理論,能夠經過這個demo:http://liuyunzhuge.github.io/blog/pushState/demo2.html,按如下步驟作一些操做測試:
打開新選項卡;輸入該demo地址;點擊demo3的連接;點擊demo4的連接;點擊demo4裏的返回;點擊demo3裏的返回;點擊pushState(‘foo’)的按鈕;點擊pushState(‘bar')的按鈕。

瀏覽器歷史記錄棧的變化過程應該是下面這個狀態:
image

2)history.replaceState(stateObj,title,url)

這個api和history.pushState的用法徹底一致,只不過它不會在歷史記錄棧中增長新的條目,只會影響當前條目,好比若是傳遞了stateObj,就會更新當前條目關聯的狀態對象;若是傳遞了url,就會替換當前條目的頁面地址和更改瀏覽器地址欄的地址。有一種很是常見的場景,若是利用replaceState,能夠優化它的實現方式。

網頁中搜索列表是比較常見的功能:
image
有2種常見的方式來實現這樣的功能:
一是將查詢條件區封裝好,列表展現區封裝好,當查詢條件改變的時候,利用ajax,觸發列表的查詢;可是這種方式有個很差的體驗問題就是,查詢條件更改後,若是刷新頁面,查詢條件不能恢復刷新前的狀態;因此就有了第二種方式;
二是在查詢條件更改的時候,不用ajax更換列表,而是更新url參數,從新刷新頁面,而後在後端或在前端將查詢條件的狀態根據url裏面的參數初始化好再展現。

目前電商都是第二種方式多,一來比較簡單,二來兼容性也好。若是不考慮兼容IE9之前的瀏覽器,利用replaceState能夠優化第一種作法:就是在查詢條件更改的時候,除了用ajax查詢數據,同時用replaceState更新頁面的url,把條件封裝到url參數中;當用戶刷新頁面時,根據url裏面的條件參數作查詢條件的初始化,這一步跟第二個方案的作法一致。

history.pushState和history.replaceState還有一個共同的特色就是都不會觸發hashchange,你能夠下面這個demo來測試:http://liuyunzhuge.github.io/blog/pushState/demo5.html,以新選項卡打開這個demo,無論先點擊什麼按鈕,頁面上都不會看到有任何的打印信息,儘管我在代碼中是有添加window.onhashchange回調的:
image
可是當我直接在地址欄後面添加一個#3的時候,頁面上就會看到onhashchange回調打印的信息了:
image

3) window.onpopstate事件

這個事件觸發的時機比較有特色:
1、history.pushState和history.replaceState都不會觸發這個事件
2、僅在瀏覽器前進後退操做、history.go/back/forward調用、hashchange的時候觸發
你能夠下面這個demo來驗證:http://liuyunzhuge.github.io/blog/pushState/demo6.html,這個demo裏我添加了onpopstate回調,嘗試打印一些信息,若是按如下幾組步驟測試:
a. 打開新選項卡,輸入demo地址,點擊pushState的按鈕,再點擊瀏覽器的後退按鈕,再點擊瀏覽器前進按鈕;
b. 打開新選項卡,輸入demo地址,點擊pushState的按鈕,點擊replaceState的按鈕,再點擊瀏覽器的後退按鈕,再點擊瀏覽器前進按鈕;
c. 打開新選項卡,輸入demo地址,點擊#yes的連接,再點擊瀏覽器的後退按鈕,再點擊瀏覽器前進按鈕;
d. 打開新選項卡,輸入demo地址,點擊location.hash = '#no'的連接,再點擊瀏覽器的後退按鈕,再點擊瀏覽器前進按鈕。
最後會獲得的結果以下:
a. 點擊pushState的按鈕不會有打印信息,點擊後退按鈕後會有打印信息,再點擊前進按鈕會有打印信息;
b. 點擊pushState&replaceState的按鈕不會有打印信息,點擊後退按鈕後會有打印信息,再點擊前進按鈕會有打印信息;
c&d. 點擊連接,點擊後退按鈕,點擊前進按鈕都會有打印信息。
雖然測試的場景很少,可是也夠咱們去判斷前面那兩點結論的正確性了。

比較有意思的是,history.pushState會增長曆史記錄的條目,可是不會觸發hashchange和popstate;hashchange也能夠增長曆史記錄的條目,可是它卻能夠觸發popstate。[疑惑]

前面介紹說到pushState和replaceState的第一個參數stateObj,會與第三個參數對應的歷史條目綁定在一塊,當popstate事件觸發的時候,意味着有新的歷史記錄條目被激活,在popstate的事件對象裏面,有一個state屬性,會返回這個激活條目關聯的stateObj對象的拷貝。一個歷史記錄條目只有當它是被pushState建立的,或者用replaceState改過的,纔可能有關聯的stateObj對象,因此當某些非這2種條件的歷史記錄條目被激活的時候,可能拿到的stateObj就是null,正如你在demo6裏面看到的打印信息顯示的那樣。

stateObj是會被持久化的硬盤上進行存儲的,至少firefox是這麼說的,我猜只要歷史記錄不銷燬,它關聯的stateObj就會一直存在。因此假如某一個網頁在用戶最後一次操做後,有關聯某個stateObj,那麼當用戶再次打開這個網頁的時候,它的stateObj也是能夠被訪問的。若是要直接訪問當前網頁對應條目的stateObj,能夠經過history.state屬性來訪問。

firfox,chrome在頁面首次打開時都不會觸發popstate事件,可是safari會。。。

popstate事件做用範圍僅在於一個document裏面,因爲pushState和hashchange都不會改變網頁的內容也就是document,因此這樣的網頁裏面纔能有效使用popstate。假如咱們輸入一個網頁,而且在它裏面添加了popstate回調;而後經過連接跳轉的方式轉到另一個網頁;再點擊後退按鈕回到第一個網頁。這樣的狀況,第一個網頁裏面的popstate回調,除了有可能由於頁面初始化被觸發外,瀏覽器的後退前進是不會觸發它的,由於這種方式改變了窗口的document。

以上就是pushState的相關內容。如今主流的SPA路由主要是靠pushState,它比hashchange的優點,我認爲最大的一點就是url的友好性,由於它比hashchange看起來更像是常規的跳轉操做,但是體驗上又跟hashchange同樣,不會給用戶形成瀏覽器發生了刷新的感受;並且從url的規劃層面來講,pushState的url跟原來的url形式都是根據具體場景而定的,hashchange可能就得用同一個url加不一樣的hash的形式了,這種形式對於系統設計跟seo來講也是不合理的。缺點就是pushState的兼容性沒有hashchange那麼靠前。要是在移動端,這個天然就不成問題了。

pushState參考資料:

https://developer.mozilla.org/zh-CN/docs/DOM/Manipulating_the_browser_history

https://developer.mozilla.org/zh-CN/docs/Web/API/Window/onpopstate

相關文章
相關標籤/搜索