【再探backbone04】單頁應用的基石-路由處理


前言

首先發一點牢騷,互聯網公司變化就是快,我去一個團隊往往就一年時間該團隊就得解散,這不,公司居然把無線團隊解散了,我只能說,我那個去!!!

我去年還到處讓人來呢,一個興興向榮的團隊說沒就沒了啊!我找誰哭去......

於是我們團隊一個大哥說他去哪哪就解散,我老大說他去哪哪就倒霉,如此看來,不是我們導致團隊解散,而是所有的團隊變化都快啊。。。。。。

於是換了個團隊,近幾周情緒較低落啊,但是低落也不能不干實事,所以在此收拾心情明天開始好好干事情吧!

在去年的時候,我們初略的學習了下backbone,一方面是因為期間比較忙,另一方面是因為沒有使用backbone開發項目,所以到路由一節就有點斷斷續續了

於是這個星期正好有空,我們來回看下backbone路由相關的知識點,這里以之前的demo為例

pushState

在介紹backbone路由之前,我們簡單說一下pushState,pushState的出現與Ajax有莫大的關系,無刷新操作帶來了很多優點,但是也會造成一些問題

這些問題中有兩個問題讓人比較頭疼:

① SEO

② 瀏覽器后退

SEO不在我們今天的考慮范疇,所以我們來看看讓人頭疼的瀏覽器后退問題,這里就不得不提pushState了

pushState的提出便是為了即實現局部刷新,又同時改變瀏覽器URL,並且很友好的支持瀏覽器前進后退功能

之前我們為了實現同樣的功能會注冊hashChange事件,劫持瀏覽器的跳轉,但是這樣做不太標准,於是我們來見識一下pushState

window.history.pushState({
  title: title,
  url: url,
  XXX: xxx
}, document.title, url)

與之對應的是popstate事件,該事件會在瀏覽器前進后退時候觸發,他會捕捉當前URL中的data,這里以一個例子說明

首先第一步,為簡單Ajax:

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title></title>
</head>
<body>
  <ul id="sec">
    <li data-key="1">北京</li>
    <li data-key="2">上海</li>
    <li data-key="3">成都</li>
  </ul>
  <h1 id="city">
  </h1>
  <script src="../../jquery-1.7.1.js" type="text/javascript"></script>
  <script type="text/javascript">
    $('#sec li').on('click', function (e) {
      var el = $(e.target);
      $.get('./Handler.ashx', { id: el.attr('data-key') }, function (data) {

        $('#city').html(data);
        var s = '';
      });
    });
//    window.addEventListener("popstate", function (e) {
//      var s = '';
//    });
  </script>
</body>
</html>

對應服務器程序:

public class Handler : IHttpHandler {
    
    public void ProcessRequest (HttpContext context) {
        context.Response.ContentType = "text/plain";

        string name = "無名";

        string id = "0";

        id = context.Request.QueryString["id"];

        if (id == "1") name = "北京";
        if (id == "2") name = "上海";
        if (id == "3") name = "成都";
      
        context.Response.Write(name);
    }
 
    public bool IsReusable {
          get {
            return false;
        }
    }

}
View Code

這個時候便是點擊一個項目便從服務器取得一個項目顯示,這里有兩個新需求:

① url需要跟着變

② 可前進后退,於是我們這么修改

 1 $('#sec li').on('click', function (e) {
 2   var el = $(e.target);
 3   $.get('./Handler.ashx', { id: el.attr('data-key') }, function (data) {
 4     $('#city').html(data);
 5     //新增pushState邏輯
 6     var state = {};
 7     state.title = data;
 8     document.title = data;
 9     history.pushState(state, data, location.href.split("?")[0] + '?cityId=' + el.attr('data-key'));
10     var s = '';
11   });
12 });

這個時候每次點擊URL就會跟着改變了,但是並不會跳轉哦

