Skip to content

Instantly share code, notes, and snippets.

@303182519
Created June 14, 2014 11:03
Show Gist options
  • Save 303182519/f1fcaa2b7d5a2cf9377d to your computer and use it in GitHub Desktop.
Save 303182519/f1fcaa2b7d5a2cf9377d to your computer and use it in GitHub Desktop.
/**
* 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;
})()
@303182519
Copy link
Author

就是路由啊,backbone里面的,减了一些功能,就是给那些只想用到路由的人用

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment