Fork me on GitHub

  今天是四月十二号,距离上次写博已经将近二十天了。一直忙于工作,回家被看书的时间占用了。连续两个礼拜被频繁的足球篮球以及各种体育运动弄的精疲力竭,所以很少抽时间来写技术博客。今天抽出时间把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做框架的时候,不建议使用低版本的浏览器。

posted on 2015-04-12 17:30  chen·yan  阅读(1429)  评论(1编辑  收藏  举报