最后加上前進后退邏輯

 1 $('#sec li').on('click', function (e) {
 2   var el = $(e.target);
 3   $.get('./Handler.ashx', { id: el.attr('data-key') }, function (data) {
 4     $('#city').html(data);
 5     //新增pushState邏輯
 6     var state = {};
 7     state.title = data;
 8     document.title = data;
 9     history.pushState(state, data, location.href.split("?")[0] + '?cityId=' + el.attr('data-key'));
10     var s = '';
11   });
12 });
13 window.addEventListener("popstate", function (e) {
14   var state = history.state;//e.state
15   document.title = state.title;
16   $('#city').html(state.title);
17   var s = '';
18 });

如此便能簡單達到目的,對於其講解暫時到此,為什么這里會提到pushState呢,因為在單頁應用中,路由的處理是一個問題,而pushState是個很好的處理方案

但不是沒有問題的,pushState只支持IE10,所以意味着直接拋棄了winphone為IE9的手機,有幾個人投訴winphone不能訪問就得扣工資 

popstate與hashChange

這里我們順帶說下popstate與hashChange直接的關系,事實上他們之間沒有關系
http://www.google.com/path?query=querystring#hashstring

① http://為協議
② www.google.com為host
③ path為path,也就是我們的子目錄,每個子目錄可以干很多事情
④ #hashstring為hash相關

①-③的變化會觸發服務器請求,會刷新頁面,hash改變不會觸發服務器請求
所以最初的單頁應用,很多都是以hashChange為主,Backbone也不例外初始狀態下是監控hashChange
而pushState的提出多少改變了這一現狀,我們可以改變前面的東西而不引起服務器請求,pushState基本就是干這個事情的

popstate 為1-3環節的變化引起的回調
hashChange 為hash變化引起的回調,是不同滴
一個單頁應用一般只會注冊一個事件,我們之前是hashChange,今天試一試pushState

backbone簡單應用

了解了pushState后,再讓我們回到今天的正題,其實,我們團隊自己也有一套單頁應用框架,前段時間與backbone做了一次競品分析

事實上的結果是使用backbone代碼量會少一點,所以backbone仍然有一定優勢

PS:當時沒好意思給老板匯報給糊弄過去了

下載源碼:02backbone.zip

 

這里還是簡單做一下解釋:

這里主要定義了兩個視圖,index(列表)以及detail,以及對應的model信息,這些我們不予關注,我們將關注點放到路由一塊

 1 var App = Backbone.Router.extend({
 2   routes: {
 3     "": "index",    // #index
 4     "index": "index",    // #index
 5     "detail": "detail"    // #detail
 6   },
 7   index: function () {
 8     var index = new Index(this.interface);
 9 
10   },
11   detail: function () {
12     var detail = new Detail(this.interface);
13 
14   },
15   initialize: function () {
16 
17   },
18   interface: {
19     forward: function (url) {
20       window.location.href = ('#' + url).replace(/^#+/, '#');
21     }
22 
23   }
24 });
25 
26 var app = new App();
27 Backbone.history.start();

由於app中initialize什么都沒有干,所以實例化並未執行任何處理,入口在history.start處

Backbone.history

Backbone全局有一個處理hash的實例,其構造函數為History

var History = Backbone.History = function () {
  this.handlers = [];
  _.bindAll(this, 'checkUrl');

  // Ensure that `History` can be used outside of the browser.
  if (typeof window !== 'undefined') {
    this.location = window.location;
    this.history = window.history;
  }
};

我們調用的start其實是他的一個原型方法,這里便用到了pushState

start: function (options) {
  if (History.started) throw new Error("Backbone.history has already been started");
  History.started = true;

  // Figure out the initial configuration. Do we need an iframe?
  // Is pushState desired ... is it available?
  this.options = _.extend({}, { root: '/' }, this.options, options);
  this.root = this.options.root;
  this._wantsHashChange = this.options.hashChange !== false;
  this._wantsPushState = !!this.options.pushState;
  this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState);
  var fragment = this.getFragment();
  var docMode = document.documentMode;
  var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));

  // Normalize root to always include a leading and trailing slash.
  this.root = ('/' + this.root + '/').replace(rootStripper, '/');

  if (oldIE && this._wantsHashChange) {
    this.iframe = Backbone.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
    this.navigate(fragment);
  }

  // Depending on whether we're using pushState or hashes, and whether
  // 'onhashchange' is supported, determine how we check the URL state.
  if (this._hasPushState) {
    Backbone.$(window).on('popstate', this.checkUrl);
  } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
    Backbone.$(window).on('hashchange', this.checkUrl);
  } else if (this._wantsHashChange) {
    this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
  }

  // Determine if we need to change the base url, for a pushState link
  // opened by a non-pushState browser.
  this.fragment = fragment;
  var loc = this.location;
  var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root;

  // If we've started off with a route from a `pushState`-enabled browser,
  // but we're currently in a browser that doesn't support it...
  if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
    this.fragment = this.getFragment(null, true);
    this.location.replace(this.root + this.location.search + '#' + this.fragment);
    // Return immediately as browser will do redirect to new url
    return true;

    // Or if we've started out with a hash-based route, but we're currently
    // in a browser where it could be `pushState`-based instead...
  } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
    this.fragment = this.getHash().replace(routeStripper, '');
    this.history.replaceState({}, document.title, this.root + this.fragment + loc.search);
  }

  if (!this.options.silent) return this.loadUrl();
},
View Code

