隨著AJAX動態更新技術的普及,手邊專案有愈來愈多網頁開始實現"無PostBack"的設計風格,透過jQuery $.post(), $.get()與ASP.NET程式溝通,執行查詢、更新作業並取得結果,再動態改變HTML DOM回應使用者。(註: 對ASP.NET開發者來說,UpdatePanel是另一個無痛實現AJAX化的選項,但有些副作用)

由使用者的回饋來看,減少網頁PostBack與網頁重新導向次數,確實大幅提高操作回應速度,提供更好的操作體驗,不過倒有一個常被垢病之處 --- 使用者明明覺得點選操作後瀏覽器切換到下一個畫面,為什麼按覽器的回上頁卻無法回到上個畫面?

細究這個問題的根源,在於程式是透過AJAX方式取得下一畫面內容更新在網頁上,或是動態顯示/隱藏不同的DOM元素,給予使用者"由操作網頁A切換到操作網頁B"的認知,但以瀏覽器的角度,卻始終是同一個網頁;由於使用者覺得網頁已被切換過,當需要回前一個介面進行資料修改或重新選擇時,直覺上便會去點選"回上頁",結果跳過了AJAX切換介面的過程,直接進入這個網頁前的瀏覽網頁,造成認知與執行結果的落差,形成抱怨。

當然,在吃過幾次悶虧(例如: 回上頁造成結果沒儲存之類的,這個倒有解藥),碰了一鼻子灰之後,使用者就會學乖,自動避開"在某某網頁不能按【回上頁】,很可怕,不要問"的地雷。或者,我們也可以恐嚇提示或教育使用者避免在這類介面按回上頁,或是在網頁加上自己寫的"AJAX版回上頁"按鈕實現切換回上一個UI效果做為替代,不過,這些治標做法終究無法完全避免使用者誤觸回上頁所造成的困擾。

用一個實例來展示這個問題:

<!DOCTYPE html>
 
<html>
<head>
    <title>AJAX GoBack</title>
    <script src="Scripts/jquery-1.6.3.js" type="text/javascript"> </script>
    <script>
        $(function () {
            $("#s1").show();
            $("input.nav-btn").click(function () {
                var $btn = $(this);
                //隱藏目前按鈕所在的div
                $btn.parent().hide();
                //由按鈕的data-nav屬性決定要顯示div的id
                $("#" + $btn.data("nav")).show();
            });
        });
    </script>
    <style>
    #main div { width: 300px; height: 200px; display: none; padding: 10px; }
    #s1 { background-color: #ff7777; }
    #s2 { background-color: #77ff77; }
    #s3 { background-color: #7777ff; }
    </style>
</head>
<body>
<div id="main">
    <div id="s1">
    STEP1
    <input type="button" class="nav-btn" value="Next" data-nav="s2" />
    </div>
    <div id="s2">
    STEP2
    <input type="button" class="nav-btn" value="Next" data-nav="s3" />
    </div>
    <div id="s3">
    FINAL
    <input type="button" value="Submit" />
    </div>
</div>
</body>
</html>

線上展示

以上的網頁包含三個<div>,假裝是輸入流程中的三個步驟,STEP1按Next可以跳到STEP2,STEP2按Next可以跳到STEP3。在操作上,讓使用者感覺在不同的網頁介面間切換,但對瀏覽器來說,自始至終都在同一網頁,於是,如圖所以,操作時連回上頁鈕都沒得按。

搞HTML的老骨頭可能記得#書籤的用法(參考[請看"書籤"相關說明]),在網頁MyPage.htm中放入<a name="myBookmark"></a>,當URL指定MyPage.htm#myBookmark,瀏覽器就會開啟該網頁並捲動到<a name="myBookmark"></a>的所在位置。有趣的是,MyPage.htm#a跟MyPage.htm#b可被視為不同網址(這表示在瀏覽歷史中會出現一筆),卻又不會觸發網頁重新載入,這個特性讓AJAX回上頁問題出現一線曙光。

在URI規範中,#符號後方所帶的資訊被定義成Fragment Identifier(#是Hash symbol,#後方的內容也被稱為hash),不只是作為書籤定址,還可實現AJAX的歷史巡覽功能(from Wiki: With the rise of AJAX, some websites use fragment identifiers to emulate the back button behavior of browsers for page changes that do not require a reload, or to emulate subpages.),硬綁綁的規範讓人頭好痛,這裡只研究怎麼應用就好。

以下是一個簡單的hash應用示範(請使用IE8/9/10的標準模式、Firefox、Chrome或Safari執行)

<!DOCTYPE html>
 
