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;