Ajax區域性頁面重新整理和History API結合的陷阱

發表於2016-08-13

ajax在現代網站已經得到非常普遍地應用,主要的好處大家都知道(非同步載入資料,不用重新整理整個瀏覽器,更小的資料傳輸尺寸)。對於那些老網站或者老專案來說全盤改造成ajax並不現實,於是就有了“區域性頁面重新整理”這個解決方案。如果不知道“區域性頁面重新整理”是何物請看這裡這裡這裡

在我們的專案裡,將原來的iframe或者frame統統替換成了時髦的div,然後修改了頁面上所有發起請求的地方,把響應內容jQuery.loaddiv裡。

於是乎原來老舊的網站變成了一個時髦的基於ajax的網站,每個頁面傳輸的資料量變小了,再也不用解決令人頭疼的:

  1. 為了消除滾動條讓iframe自適應大小
  2. 如何訪問parent window變數的問題(還有如何訪問parent的parent的parent… window變數的問題)
  3. 如何訪問child iframe裡的變數[]的問題了(還有如何訪問child的child的child… iframe裡的變數的問題)

因為大家永遠都在同一個window裡,而且div本身就會根據內容自動撐大。但是等等!瀏覽器怎麼不能後退了?

我們的那個專案是一個滿大街可見的XX管理資訊系統,這種系統最常見的佈局就是左側一個樹形選單區域,右側是一個功能區域,功能區域裡有一個查詢條件區域(裡面有個查詢按鈕),還有一個空白的區域用來顯示查詢結果,同時是使用者運算元據的地方(比如form表單)。

在iframe時代,上面講到的4個區域都是一個iframe,這也就意味著我們可以有很變態的後退能力。
當然了一般來說使用者最常用的就是對操作區域做後退動作,比如查詢一下,選擇一條記錄點選修改,看到form表單,修改一下,在點選儲存前後悔了,點選瀏覽器的後退,回到查詢結果頁面。

但是在引入了ajax後無法後退了,因為ajax請求不會記錄到瀏覽器歷史裡,歷史都沒有了自然就無法後退了。

好在Html5的History API能夠幫助我們解決問題。我們可以人為的使用history.pushState來人造歷史資訊,並且通過監聽popstate事件來知道使用者點選了瀏覽器後退或前進按鈕,然後將頁面元素還原到歷史上的某個狀態。關於Html5 History API的相關資訊可以看這裡

但是事情遠不止這麼簡單,下面是我們遇到的一些坑:

陷阱1:重複執行js指令碼

一切看上去都OK,直到…我們發現區域性頁面重新整理所獲得的結果裡包含了操作dom元素的js。

當遇到這種情況時會發生很奇妙的現象,history state.content是已經載入完畢+js執行後的結果,當我們重新還原的時候,我們會把這個結果載入出來,並且又會執行一遍js。如果這個js是一個新增dom的動作那麼在後退的時候你會看到這個重複的dom元素。

我們想過跟蹤哪些dom元素是被js修改過的來避免這個問題,但是…這是不現實的。

陷阱2:無法還原到最初狀態

前面的方案因為load的內容裡可能有js指令碼所以有嚴重缺陷,於是我們換了個思路,history裡儲存responseText,而不是已經load好後的東西。

但是仍然遇到了這麼一個問題,如果container(重新整理目標區域,某個div)原來是有內容的,而這個內容不是通過ajax區域性頁面重新整理而來,而是使用者一進入這個頁面就已經有的,比如使用伺服器端的模板引擎生成的頁面,那麼在它載入完html片段後就無法回退了。因為它的內容一開始就不在history裡(事實上瀏覽器自己產生的history是沒有state的),這樣就形成了退無可退的局面。

如果你想,我們只要儲存這個container原來的內容不就行了,當後退的時候我們直接恢復它原來的內容,但是請看陷阱1

不過當發生退無可退的情況時,我們認為已經退回到了第一次進入頁面的狀態,這個時候我們重新整理整個頁面就行了。