<html>
<head>
    <title>Hash Demo</title>
    <script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.6.3.js">    
    </script>
    <script>
        $(function () {
 
            function setHash(x) {
                var n = x;
                //在第n秒將loation.href改為後方加#sn
                //注意: 只變動#以後的參數不會導致網頁重新載入
            setTimeout(function () {
                    location.hash = "#s" + n;
                }, n * 1000);
            }
            //在第0,1,...,7秒為location.url加上#s0-#s7
            for (var i = 0; i < 8; i++) setHash(i);
            $("#btnShowHash").click(function () {
                $("#ulDisp").append("<li>hash=" + location.hash + "</li>");
            });
        });
    </script>
</head>
<body>
<input type="button" id="btnShowHash" value="Show location.hash" />
<ul id="ulDisp"></ul>
</body>
</html>

線上展示

網頁裡有一小段程式,會在第0-7秒時,在URL的最後方加上#s0-#s7,執行完畢後,按瀏覽器回上頁及回下頁可在不同的#sn間切換,按下"Show location.href"則會顯示目前的location.hash,當來回切換上一頁下一頁,下方hash=#sn的<li>顯示並不會被清除,驗證了網頁並未被重新載入,Javascript及DOM的狀態在切換過程中不會遺失。

但很不幸地,IE7(或IE8/9開相容模式)對location.hash特性支援不夠完整,hash改變時並不會在瀏覽歷史中產生記錄,因此需要透過加入隱藏式iframe等特殊手法來克服,自己處理跨瀏覽器議題太辛苦,有個很好用的jQuery外掛BBQ(不是中秋烤肉巴比Q,是Back Button & Query)可以讓我們輕鬆寫出跨瀏覽器版的AJAX回上頁功能。

小小改寫前述STEP1-STEP2-FINAL網頁,主要是在切換網頁時透過$.bbq.pushState()將目前的階段設成location.hash(#step=sn),另外在window.onhashchage事件則加入一小段程式,讀取hash值決定現在是STEP1,2還是FINAL,以及該顯示哪一個<div>。如此,一旦使用者按下回上頁/回下頁,URL中的hash不同就會觸發hashchange事件,程式就能依指定的階段切換不同的<div>。

<!DOCTYPE html>
 
<html>
<head>
    <title>AJAX GoBack</title>
    <script src="Scripts/jquery-1.6.3.js" type="text/javascript"> </script>
    <script src="Scripts/jquery.ba-bbq.js" type="text/javascript"> </script>
    <script>
        $(function () {
            $("#s1").show();
            $("input.nav-btn").click(function () {
                var $btn = $(this);
                $btn.parent().hide();
                var nav = $btn.data("nav");
                $("#" + nav).show();
                //將目前的步驟加註在location.hash
                $.bbq.pushState({ step: nav });
            });
            //hash變化時觸發hashchange事件
            $(window).bind('hashchange', function (e) {
                //由hash取出step參數,決定要顯示哪一個div
                var s = e.getState("step") || "s1";
                if (!$("#" + s + ":visible").length) {
                    $("#main > div").hide();
                    $("#" + s).show();
                }
            });
        });
    </script>
    <style>
    #main div { width: 300px; height: 200px; display: none; padding: 10px; }
    #s1 { background-color: #ff7777; }
    #s2 { background-color: #77ff77; }
    #s3 { background-color: #7777ff; }
    </style>
</head>
<body>
<div id="main">
    <div id="s1">
    STEP1
    <input type="button" class="nav-btn" value="Next" data-nav="s2" />
    </div>
    <div id="s2">
    STEP2
    <input type="button" class="nav-btn" value="Next" data-nav="s3" />
    </div>
    <div id="s3">
    FINAL
    <input type="button" value="Submit" />
    </div>
</div>
</body>
</html>

線上展示

經過這番加工,網頁就能跨瀏覽器支援回上頁功能,我們又向高級AJAX網頁開發再前進一步囉!

2011-09-23更新】另外補充HTML5支援AJAX動態內容瀏覽歷程的新做法


Comments

# by Ammon

用 Hash 已經過時了,看看新的 History API ( http://goo.gl/47dk3 ) 透過 pushState popState replaceState 改變網址並且不會 reload 網頁,Facebook 和 github 都有用這項技術。

# by Jeffrey

to Ammon, 感謝提供HTML5武器新知,稍後會再整理補充。

# by 星寂

我記得ASP.NET Ajax 3.5 SP 1已經有這個問題的解決方式(http://www.dotblogs.com.tw/alonstar/archive/2009/11/19/ajax-history-support.aspx),也是用HASH去控制。JQuery也有UI可以控制,單純的用HASH如果是複雜的網址會不好處理。

# by Jeffrey

to 星寂, 謝謝補充,這再度證明了UpdatePanel真是很優秀的傻瓜式AJAX實作選擇。

Post a comment