1. 程式人生 > >pjax使用小結

pjax使用小結

處理 繼續 暫時 無刷新 從服務器 back 更多 mage win

前言


上周看到一篇文章在分析簡書我的主頁頁面3個tab頁切換的bug,起先以為是尋常的樣式bug而已沒怎麽在意,後來在文章中看到pjax這個術語,長得和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 。 實際的效果見:https://pjax.herokuapp.com/ 沒有勾選pjax的時候點擊鏈接是跳轉的, 勾選了之後鏈接都是變成了ajax刷新(實際效果如下圖的請求內容對比)。

技術分享
不使用pjax 技術分享
使用pjax
優點:
  • 減輕服務端壓力

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

  • 優化頁面跳轉體驗

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

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

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

  • 使服務端處理變得復雜

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

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

用法


  1. 引入jquery和jquery.pjax.js
  2. 註冊事件
    /**
     * 方式一 按鈕父節點監聽事件
     *
     * @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‘);

options默認參數說明


參數名默認值說明
timeout 650 ajax 超時時間(單位ms),超時後會執行默認的頁面跳轉,所以超時時間不應過短,不過一般不需要設置
push true 使用window.history.pushState改變地址欄url(會添加新的歷史記錄)
replace false 使用window.history.replaceState改變地址欄url(不會添加歷史記錄)
maxCacheLength 20 緩存的歷史頁面個數(pjax加載新頁面前會把原頁面的內容緩存起來,緩存加載後其中的腳本會再次執行)
version 是一個函數,返回當前頁面的pjax-version,即頁面中<meta http-equiv="x-pjax-version">標簽內容。使用response.setHeader("X-PJAX-Version", "")設置與當前頁面不同的版本號,可強制頁面跳轉而不是局部刷新。
scrollTo 0 頁面加載後垂直滾動距離(與原頁面保持一致可使過度效果更平滑)
type "GET" ajax的參數,http請求方式
dataType "html" ajax的參數,響應內容的Content-Type
container 用於查找容器的CSS選擇器,[container]參數沒有指定時使用
url link.href 要跳轉的連接,默認a標簽的href屬性
target link pjax事件參數e的relatedTarget屬性,默認為點擊的a標簽
fragment 使用響應內容的指定部分(css選擇器)填充頁面,服務端不進行處理導致全頁面請求的時候需要使用該參數,簡單的說就是對請求到的頁面做截取

除了上述參數外,ajax的一些參數也是可以設置在這裏的,不過一般沒什麽必要。
// ajax 最終參數: options = $.extend(true, {}, $.ajaxSettings, pjax.defaults, options);

pjax失效情況


會有一些情況導致pjax失效,下面結合源碼分析下(省略部分無關代碼)

function handleClick(event, container, options) {
    ...

    // 1. 點擊事件的事件源不是a標簽。使用a標簽可以做到對舊版本瀏覽器的兼容,所以不建議使用其他標簽註冊事件
    if (link.tagName.toUpperCase() !== ‘A‘)
        throw "$.fn.pjax or $.pjax.click requires an anchor element"

    // 2. 使用鼠標滾輪點擊(新標簽頁打開)
    // 點擊超鏈接的同時按下Shift、Ctrl、Alt和Meta(在Windows鍵盤中是Windows鍵,在蘋果機中是Cmd鍵)
    // 作用分別代表新窗口打開、新標簽打開(不切換標簽)、下載、新標簽打開(切換標簽)
    if (event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
        return

    // 3. 跨域(網絡通訊協議,域名不一致)
    if (location.protocol !== link.protocol || location.hostname !== link.hostname)
        return

    // 4. 當前頁面的錨點定位
    if (link.href.indexOf(‘#‘) > -1 && stripHash(link) == stripHash(location))
        return

    // 5. 已經阻止元素發生默認的行為(url跳轉)
    if (event.isDefaultPrevented())
        return

    ...

    var clickEvent = $.Event(‘pjax:click‘)
    $(link).trigger(clickEvent, [opts])

    // 6. pjax:click事件回調中已經阻止元素發生默認的行為(url跳轉)
    if (!clickEvent.isDefaultPrevented()) {
        pjax(opts)
        event.preventDefault()// 阻止url跳轉
        $(link).trigger(‘pjax:clicked‘, [opts])
    }
}

除了上述情況之外,還有下列幾種情況:

  • ajax請求失敗,或者timeout後請求被中止
  • 當前頁面X-PJAX-Version和請求的新頁面版本不一致
  • 請求得到完整的頁面(包含html標簽)卻沒設置fragment參數

事件


1. 點擊鏈接後觸發的一系列事件, 除了pjax:click 和 pjax: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結束

服務端配置


我的項目是spring MVC + velocity 的組合,這裏就以此為例子,其他語言和框架的服務端可以參考下這裏的思路。
項目中使用的識圖解析器是org.springframework.web.servlet.view.velocity.VelocityLayoutViewResolver這個類,好處是可以使用模版技術,每個頁面可以只寫主體內容,公共部分統一寫在模版裏面,是不是和pjax絕配哈!pjax.js默認會在請求頭加入X_PJAX字段,並置為true,所以以此來判斷是否pjax請求。對於普通的請求使用常規的模版,pjax請求則使用空模版或者特定的模版。

  • 常規模版內容:

  • <!doctype html>
    <html>
      #set($basePath = "screen/contain")
      <head>
          <meta http-equiv="x-pjax-version" content="$!{X-PJAX-Version}"/>
          #parse("$basePath/html-head.vm")
      </head>
      <body>
          <section id="container">
              #parse("$basePath/frame-head.vm")
              #parse("$basePath/frame-left.vm")
              <section id="main-content">
                  <section class="wrapper">
                      $screen_content ##頁面內容
                  </section>
              </section>
              #parse("$basePath/frame-bottom.vm")
          </section>
      </body>
    </html>
  • 添加SpringMVC 中的Interceptor 攔截器,用於後端渲染前插入pjax處理

    public class PjaxInterceptor extends HandlerInterceptorAdapter {
    
      @Value("${X-PJAX-Version}")
      private String X_PJAX_VERSION;
    
      /**
       * Controller 方法調用之後,頁面渲染前執行
       * 
       * @param request
       * @param response
       * @param handler
       * @param modelAndView
       * @throws Exception
       */
      public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
          if (modelAndView != null) {
              boolean isPajx = Boolean.parseBoolean(request.getHeader("X-PJAX"));// 值為true表示pjax請求,這是重點
              ModelMap model = modelAndView.getModelMap();
              model.addAttribute("X-PJAX-Version", X_PJAX_VERSION);// 設置當前頁面的pjax版本
              if (isPajx) {
                  model.addAttribute("layout", "layout_pjax.vm");// 指定pjax請求時使用的模版
                  // 在vm頁面中通過 #set($layout = ‘xxx.vm‘) 的方式指定模版
                  response.setHeader("X-PJAX-Version", X_PJAX_VERSION);// 響應內容的pjax版本,有新模版發布時,通過配置文件修改版本來強制頁面刷新
              }
          }
      }
    }
  • xml配置
    <mvc:interceptors>
      <mvc:interceptor>
          <mvc:mapping path="/**"/>
          <bean id="pjaxInterceptor" class="xxx.PjaxInterceptor"/>
      </mvc:interceptor>
    </mvc:interceptors>
  • pjax請求模版頁面:layout_pjax.vm
    <title>$!{title}</title>
    $screen_content
    模版中使用title標簽,這樣pjax請求時不僅地址欄url會變化,而且瀏覽器標簽的標題內容也會變化。
    針對沒有服務端處理的方案如下:
    $(document).pjax(‘a[data-pjax]‘, ‘#main-content .wrapper‘, {fragment: ‘#main-content .wrapper‘});// fragment一般同container一致

插件伴侶——NProgress


比較漂亮的一款進度條插件,用法十分簡單,很適合做pjax的過度動畫,詳細用法在該項目github上有介紹

技術分享
NProgress
  • 示例:$(document).on(‘pjax:start‘, NProgress.start).on(‘pjax:end‘, NProgress.done);

結語


雖然個人還是比較喜歡造輪子(有成就感),不怎麽喜歡用插件(一般插件使用復雜,文檔少學習成本大,還不如自己寫),但看了pjax的源碼後感覺真要自己也使用pushState + ajax的方式簡單的實現它的功能,還是要踩不少坑的,所以為什麽要放著這麽個易用又精致的小輪子不用呢?我的項目是一個管理系統,統一的左側菜單+右側table的布局,每個頁面都需要一個獨立訪問的url,非常適合使用pjax。由於使用的velocity模版技術,集成pjax就是分分鐘的事,不僅對原先的代碼完全沒影響,還提升了加載速度和頁面過度體驗效果,再用上了NProgress,感覺逼格又上升不少,哈哈。

前段時間工作比較忙好久沒寫文章了,這段時間有點閑下來就抽空學了些新東西記錄下,對於這次的學習成果還是比較滿意的。( *^_^* )

前端

pjax使用小結