好的代碼是會檢測兼容性的,若是支持便使用pushState,不支持便還是使用hashChange

if (this._hasPushState) {
  Backbone.$(window).on('popstate', this.checkUrl);
} else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
  Backbone.$(window).on('hashchange', this.checkUrl);
} else if (this._wantsHashChange) {
  this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
}

值得注意的是,要使用的話調用方法有所不同

Backbone.history.start({ pushState: true });

所以默認情況使用的是hashChange,我們這先繼續往下看,統一的入口是loadUrl

loadUrl: function (fragmentOverride) {
  var fragment = this.fragment = this.getFragment(fragmentOverride);
  var matched = _.any(this.handlers, function (handler) {
    if (handler.route.test(fragment)) {
      handler.callback(fragment);
      return true;
    }
  });
  return matched;
},

這里會的handlers事實上是Router里面定義的集合

這個值是實例化Router時候導入的,這里暫時不予關注

this._bindRoutes();

因為首次url為'',所以會執行相關的路由回調,這里是執行實例化Index view的操作

我們點擊列表項會觸發hashChange,最后又會執行loadUrl,此時因為hash已經變成了detail,所以會執行對應的回調

所以,一輪下來,我們發現Backbone初始化狀態並未使用pushState,而是簡單的hashChange處理路由,開啟pushState規則會有一點點變化

我去有道雲筆記看了下,他也是使用Backbone的項目,並且啟用了pushState,但是其操作中后退功能做的並不好,事實上他后退沒有操作

我們這里要使用pushState,對forward接口的代碼需要做一定調整,第一個要調整的就是“#”,我們得使用標准的“?”

我們這里直接使用Backbone自帶的navigation了,開啟pushState的情況下,每次會在后面加一個“/view”
然后設置trigger為true,便會觸發檢查url找到對應路由后會執行相關回調

最開始,我這里開啟pushState不成功,是因為要設置root的關系......不正在使用下,就算看到源碼也不知道在干什么
if (!fragment.indexOf(root)) fragment = fragment.substr(root.length);

如此一來,我們整個邏輯也就結束了

階段總結

Backbone路由一塊事實上分為兩個大塊,Router以及History
用戶在Router中定義相關規則,然后開啟history.start進行路由監控,一來就會執行默認的回調
所以,Router本身除了定義路由規則,全部調用的是Backbone.history的方法
Backbone.History的主要關注點事實上是 popstate(hashChange)的回調checkUrl,無論我們觸發navigate或者點擊瀏覽器后退
皆是進入此入口,再回調至Router定義的回調,從而實現相關邏輯

片面想法
我其實也是初次實際使用Backbone的路由功能,Backbone的Router以及History事實上作為了控制器而存在
但是,我對這樣的應用卻有不一樣的想法
首先,全部的路由都定義到Router里面有點過重,雖然現在不會有過於復雜的路由配置,難保以后不會出現
其次,作為控制器而言,若是用戶不做一定擴展的話,很多特性加不上去,比如我要實現View以及View之間的通信就缺少依據
最后,以上皆是我移動片面的想法,初學者嘛......

我們這個周末學習了Backbone,卻忘了發出來,現在發出來,希望對各位有幫助

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM