Created
June 14, 2014 11:03
-
-
Save 303182519/f1fcaa2b7d5a2cf9377d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* router路由功能 | |
*/ | |
(function(){ | |
//辅助方法 | |
//=============================== | |
var _={}; | |
// 创建一个空的对象常量, 便于内部共享使用 | |
var breaker = {}; | |
// 将内置对象的原型链缓存在局部变量, 方便快速调用 | |
var ObjProto = Object.prototype, | |
FuncProto = Function.prototype, | |
ArrayProto = Array.prototype; | |
// 将内置对象原型中的常用方法缓存在局部变量, 方便快速调用 | |
var toString = ObjProto.toString, | |
slice = ArrayProto.slice, | |
hasOwnProperty = ObjProto.hasOwnProperty; | |
// 如果宿主环境中支持这些方法则优先调用, 如果宿主环境中没有提供, 则会由Underscore实现 | |
var nativeKeys = Object.keys, | |
nativeMap = ArrayProto.map, | |
nativeBind = FuncProto.bind, | |
nativeSome = ArrayProto.some, | |
nativeForEach = ArrayProto.forEach; | |
// 检查数据是否是一个正则表达式类型 | |
_.isRegExp = function(obj) { | |
return toString.call(obj) == '[object RegExp]'; | |
}; | |
// 验证对象是否是一个函数类型 | |
_.isFunction = function(obj) { | |
return toString.call(obj) == '[object Function]'; | |
}; | |
// 检查一个属性是否属于对象本身, 而非原型链中 | |
_.has = function(obj, key) { | |
return hasOwnProperty.call(obj, key); | |
}; | |
// 获取一个对象的属性名列表(不包含原型链中的属性) | |
_.keys = nativeKeys || | |
function(obj) { | |
if(obj !== Object(obj)) | |
throw new TypeError('Invalid object'); | |
var keys = []; | |
// 记录并返回对象的所有属性名 | |
for(var key in obj) | |
if(_.has(obj, key)) | |
keys[keys.length] = key; | |
return keys; | |
}; | |
// 迭代处理器, 对集合中每一个元素执行处理器方法 | |
var each = _.each = _.forEach = function(obj, iterator, context) { | |
// 不处理空值 | |
if(obj == null) | |
return; | |
if(nativeForEach && obj.forEach === nativeForEach) { | |
// 如果宿主环境支持, 则优先调用JavaScript 1.6提供的forEach方法 | |
obj.forEach(iterator, context); | |
} else if(obj.length === +obj.length) { | |
// 对<数组>中每一个元素执行处理器方法 | |
for(var i = 0, l = obj.length; i < l; i++) { | |
if( i in obj && iterator.call(context, obj[i], i, obj) === breaker) | |
return; | |
} | |
} else { | |
// 对<对象>中每一个元素执行处理器方法 | |
for(var key in obj) { | |
if(_.has(obj, key)) { | |
if(iterator.call(context, obj[key], key, obj) === breaker) | |
return; | |
} | |
} | |
} | |
}; | |
// 迭代处理器, 与each方法的差异在于map会存储每次迭代的返回值, 并作为一个新的数组返回 | |
_.map = function(obj, iterator, context) { | |
// 用于存放返回值的数组 | |
var results = []; | |
if(obj == null) | |
return results; | |
// 优先调用宿主环境提供的map方法 | |
if(nativeMap && obj.map === nativeMap) | |
return obj.map(iterator, context); | |
// 迭代处理集合中的元素 | |
each(obj, function(value, index, list) { | |
// 将每次迭代处理的返回值存储到results数组 | |
results[results.length] = iterator.call(context, value, index, list); | |
}); | |
// 返回处理结果 | |
if(obj.length === +obj.length) | |
results.length = obj.length; | |
return results; | |
}; | |
// 检查集合中任何一个元素在被转换为Boolean类型时, 是否为true值?或者通过处理器处理后, 是否值为true? | |
var any = _.some = _.any = function(obj, iterator, context) { | |
// 如果没有指定处理器参数, 则默认的处理器函数会返回元素本身, 并在迭代时通过将元素转换为Boolean类型来判断是否为true值 | |
iterator || ( iterator = _.identity); | |
var result = false; | |
if(obj == null) | |
return result; | |
// 优先调用宿主环境提供的some方法 | |
if(nativeSome && obj.some === nativeSome) | |
return obj.some(iterator, context); | |
// 迭代集合中的元素 | |
each(obj, function(value, index, list) { | |
if(result || ( result = iterator.call(context, value, index, list))) | |
return breaker; | |
}); | |
return !!result; | |
}; | |
// 创建一个用于设置prototype的公共函数对象 | |
var ctor = function() { | |
}; | |
// 为一个函数绑定执行上下文, 任何情况下调用该函数, 函数中的this均指向context对象 | |
// 绑定函数时, 可以同时给函数传递调用形参 | |
_.bind = function bind(func, context) { | |
var bound, args; | |
// 优先调用宿主环境提供的bind方法 | |
if(func.bind === nativeBind && nativeBind) | |
return nativeBind.apply(func, slice.call(arguments, 1)); | |
// func参数必须是一个函数(Function)类型 | |
if(!_.isFunction(func)) | |
throw new TypeError; | |
// args变量存储了bind方法第三个开始的参数列表, 每次调用时都将传递给func函数 | |
args = slice.call(arguments, 2); | |
return bound = function() { | |
if(!(this instanceof bound)) | |
return func.apply(context, sargs.concat(slice.call(arguments))); | |
ctor.prototype = func.prototype; | |
var self = new ctor; | |
var result = func.apply(self, args.concat(slice.call(arguments))); | |
if(Object(result) === result) | |
return result; | |
return self; | |
}; | |
}; | |
// 将指定的函数, 或对象本身的所有函数上下本绑定到对象本身, 被绑定的函数在被调用时, 上下文对象始终指向对象本身 | |
// 该方法一般在处理对象事件时使用, 例如: | |
// _(obj).bindAll(); // 或_(obj).bindAll('handlerClick'); | |
// document.addEventListener('click', obj.handlerClick); | |
// 在handlerClick方法中, 上下文依然是obj对象 | |
_.bindAll = function(obj) { | |
// 第二个参数开始表示需要绑定的函数名称 | |
var funcs = slice.call(arguments, 1); | |
// 如果没有指定特定的函数名称, 则默认绑定对象本身所有类型为Function的属性 | |
if(funcs.length == 0) | |
funcs = _.functions(obj); | |
// 循环并将所有的函数上下本设置为obj对象本身 | |
// each方法本身不会遍历对象原型链中的方法, 但此处的funcs列表是通过_.functions方法获取的, 它已经包含了原型链中的方法 | |
each(funcs, function(f) { | |
obj[f] = _.bind(obj[f], obj); | |
}); | |
return obj; | |
}; | |
// 获取一个对象中所有属性值为Function类型的key列表, 并按key名进行排序(包含原型链中的属性) | |
_.functions = _.methods = function(obj) { | |
var names = []; | |
for(var key in obj) { | |
if(_.isFunction(obj[key])) | |
names.push(key); | |
} | |
return names.sort(); | |
}; | |
// 将一个或多个对象的属性(包含原型链中的属性), 复制到obj对象, 如果存在同名属性则覆盖 | |
_.extend = function(obj) { | |
// each循环参数中的一个或多个对象 | |
each(slice.call(arguments, 1), function(source) { | |
// 将对象中的全部属性复制或覆盖到obj对象 | |
for(var prop in source) { | |
obj[prop] = source[prop]; | |
} | |
}); | |
return obj; | |
}; | |
var Router = function(options) { | |
// options默认是一个空对象 | |
options || ( options = {}); | |
// 如果在options中设置了routes对象(路由规则), 则赋给当前实例的routes属性 | |
// routes属性记录了路由规则与事件方法的绑定关系, 当URL与某一个规则匹配时, 会自动调用关联的事件方法 | |
if(options.routes) | |
this.routes = options.routes; | |
// 解析和绑定路由规则 | |
this._bindRoutes(); | |
// 调用自定义的初始化方法 | |
this.initialize.apply(this, arguments); | |
}; | |
var optionalParam = /\((.*?)\)/g; | |
var namedParam = /(\(\?)?:\w+/g; | |
var splatParam = /\*\w+/g; | |
var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; | |
Router.prototype={ | |
initialize: function(){}, | |
// Manually bind a single named route to a callback. For example: | |
// | |
// this.route('search/:query/p:num', 'search', function(query, num) { | |
// ... | |
// }); | |
// | |
route: function(route, name, callback) { | |
if (!_.isRegExp(route)) route = this._routeToRegExp(route); | |
if (_.isFunction(name)) { | |
callback = name; | |
name = ''; | |
} | |
if (!callback) callback = this[name]; | |
var router = this; | |
history.route(route, function(fragment) { | |
var args = router._extractParameters(route, fragment); | |
router.execute(callback, args); | |
}); | |
return this; | |
}, | |
execute: function(callback, args) { | |
if (callback) callback.apply(this, args); | |
}, | |
navigate: function(fragment, options) { | |
history.navigate(fragment, options); | |
return this; | |
}, | |
_bindRoutes: function() { | |
if (!this.routes) return; | |
var route, routes = _.keys(this.routes); | |
while ((route = routes.pop()) != null) { | |
this.route(route, this.routes[route]); | |
} | |
}, | |
_routeToRegExp: function(route) { | |
route = route.replace(escapeRegExp, '\\$&') | |
.replace(optionalParam, '(?:$1)?') | |
.replace(namedParam, function(match, optional) { | |
return optional ? match : '([^/?]+)'; | |
}) | |
.replace(splatParam, '([^?]*?)'); | |
return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$'); | |
}, | |
_extractParameters: function(route, fragment) { | |
var params = route.exec(fragment).slice(1); | |
return _.map(params, function(param, i) { | |
// Don't decode the search params. | |
if (i === params.length - 1) return param || null; | |
return param ? decodeURIComponent(param) : null; | |
}); | |
} | |
} | |
var History = function() { | |
this.handlers = []; | |
_.bindAll(this, 'checkUrl'); | |
// Ensure that `History` can be used outside of the browser. | |
if (typeof window !== 'undefined') { | |
this.location = window.location; | |
this.history = window.history; | |
} | |
}; | |
// 定义用于匹配URL片段中首字符是否为"#"或"/"的正则或空格 | |
var routeStripper = /^[#\/]|\s+$/g; | |
// 定义用于匹配URL片段中首位字符是否"/"的正则 | |
var rootStripper = /^\/+|\/+$/g; | |
// 定义用于匹配从userAgent中获取的字符串是否包含IE浏览器的标识, 用于判断当前浏览器是否为IE | |
var isExplorer = /msie [\w.]+/; | |
// 定义用于匹配URL片段尾字符"/" | |
var trailingSlash = /\/$/; | |
var pathStripper = /#.*$/; | |
History.started = false; | |
History.prototype={ | |
//IE7以下定时器时间 | |
interval: 50, | |
//判断我们是否在根目录下 | |
atRoot: function() { | |
return this.location.pathname.replace(/[^\/]$/, '$&/') === this.root; | |
}, | |
// 获取location中Hash字符串(锚点#后的片段) | |
getHash: function(window) { | |
var match = (window || this).location.href.match(/#(.*)$/); | |
return match ? match[1] : ''; | |
}, | |
// 根据当前设置的路由方式, 处理并返回当前URL中的路由片段 | |
getFragment: function(fragment, forcePushState) { | |
// fragment是通过getHash或从URL中已经提取的待处理路由片段(如 #/id/1288) | |
if (fragment == null) {// 如果没有传递fragment, 则根据当前路由方式进行提取 | |
if (this._hasPushState || forcePushState) { | |
// 使用了pushState方式进行路由 | |
fragment = decodeURI(this.location.pathname + this.location.search); | |
var root = this.root.replace(trailingSlash, ''); | |
if (!fragment.indexOf(root)) fragment = fragment.slice(root.length); | |
} else { | |
fragment = this.getHash(); | |
} | |
} | |
return fragment.replace(routeStripper, ''); | |
}, | |
// 初始化History实例, 该方法只会被调用一次, 应该在创建并初始化Router对象之后被自动调用 | |
// 该方法作为整个路由的调度器, 它将针对不同浏览器监听URL片段的变化, 负责验证并通知到监听函数 | |
start: function(options) { | |
// 如果history对象已经被初始化过, 则抛出错误 | |
if (History.started) throw new Error("Backbone.history has already been started"); | |
// 设置history对象的初始化状态 | |
History.started = true; | |
this.options = _.extend({root: '/'}, this.options, options); | |
this.root = this.options.root; | |
// _wantsHashChange属性记录是否希望使用hash(锚点)的方式来记录和导航路由器 | |
// 除非在options配置项中手动设置hashChange为false, 否则默认将使用hash锚点的方式 | |
// (如果手动设置了options.pushState为true, 且浏览器支持pushState特性, 则会使用pushState方式) | |
this._wantsHashChange = this.options.hashChange !== false; | |
// _wantsPushState属性记录是否希望使用pushState方式来记录和导航路由器 | |
// pushState是HTML5中为window.history添加的新特性, 如果没有手动声明options.pushState为true, 则默认将使用hash方式 | |
this._wantsPushState = !!this.options.pushState; | |
// _hasPushState属性记录浏览器是否支持pushState特性 | |
// 如果在options中设置了pushState(即希望使用pushState方式), 则检查浏览器是否支持该特性 | |
this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); | |
// 获取当前URL中的路由字符串 | |
var fragment = this.getFragment(); | |
// documentMode是IE浏览器的独有属性, 用于标识当前浏览器使用的渲染模式 | |
var docMode = document.documentMode; | |
// oldIE用于检查当前浏览器是否为低版本的IE浏览器(即IE 7.0以下版本) | |
// 这句代码可理解为: 当前浏览器为IE, 但不支持documentMode属性, 或documentMode属性返回的渲染模式为IE7.0以下 | |
var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); | |
//规范根目录的路径 | |
this.root = ('/' + this.root + '/').replace(rootStripper, '/'); | |
if (oldIE && this._wantsHashChange) { | |
var frame = Backbone.$('<iframe src="javascript:0" tabindex="-1">'); | |
this.iframe = frame.hide().appendTo('body')[0].contentWindow; | |
//一阵看 | |
this.navigate(fragment); | |
} | |
// 开始监听路由状态变化 | |
if (this._hasPushState) { | |
// 如果使用了pushState方式路由, 且浏览器支持该特性, 则将popstate事件监听到checkUrl方法 | |
$(window).on('popstate', this.checkUrl); | |
} else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) { | |
// 如果使用Hash方式进行路由, 且浏览器支持onhashchange事件, 则将hashchange事件监听到checkUrl方法 | |
$(window).on('hashchange', this.checkUrl); | |
} else if (this._wantsHashChange) { | |
// 对于低版本的浏览器, 通过setInterval方法心跳监听checkUrl方法, interval属性标识心跳频率 | |
this._checkUrlInterval = setInterval(this.checkUrl, this.interval); | |
} | |
// 记录当前的URL片段 | |
this.fragment = fragment; | |
// 验证当前是否处于根路径(即options.root中所配置的路径) | |
var loc = this.location; | |
if (this._wantsHashChange && this._wantsPushState) { | |
// 如果用户通过pushState方式的URL访问到当前地址, 但用户此时所使用的浏览器并不支持pushState特性 | |
// (这可能是某个用户通过pushState方式访问该应用, 然后将地址分享给其他用户, 而其他用户的浏览器并不支持该特性) | |
if (!this._hasPushState && !this.atRoot()) { | |
// 获取当前pushState方式中的URL片段, 并通过Hash方式重新打开页面 | |
this.fragment = this.getFragment(null, true); | |
// 例如hashState方式的URL为 /root/topic/12001, 重新打开的Hash方式的URL则为 /root#topic/12001 | |
this.location.replace(this.root + '#' + this.fragment); | |
return true; | |
// 如果用户通过Hash方式的URL访问到当前地址, 但调用Backbone.history.start方法时设置了pushState(希望通过pushState方式进行路由) | |
// 且用户浏览器支持pushState特性, 则将当前URL替换为pushState方式(注意, 这里使用replaceState方式进行替换URL, 而页面不会被刷新) | |
// 以下分支条件可理解为: 如果我们希望使用pushState方式进行路由, 且浏览器支持该特性, 同时用户还使用了Hash方式打开当前页面 | |
// (这可能是某个用户使用Hash方式浏览到一个URL, 并将URL分享给另一个浏览器支持pushState特性的用户, 当该用户访问时会执行此分支) | |
} else if (this._hasPushState && this.atRoot() && loc.hash) { | |
this.fragment = this.getHash().replace(routeStripper, ''); | |
// 使用replaceState方法将当前浏览器的URL替换为pushState支持的方式, 即: 协议//主机地址/URL路径/Hash参数, 例如: | |
// 当用户访问Hash方式的URL为 /root/#topic/12001, 将被替换为 /root/topic/12001 | |
// 注: | |
// pushState和replaceState方法的参数有3个, 分别是state, title, url | |
// -state: 用于存储插入或修改的history实体信息 | |
// -title: 用于设置浏览器标题(属于保留参数, 目前浏览器还没有实现该特性) | |
// -url: 设置history实体的URL地址(可以是绝对或相对路径, 但无法设置跨域URL) | |
this.history.replaceState({}, document.title, this.root + this.fragment); | |
} | |
} | |
if (!this.options.silent) return this.loadUrl(); | |
}, | |
// 停止history对路由的监控, 并将状态恢复为未监听状态 | |
// 调用stop方法之后, 可重新调用start方法开始监听, stop方法一般用户在调用start方法之后, 需要重新设置start方法的参数, 或用于单元测试 | |
stop: function() { | |
$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl); | |
if (this._checkUrlInterval) clearInterval(this._checkUrlInterval); | |
History.started = false; | |
}, | |
// 向handlers中绑定一个路由规则(参数route, 类型为正则表达式)与事件(参数callback)的映射关系(该方法由Router的实例自动调用) | |
route: function(route, callback) { | |
// 将route和callback插入到handlers列表的第一个位置 | |
// 这是为了确保最后调用route时传入的规则被优先进行匹配 | |
this.handlers.unshift({ | |
// 路由规则(正则) | |
route : route, | |
// 匹配规则时执行的方法 | |
callback : callback | |
}); | |
}, | |
// 检查当前的URL相对上一次的状态是否发生了变化 | |
// 如果发生变化, 则记录新的URL状态, 并调用loadUrl方法触发新URL与匹配路由规则的方法 | |
// 该方法在onpopstate和onhashchange事件被触发后自动调用, 或者在低版本的IE浏览器中由setInterval心跳定时调用 | |
checkUrl: function() { | |
// 获取当前的URL片段 | |
var current = this.getFragment(); | |
// 对低版本的IE浏览器, 将从iframe中获取最新的URL片段并赋给current变量 | |
if (current === this.fragment && this.iframe) { | |
current = this.getFragment(this.getHash(this.iframe)); | |
} | |
// 如果当前URL与上一次的状态没有发生任何变化, 则停止执行 | |
if (current === this.fragment) return false; | |
// 执行到这里, URL已经发生改变, 调用navigate方法将URL设置为当前URL | |
// 这里在自动调用navigate方法时, 并没有传递options参数, 因此不会触发navigate方法中的loadUrl方法 | |
if (this.iframe) this.navigate(current); | |
// 调用loadUrl方法, 检查匹配的规则, 并执行规则绑定的方法 | |
this.loadUrl(); | |
}, | |
// 根据当前URL, 与handler路由列表中的规则进行匹配 | |
// 如果URL符合某一个规则, 则执行这个规则所对应的方法, 函数将返回true | |
// 如果没有找到合适的规则, 将返回false | |
// loadUrl方法一般在页面初始化时调用start方法会被自动调用(除非设置了silent参数为true) | |
// - 或当用户改变URL后, 由checkUrl监听到URL发生变化时被调用 | |
// - 或当调用navigate方法手动导航到某个URL时被调用 | |
loadUrl: function(fragment) { | |
fragment = this.fragment = this.getFragment(fragment); | |
return _.any(this.handlers, function(handler) { | |
if (handler.route.test(fragment)) { | |
handler.callback(fragment); | |
return true; | |
} | |
}); | |
}, | |
navigate: function(fragment, options) { | |
// 如果没有调用start方法, 或已经调用stop方法, 则无法导航 | |
if (!History.started) return false; | |
// 如果options参数不是一个对象, 而是true值, 则默认trigger配置项为true(即触发导航的URL与对应路由规则的事件) | |
if (!options || options === true) options = {trigger: !!options}; | |
var url = this.root + (fragment = this.getFragment(fragment || '')); | |
//去掉#后面的所有字符 | |
fragment = fragment.replace(pathStripper, ''); | |
// 如果当前URL与需要导航的URL没有变化, 则不继续执行 | |
if (this.fragment === fragment) return; | |
this.fragment = fragment; | |
// Don't include a trailing slash on the root. | |
if (fragment === '' && url !== '/') url = url.slice(0, -1); | |
// 如果当前支持并使用了pushState方式进行导航 | |
if (this._hasPushState) { | |
this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url); | |
// If hash changes haven't been explicitly disabled, update the hash | |
// fragment to store history. | |
} else if (this._wantsHashChange) { | |
// 调用_updateHash方法更新当前URL为新的hash, 并将options中的replace配置传递给_updateHash方法(在该方法中实现替换或追加新的hash) | |
this._updateHash(this.location, fragment, options.replace); | |
// 对于低版本的IE浏览器, 当Hash发生变化时, 更新iframe URL中的Hash | |
if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) { | |
// 如果使用了replace参数替换当前URL, 则直接将iframe替换为新的文档 | |
// 调用document.open打开一个新的文档, 以擦除当前文档中的内容(这里调用close方法是为了关闭文档的状态) | |
// open和close方法之间没有使用write或writeln方法输出内容, 因此这是一个空文档 | |
if(!options.replace) this.iframe.document.open().close(); | |
// 调用_updateHash方法更新iframe中的URL | |
this._updateHash(this.iframe.location, fragment, options.replace); | |
} | |
// 如果在调用start方法时, 手动设置hashChange参数为true, 不希望使用pushState和hash方式导航 | |
// 则直接将页面跳转到新的URL | |
} else { | |
return this.location.assign(url); | |
} | |
// 如果在options配置项中设置了trigger属性, 则调用loadUrl方法查找路由规则, 并执行规则对应的事件 | |
// 在URL发生变化时, 通过checkUrl方法监听到的状态, 会在checkUrl方法中自动调用loadUrl方法 | |
// 在手动调用navigate方法时, 如果需要触发路由事件, 则需要传递trigger参数 | |
if (options.trigger) return this.loadUrl(fragment); | |
}, | |
// 更新或设置当前URL中的Has串, _updateHash方法在使用hash方式导航时被自动调用(navigate方法中) | |
// location是需要更新hash的window.location对象 | |
// fragment是需要更新的hash串 | |
// 如果需要将新的hash替换到当前URL, 可以设置replace为true | |
_updateHash: function(location, fragment, replace) { | |
// 如果设置了replace为true, 则使用location.replace方法替换当前的URL | |
// 使用replace方法替换URL后, 新的URL将占有原有URL在history历史中的位置 | |
if (replace) { | |
// 将当前URL与hash组合为一个完整的URL并替换 | |
var href = location.href.replace(/(javascript:|#).*$/, ''); | |
location.replace(href + '#' + fragment); | |
} else { | |
// 没有使用替换方式, 直接设置location.hash为新的hash串 | |
//这里自动加# | |
location.hash = '#' + fragment; | |
} | |
} | |
} | |
// Create the default history. | |
var history = new History; | |
window.Router=Router; | |
window.historys=history; | |
})() |
就是路由啊,backbone里面的,减了一些功能,就是给那些只想用到路由的人用
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
作用是?