• backbone.js1.3.3------------------history和router


    backbone的router和history对象就是对window.history对象的操作。

    学习backbone的router和history之前必须要学习window.history对象。html5给开发者添加了操作history的api。

    这里需要了解两个概念:

    hash:个人理解,hash就是url最后#后面的东西,可以定位到页面的某一个位置

    state:是指push到window.history中的对象。

    history可以在不刷新页面的情况下修改url地址,这里就使用了锚点。通过向history中添加锚点不同的url,这样就可以做到通过浏览器的前进后退修改url而不刷新页面(其实只是监听事件后跳转到锚点所在的位置)。

    在ajax给我们带来提高用户体验,减少http请求好处的同时,显露出了一些不足:

    1.无法使用浏览器的前进后退按钮。

    2.直接复制浏览器url,在新窗口中打开时不是我们想要的页面。

    3.单纯的使用ajax不利于搜索引擎优化,因为搜索引擎无法获取ajax请求的内容

    可以利用history解决这个问题。

      1 // Backbone.Router
      2   // ---------------
      3 
      4   // Routers map faux-URLs to actions, and fire events when routes are
      5   // matched. Creating a new one sets its `routes` hash, if not set statically.
      6   var Router = Backbone.Router = function(options) {
      7     options || (options = {});
      8     this.preinitialize.apply(this, arguments);
      9     if (options.routes) this.routes = options.routes;
     10     this._bindRoutes();
     11     this.initialize.apply(this, arguments);
     12   };
     13 
     14   // Cached regular expressions for matching named param parts and splatted
     15   // parts of route strings.
     16   var optionalParam = /((.*?))/g;
     17   var namedParam    = /((?)?:w+/g;
     18   var splatParam    = /*w+/g;
     19   var escapeRegExp  = /[-{}[]+?.,\^$|#s]/g;
     20 
     21   // Set up all inheritable **Backbone.Router** properties and methods.
     22   _.extend(Router.prototype, Events, {
     23 
     24     // preinitialize is an empty function by default. You can override it with a function
     25     // or object.  preinitialize will run before any instantiation logic is run in the Router.
     26     preinitialize: function(){},
     27 
     28     // Initialize is an empty function by default. Override it with your own
     29     // initialization logic.
     30     initialize: function(){},
     31 
     32     // Manually bind a single named route to a callback. For example:
     33     //
     34     //     this.route('search/:query/p:num', 'search', function(query, num) {
     35     //       ...
     36     //     });
     37     //
     38     //这里调用了backbone.history.route方法将这个对象添加到了backbone.history的handlers中,这样可在backbone.history的loadurl方法遍历handlers并执行匹配的url
     39     route: function(route, name, callback) {
     40       if (!_.isRegExp(route)) route = this._routeToRegExp(route);
     41       if (_.isFunction(name)) {
     42         callback = name;
     43         name = '';
     44       }
     45       if (!callback) callback = this[name];
     46       var router = this;
     47       Backbone.history.route(route, function(fragment) {
     48         var args = router._extractParameters(route, fragment);
     49         if (router.execute(callback, args, name) !== false) {
     50           router.trigger.apply(router, ['route:' + name].concat(args));
     51           router.trigger('route', name, args);
     52           Backbone.history.trigger('route', router, name, args);
     53         }
     54       });
     55       return this;
     56     },
     57 
     58     // Execute a route handler with the provided parameters.  This is an
     59     // excellent place to do pre-route setup or post-route cleanup.
     60     execute: function(callback, args, name) {
     61       if (callback) callback.apply(this, args);
     62     },
     63 
     64     // Simple proxy to `Backbone.history` to save a fragment into the history.
     65     navigate: function(fragment, options) {
     66       Backbone.history.navigate(fragment, options);
     67       return this;
     68     },
     69 
     70     // Bind all defined routes to `Backbone.history`. We have to reverse the
     71     // order of the routes here to support behavior where the most general
     72     // routes can be defined at the bottom of the route map.
     73     _bindRoutes: function() {
     74       if (!this.routes) return;
     75       this.routes = _.result(this, 'routes');
     76       var route, routes = _.keys(this.routes);
     77       while ((route = routes.pop()) != null) {
     78         this.route(route, this.routes[route]);
     79       }
     80     },
     81 
     82     // Convert a route string into a regular expression, suitable for matching
     83     // against the current location hash.
     84     _routeToRegExp: function(route) {
     85       route = route.replace(escapeRegExp, '\$&')
     86                    .replace(optionalParam, '(?:$1)?')
     87                    .replace(namedParam, function(match, optional) {
     88                      return optional ? match : '([^/?]+)';
     89                    })
     90                    .replace(splatParam, '([^?]*?)');
     91       return new RegExp('^' + route + '(?:\?([\s\S]*))?$');
     92     },
     93 
     94     // Given a route, and a URL fragment that it matches, return the array of
     95     // extracted decoded parameters. Empty or unmatched parameters will be
     96     // treated as `null` to normalize cross-browser behavior.
     97     _extractParameters: function(route, fragment) {
     98       var params = route.exec(fragment).slice(1);
     99       return _.map(params, function(param, i) {
    100         // Don't decode the search params.
    101         if (i === params.length - 1) return param || null;
    102         return param ? decodeURIComponent(param) : null;
    103       });
    104     }
    105 
    106   });
    107 
    108   // Backbone.History
    109   // ----------------
    110 
    111   // Handles cross-browser history management, based on either
    112   // [pushState](http://diveintohtml5.info/history.html) and real URLs, or
    113   // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange)
    114   // and URL fragments. If the browser supports neither (old IE, natch),
    115   // falls back to polling.
    116   var History = Backbone.History = function() {
    117     this.handlers = [];
    118     this.checkUrl = _.bind(this.checkUrl, this);
    119 
    120     // Ensure that `History` can be used outside of the browser.
    121     if (typeof window !== 'undefined') {
    122       this.location = window.location;
    123       this.history = window.history;
    124     }
    125   };
    126 
    127   // Cached regex for stripping a leading hash/slash and trailing space.
    128   var routeStripper = /^[#/]|s+$/g;
    129 
    130   // Cached regex for stripping leading and trailing slashes.
    131   var rootStripper = /^/+|/+$/g;
    132 
    133   // Cached regex for stripping urls of hash.
    134   var pathStripper = /#.*$/;
    135 
    136   // Has the history handling already been started?
    137   History.started = false;
    138 
    139   // Set up all inheritable **Backbone.History** properties and methods.
    140   _.extend(History.prototype, Events, {
    141 
    142     // The default interval to poll for hash changes, if necessary, is
    143     // twenty times a second.
    144     interval: 50,
    145 
    146     // Are we at the app root?
    147     atRoot: function() {
    148       var path = this.location.pathname.replace(/[^/]$/, '$&/');
    149       return path === this.root && !this.getSearch();
    150     },
    151 
    152     // Does the pathname match the root?
    153     matchRoot: function() {
    154       var path = this.decodeFragment(this.location.pathname);
    155       var rootPath = path.slice(0, this.root.length - 1) + '/';
    156       return rootPath === this.root;
    157     },
    158 
    159     // Unicode characters in `location.pathname` are percent encoded so they're
    160     // decoded for comparison. `%25` should not be decoded since it may be part
    161     // of an encoded parameter.
    162     decodeFragment: function(fragment) {
    163       return decodeURI(fragment.replace(/%25/g, '%2525'));
    164     },
    165 
    166     // In IE6, the hash fragment and search params are incorrect if the
    167     // fragment contains `?`.
    168     getSearch: function() {
    169       var match = this.location.href.replace(/#.*/, '').match(/?.+/);
    170       return match ? match[0] : '';
    171     },
    172 
    173     // Gets the true hash value. Cannot use location.hash directly due to bug
    174     // in Firefox where location.hash will always be decoded.
    175     getHash: function(window) {
    176       var match = (window || this).location.href.match(/#(.*)$/);
    177       return match ? match[1] : '';
    178     },
    179 
    180     // Get the pathname and search params, without the root.
    181     getPath: function() {
    182       var path = this.decodeFragment(
    183         this.location.pathname + this.getSearch()
    184       ).slice(this.root.length - 1);
    185       return path.charAt(0) === '/' ? path.slice(1) : path;
    186     },
    187 
    188     // Get the cross-browser normalized URL fragment from the path or hash.
    189     getFragment: function(fragment) {
    190       if (fragment == null) {
    191         if (this._usePushState || !this._wantsHashChange) {
    192           fragment = this.getPath();
    193         } else {
    194           fragment = this.getHash();
    195         }
    196       }
    197       return fragment.replace(routeStripper, '');
    198     },
    199 
    200     // Start the hash change handling, returning `true` if the current URL matches
    201     // an existing route, and `false` otherwise.
    202     //完成以下事情:
    203     //options={root:'',
    204     //hashChange:''默认为true,
    205     //pushState:''默认为true}
    206     //1.解析option初始化参数。
    207     //2.监听popstate和hashchange事件触发则执行checkurl。若不支持,则定时执行checkurl
    208     start: function(options) {
    209       if (History.started) throw new Error('Backbone.history has already been started');
    210       History.started = true;
    211       // Figure out the initial configuration. Do we need an iframe?
    212       // Is pushState desired ... is it available?
    213       this.options          = _.extend({root: '/'}, this.options, options);
    214       this.root             = this.options.root;
    215       this._wantsHashChange = this.options.hashChange !== false;
    216       this._hasHashChange   = 'onhashchange' in window && (document.documentMode === void 0 || document.documentMode > 7);
    217       this._useHashChange   = this._wantsHashChange && this._hasHashChange;
    218       this._wantsPushState  = !!this.options.pushState;
    219       this._hasPushState    = !!(this.history && this.history.pushState);
    220       this._usePushState    = this._wantsPushState && this._hasPushState;
    221       this.fragment         = this.getFragment();
    222 
    223       // Normalize root to always include a leading and trailing slash.
    224       this.root = ('/' + this.root + '/').replace(rootStripper, '/');
    225 
    226       // Transition from hashChange to pushState or vice versa if both are
    227       // requested.
    228       if (this._wantsHashChange && this._wantsPushState) {
    229 
    230         // If we've started off with a route from a `pushState`-enabled
    231         // browser, but we're currently in a browser that doesn't support it...
    232         if (!this._hasPushState && !this.atRoot()) {
    233           var rootPath = this.root.slice(0, -1) || '/';
    234           this.location.replace(rootPath + '#' + this.getPath());
    235           // Return immediately as browser will do redirect to new url
    236           return true;
    237 
    238         // Or if we've started out with a hash-based route, but we're currently
    239         // in a browser where it could be `pushState`-based instead...
    240         } else if (this._hasPushState && this.atRoot()) {
    241           this.navigate(this.getHash(), {replace: true});
    242         }
    243 
    244       }
    245 
    246       // Proxy an iframe to handle location events if the browser doesn't
    247       // support the `hashchange` event, HTML5 history, or the user wants
    248       // `hashChange` but not `pushState`.
    249       if (!this._hasHashChange && this._wantsHashChange && !this._usePushState) {
    250         this.iframe = document.createElement('iframe');
    251         this.iframe.src = 'javascript:0';
    252         this.iframe.style.display = 'none';
    253         this.iframe.tabIndex = -1;
    254         var body = document.body;
    255         // Using `appendChild` will throw on IE < 9 if the document is not ready.
    256         var iWindow = body.insertBefore(this.iframe, body.firstChild).contentWindow;
    257         iWindow.document.open();
    258         iWindow.document.close();
    259         iWindow.location.hash = '#' + this.fragment;
    260       }
    261 
    262       // Add a cross-platform `addEventListener` shim for older browsers.
    263       var addEventListener = window.addEventListener || function(eventName, listener) {
    264         return attachEvent('on' + eventName, listener);
    265       };
    266 
    267       // Depending on whether we're using pushState or hashes, and whether
    268       // 'onhashchange' is supported, determine how we check the URL state.
    269       if (this._usePushState) {
    270         addEventListener('popstate', this.checkUrl, false);
    271       } else if (this._useHashChange && !this.iframe) {
    272         addEventListener('hashchange', this.checkUrl, false);
    273       } else if (this._wantsHashChange) {
    274         this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
    275       }
    276 
    277       if (!this.options.silent) return this.loadUrl();
    278     },
    279 
    280     // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
    281     // but possibly useful for unit testing Routers.
    282     stop: function() {
    283       // Add a cross-platform `removeEventListener` shim for older browsers.
    284       var removeEventListener = window.removeEventListener || function(eventName, listener) {
    285         return detachEvent('on' + eventName, listener);
    286       };
    287 
    288       // Remove window listeners.
    289       if (this._usePushState) {
    290         removeEventListener('popstate', this.checkUrl, false);
    291       } else if (this._useHashChange && !this.iframe) {
    292         removeEventListener('hashchange', this.checkUrl, false);
    293       }
    294 
    295       // Clean up the iframe if necessary.
    296       if (this.iframe) {
    297         document.body.removeChild(this.iframe);
    298         this.iframe = null;
    299       }
    300 
    301       // Some environments will throw when clearing an undefined interval.
    302       if (this._checkUrlInterval) clearInterval(this._checkUrlInterval);
    303       History.started = false;
    304     },
    305 
    306     // Add a route to be tested when the fragment changes. Routes added later
    307     // may override previous routes.
    308     route: function(route, callback) {
    309       this.handlers.unshift({route: route, callback: callback});
    310     },
    311 
    312     // Checks the current URL to see if it has changed, and if it has,
    313     // calls `loadUrl`, normalizing across the hidden iframe.
    314     //最终会执行loadURL
    315     checkUrl: function(e) {
    316       var current = this.getFragment();
    317 
    318       // If the user pressed the back button, the iframe's hash will have
    319       // changed and we should use that for comparison.
    320       if (current === this.fragment && this.iframe) {
    321         current = this.getHash(this.iframe.contentWindow);
    322       }
    323 
    324       if (current === this.fragment) return false;
    325       if (this.iframe) this.navigate(current);
    326       this.loadUrl();
    327     },
    328 
    329     // Attempt to load the current URL fragment. If a route succeeds with a
    330     // match, returns `true`. If no defined routes matches the fragment,
    331     // returns `false`.
    332     //在router中寻找
    333     loadUrl: function(fragment) {
    334       // If the root doesn't match, no routes can match either.
    335       if (!this.matchRoot()) return false;
    336       fragment = this.fragment = this.getFragment(fragment);
    337       return _.some(this.handlers, function(handler) {
    338         if (handler.route.test(fragment)) {
    339           handler.callback(fragment);
    340           return true;
    341         }
    342       });
    343     },
    344 
    345     // Save a fragment into the hash history, or replace the URL state if the
    346     // 'replace' option is passed. You are responsible for properly URL-encoding
    347     // the fragment in advance.
    348     //
    349     // The options object can contain `trigger: true` if you wish to have the
    350     // route callback be fired (not usually desirable), or `replace: true`, if
    351     // you wish to modify the current URL without adding an entry to the history.
    352     navigate: function(fragment, options) {
    353       if (!History.started) return false;
    354       if (!options || options === true) options = {trigger: !!options};
    355 
    356       // Normalize the fragment.
    357       fragment = this.getFragment(fragment || '');
    358 
    359       // Don't include a trailing slash on the root.
    360       var rootPath = this.root;
    361       if (fragment === '' || fragment.charAt(0) === '?') {
    362         rootPath = rootPath.slice(0, -1) || '/';
    363       }
    364       var url = rootPath + fragment;
    365 
    366       // Strip the fragment of the query and hash for matching.
    367       fragment = fragment.replace(pathStripper, '');
    368 
    369       // Decode for matching.
    370       var decodedFragment = this.decodeFragment(fragment);
    371 
    372       if (this.fragment === decodedFragment) return;
    373       this.fragment = decodedFragment;
    374 
    375       // If pushState is available, we use it to set the fragment as a real URL.
    376       if (this._usePushState) {
    377         this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
    378 
    379       // If hash changes haven't been explicitly disabled, update the hash
    380       // fragment to store history.
    381       } else if (this._wantsHashChange) {
    382         this._updateHash(this.location, fragment, options.replace);
    383         if (this.iframe && fragment !== this.getHash(this.iframe.contentWindow)) {
    384           var iWindow = this.iframe.contentWindow;
    385 
    386           // Opening and closing the iframe tricks IE7 and earlier to push a
    387           // history entry on hash-tag change.  When replace is true, we don't
    388           // want this.
    389           if (!options.replace) {
    390             iWindow.document.open();
    391             iWindow.document.close();
    392           }
    393 
    394           this._updateHash(iWindow.location, fragment, options.replace);
    395         }
    396 
    397       // If you've told us that you explicitly don't want fallback hashchange-
    398       // based history, then `navigate` becomes a page refresh.
    399       } else {
    400         return this.location.assign(url);
    401       }
    402       if (options.trigger) return this.loadUrl(fragment);
    403     },
    404 
    405     // Update the hash location, either replacing the current entry, or adding
    406     // a new one to the browser history.
    407     _updateHash: function(location, fragment, replace) {
    408       if (replace) {
    409         var href = location.href.replace(/(javascript:|#).*$/, '');
    410         location.replace(href + '#' + fragment);
    411       } else {
    412         // Some browsers require that `hash` contains a leading #.
    413         location.hash = '#' + fragment;
    414       }
    415     }
    416 
    417   });
    418 
    419   // Create the default Backbone.history.
    420   Backbone.history = new History;
  • 相关阅读:
    Atitit 华为基本法 attilax读后感
    Atitit 华为管理者内训书系 以奋斗者为本 华为公司人力资源管理纲要 attilax读后感
    Atitit 项目版本管理gitflow 与 Forking的对比与使用
    Atitit 管理的模式扁平化管理 金字塔 直线型管理 垂直管理 水平管理 矩阵式管理 网状式样管理 多头管理 双头管理
    Atitit 乌合之众读后感attilax总结 与读后感结构规范总结
    深入理解 JavaScript 异步系列(4)—— Generator
    深入理解 JavaScript 异步系列(3)—— ES6 中的 Promise
    深入理解 JavaScript 异步系列(2)—— jquery的解决方案
    深入理解 JavaScript 异步系列(1)——基础
    使用 github + jekyll 搭建个人博客
  • 原文地址:https://www.cnblogs.com/wangwei1314/p/5595958.html
Copyright © 2020-2023  润新知