陷阱3:多個並列的container

陷阱2的解決方案實際上是基於container之間是屬於巢狀關係或者就一個container的情況的。如果是這種情況就不行了:

有A和B兩個container,點選某個按鈕重新整理了A的內容(產生歷史),然後在點選某個按鈕重新整理的B的按鈕(產生歷史),按照使用者的預想情況,第一次後退還原B原來的內容,第二次後退還原A原來的內容。但實際上,第一次後退無法還原B的內容(陷阱2),第二次後退頁面重新整理了(一切恢復最初的樣子)。

如果B是巢狀在A裡的就無所謂了,第一次後退的時候獲得的是A的state,根據A的state還原A的內容的時候順便把B也還原了,第二次後退頁面重新整理,把A也還原了。

而且根據陷阱1所講,我們也不能在history裡儲存A或者B裡原來的內容。

解決辦法:對於這種操作就不要記錄歷史了。

陷阱4:看到過時頁面

我們在History state裡存的是當時load時的responseText,當我們後退的時候看到的是過時的頁面,比如我們原先查詢結果裡看到有A記錄,然後我們跳轉到其他頁面裡,然後再後退到查詢結果頁面看到A記錄還在,但是這個A記錄很可能只是一個幽靈,在資料庫裡早就已經不存在了。如果我們這個時候再對A記錄操作就有出現錯誤。

解決辦法是我們在history state裡儲存url已經相關的引數,當popstate的時候重新發起請求就行了,這樣一來的話也減少了history儲存state所需要的空間。

陷阱5:redirect

即使我們在history state儲存了url你就以為沒事了?too simple, too naive!如果我們對這個url發起的請求被伺服器redirect到另一個url,那麼在history state裡儲存這個url就不對了。

如果我們這個url是用來刪除某條記錄的,伺服器收到請求在資料庫裡刪除了這條記錄,然後redirect到了首頁url,那麼這個時候你在history裡應該存那個url呢?顯然是首頁的url,因為如果你存了刪除url,那麼在後退的時候,我們會重新發起這個url,想想這多嚇人。

解決辦法其實不太簡單,因為ajax是否被redirect你是不知道的,用jQuery封裝的jqXHR物件也沒法知道這個。

也許鏈WHATWG的XmlHttpRequest.responseURL可以救你,但是瀏覽器相容性不好。

我的做法在伺服器sendRedirect之前在requestUrlqueryString裡新增一個flag,用一個專門的servlet filter判斷過來的請求是否有這個flag,如果有那麼就將本次請求的url(也就是redirect到的url)放到response的一個特定的header裡。然後就可以用jqXHR.getResponseHeader('some-header')來獲得這個url,把這個url放到history state裡。

陷阱6:無法精確還原dom物件的狀態

不論是儲存responseText還是儲存url請求引數,都無法在瀏覽器後退的時候精確還原dom物件的狀態,比如我在IE6裡有個這樣的特性,你在某個頁面勾選了某個checkbox,然後跳轉到一個新的頁面然後再後退,那個checkbox還是處於勾選狀態,這個在利用ajax區域性頁面重新整理裡是完全做不到的,想到使用者和我說以前後退的時候那個勾還在現在勾沒有了,不解決這個BUG就不驗收的事情時才想到iframe的好啊。

所以如果要精確還原dom物件的狀態,得在history.pushState的時候自行把相關資訊儲存下來,在popstate的時候用到這些資訊並還原dom。

事實上即使用了iframe也並不是所有的瀏覽器會還原dom物件狀態,看這篇文章

總結

  1. 不要輕易從iframe切換到ajax區域性頁面重新整理
  2. 要自己控制那些ajax區域性頁面重新整理紀錄歷史,哪些不記錄,有些時候可能還需要replaceState,不要想當然的把所有請求都記錄歷史
  3. 把程式碼改造成ajax區域性頁面重新整理只是第一步,還需要對整個網站、應用的UI做規劃和設計,關於這個問題不存在通用的解決方案

參考資料

相關文章