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;
原文地址:https://www.cnblogs.com/wangwei1314/p/5595958.html