今天是四月十二号,距离上次写博已经将近二十天了。一直忙于工作,回家被看书的时间占用了。连续两个礼拜被频繁的足球篮球以及各种体育运动弄的精疲力竭,所以很少抽时间来写技术博客。今天抽出时间把backbone的基本模块部分写完,还有一篇总结篇就结束这个系列了。昨天下了一整天的雨,天空放晴,外面一片清澈,索性来到大学城图书馆待会儿。别的就不扯了,下面开始进入正题。
这次我们将Router模块和history合并在一起,我们简称它们为路由器模块, 在backbone里面充当引路人的角色,它会监听浏览器里面的hash值的变化从而响应相对的事件。它的最基本原理是利用的了浏览器中的onhashchange或者pushstate监听事件。在我们讨论路由器模块之前,先让我们了解一些浏览器的url变化监听机制是怎么样的。以下是摘自网上的一段文字说明:
“在 URL 中 传值有两方式,一种是通过 search 来传值(即 ? 后面的部分),一种是通过 hash 来传值(即 # 后面的部分)。它们之间有一个区别, 即 search 改变时浏览器会发一次的 http request,而 hash 改变时浏览器不会发送 http request。也就是 说,search 可以用来做浏览器和服务器端的信息传递,而 hash 则更适合用于本地页面的信息传递”
在了解了这种监听机制后,我们开始来介绍一下router和histroy模块的基本结构和设计思路
//router的构造函数 e.Router = function(a) { a || (a = {}); /*为routes赋值,a参数是一个对象的字面量,它里面有很多函数 分别对应的是routes中的函数名。同时其中还有一个特殊的属性是routes, 以名(hash跳转名)和值(自定义参数名)为基本机构。 所以闯入的参数大概结构如下 a = { routes : { '/toPage1' : 'toPage1Fn', '/toPage2' : 'toPage2Fn'}, toPage1Fn : function(){ xxxxxx}, toPage2Fn : function(){}}; */ if (a.routes) this.routes = a.routes; this._bindRoutes(); //执行初始化函数 this.initialize.apply(this, arguments) };
接下来是扩展router的原型,router只是一个表面上的工作者,它原始的方法很少,也比较简单,只是做一些正则的验证和一些方法的中转(也即是调用history中的方法实现路由机制)。实际上的
路由机制都是用history模块来完成的。因此,我再下面的方法中只做简单的概要,而在history中对其中用到的方法会详细讲到。下面是router模块中原始方法:
f.extend(e.Router.prototype, e.Events, { initialize: function() {}, //绑定初始化的值 即将属性routes中的各个值和外层的function对应起来。a是正则表达式,如果不是则会进行转换。 route: function(a, b, c) { e.history || (e.history = new e.History); f.isRegExp(a) || (a = this._routeToRegExp(a)); e.history.route(a, f.bind(function(d) { d = this._extractParameters(a, d); c.apply(this, d); this.trigger.apply(this, ["route:" + b].concat(d)) }, this)) }, //导航方法,手动更新url的hash值,a 是需要跳转的hash值 navigate: function(a, b) { e.history.navigate(a, b) }, //将初始化传入的参数做处理。 _bindRoutes: function() { if (this.routes) { var a = [], b; for (b in this.routes) a.unshift([b, this.routes[b]]); b = 0; for (var c = a.length; b < c; b++) this.route(a[b][0], a[b][1], this[a[b][1]]) } }, //将a转换为正则 _routeToRegExp: function(a) { a = a.replace(s, "\$&").replace(q, "([^/]*)").replace(r, "(.*?)"); return RegExp("^" + a + "$") }, _extractParameters: function(a, b) { return a.exec(b).slice(1) } });
因此我们在初始化router之前,必须先扩展它的原型对象,下面是router的具体使用:
接下来我们重点介绍history模块,它是实际上路由的承担者,内部的方法值得介绍。首先时构造构造函数:
e.History = function() { this.handlers = []; //绑定checkUrl的执行域一直在history中 f.bindAll(this, "checkUrl") };
接下来是对其原型进行扩展:
f.extend(e.History.prototype, {..........
在扩展原型的方法中,只有一个(start)是对外的方法,它在history对象被实例化之后手动调用Backbone.History.start();,其他都是供Router模块调用的方法:
start: function(a) { if (m) throw Error("Backbone.history has already been started"); this.options = f.extend({}, { root: "/" }, this.options, a); this._wantsPushState = !!this.options.pushState; this._hasPushState = !(!this.options.pushState || !window.history || !window.history.pushState); a = this.getFragment(); var b = document.documentMode; if (b = t.exec(navigator.userAgent.toLowerCase()) && (!b || b <= 7)) this.iframe = g('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow, this.navigate(a); this._hasPushState ? g(window).bind("popstate", this.checkUrl) : "onhashchange" in window && !b ? g(window).bind("hashchange", this.checkUrl) : setInterval(this.checkUrl, this.interval); this.fragment = a; m = !0; a = window.location; b = a.pathname == this.options.root; if (this._wantsPushState && !this._hasPushState && !b) return this.fragment = this.getFragment(null, !0), window.location.replace(this.options.root + "#" + this.fragment), !0; else if (this._wantsPushState && this._hasPushState && b && a.hash) this.fragment = a.hash.replace(j, ""), window.history.replaceState({}, document.title, a.protocol + "//" + a.host + this.options.root + this.fragment); if (!this.options.silent) return this.loadUrl() },
说明:start 初始化函数,实例化history模块后立即执行此函数,它做了如下几件事情:
一:设置一些基本的属性,比如是否启用pushState方法作为默认的监听方法,还有基础路径root的设置,root在默认的情况下根目录。还有是否渲染界面的slient属性。
二:绑定hash变化的方法:将checkUl的方法绑定到onhashchange上,以便监听浏览器的地址的变化。
三:对浏览器的兼容处理:对于没有pushState事件或者onhashchange事件的浏览器其,比如说早期的Ie版本,它会用setInterval来解决hash变化。是为了记录两次不同的hash值得变化,bk的做法是将上一次的hash值保存到一个iframe中,待下次改变hash值改变的时候,我们可以从iframe中取值与后一次的对比,这样就可以判断hash值是否有变化,然后进行跳转的动作。
然后是其他一些方法的说明:
//配置对应的路由方法,意思是将地址栏中的hash值和自定义的方法对应起来。此方法供router模块中的同名troute方法调用。 route: function(a, b) { this.handlers.unshift({ route: a, callback: b }) }, //侦测地址栏中的地址变化。并且放回对应的触发函数。这时候它就去iframe中取保存的hash值 来和当前的hash值做对比。 checkUrl: function() { var a = this.getFragment(); a == this.fragment && this.iframe && (a = this.getFragment(this.iframe.location.hash)); if (a == this.fragment || a == decodeURIComponent(this.fragment)) return ! 1; this.iframe && this.navigate(a); this.loadUrl() || this.loadUrl(window.location.hash) }, //在this.handlers中找到hash变化的对应函数并且返回。 loadUrl: function(a) { var b = this.fragment = this.getFragment(a); return f.any(this.handlers, function(a) { if (a.route.test(b)) return a.callback(b), !0 }) }, //手动改变hash值(即浏览器中对应的地址),导航到某个方法或者界面。a是你需要传入的hash值; navigate: function(a, b) { var c = (a || "").replace(j, ""); if (! (this.fragment == c || this.fragment == decodeURIComponent(c))) { if (this._hasPushState) { var d = window.location; c.indexOf(this.options.root) != 0 && (c = this.options.root + c); this.fragment = c; window.history.pushState({}, document.title, d.protocol + "//" + d.host + c) } else if (window.location.hash = this.fragment = c, this.iframe && c != this.getFragment(this.iframe.location.hash)) this.iframe.document.open().close(), this.iframe.location.hash = c; b && this.loadUrl(a) } }
还有一个getFragment方法是放回当前浏览器中的hash值。在此不做介绍。总的来说History模块做的事监听浏览器中url的变化然后调用对应的函数。它的流程大概就是这样的:
第一步:
第二步改变地址:
或者:
或者:
然后onhashchange响应,回掉page2对应的pageFn方法来进行模块之间的互动.
小提示:如果浏览器不支持任何原始的url响应事件,那么setInterval会不间断地(50ms)刷新监控它的变化,这样做显然是不合理的退而求其次的方法。所以在选用bk做框架的时候,不建议使用低版本的浏览器。