backbone.Router History源码笔记

Backbone.History和Backbone.Router

history和router都是控制路由的,做一个单页应用,要控制前进后退,就可以用到他们了。

History类用于监听URL的变化,和触发Action方法,他可以添加对url的监听,

Router类用于定义和解析路由规则,并将URL映射到Action。

 

router和history一些个人的注解

<!DOCTYPE html>

<html>

<head>

    <meta charset="utf-8"/>

    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />

    <title>backbone</title>

    <style type="text/css">

        *{padding:0;margin:0;}

        .wrap{width:960px; margin: 100px auto; padding: 20px 0;}

        ul{ list-style: none;}

    </style>

</head>

<body>

    <div class="wrap">

        <div id="a1"></div>

        <div id="a2"></div>

        <div id="a3"></div>

    </div>

<script src="http://files.cnblogs.com/wtcsy/jquery.js"></script> 

<script src="http://files.cnblogs.com/wtcsy/underscore.js"></script>

<script src="http://files.cnblogs.com/wtcsy/events.js"></script>

<script>

(function(){

  // Backbone.History

  // ----------------



    // Cached regex for stripping a leading hash/slash and trailing space.

    var routeStripper = /^[#\/]|\s+$/g;



    // Cached regex for stripping leading and trailing slashes.

    var rootStripper = /^\/+|\/+$/g;



    // Cached regex for stripping urls of hash.

    var pathStripper = /#.*$/;





    // Handles cross-browser history management, based on either

    // [pushState](http://diveintohtml5.info/history.html) and real URLs, or

    // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange)

    // and URL fragments. If the browser supports neither (old IE, natch),

    // falls back to polling.

    var History = Backbone.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;

        }

    };



    // Has the history handling already been started?

    History.started = false;    



    _.extend(History.prototype, Backbone.Events, {



        // The default interval to poll for hash changes, if necessary, is

        // twenty times a second.

        interval: 50,



        // Are we at the app root?

        atRoot: function() {

            var path = this.location.pathname.replace(/[^\/]$/, '$&/');

            return path === this.root && !this.location.search;

        },



        // Gets the true hash value. Cannot use location.hash directly due to bug

        // in Firefox where location.hash will always be decoded.

        getHash: function(window) {

            var match = (window || this).location.href.match(/#(.*)$/);

            return match ? match[1] : '';

        },



        // Get the pathname and search params, without the root.

        getPath: function() {

            var path = decodeURI(this.location.pathname + this.location.search);

            var root = this.root.slice(0, -1);

            if (!path.indexOf(root)) path = path.slice(root.length);

            return path.slice(1);

        },



        // Get the cross-browser normalized URL fragment from the path or hash.

        getFragment: function(fragment) {

            if (fragment == null) {

                if (this._hasPushState || !this._wantsHashChange) {

                    fragment = this.getPath();

                } else {

                    fragment = this.getHash();

                }

            }

            //var routeStripper = /^[#\/]|\s+$/g;

            return fragment.replace(routeStripper, '');

        },

        // Start the hash change handling, returning `true` if the current URL matches

        // an existing route, and `false` otherwise.

        start: function(options) {

            if (History.started) throw new Error("Backbone.history has already been started");

            History.started = true;



            // Figure out the initial configuration. Do we need an iframe?

            // Is pushState desired ... is it available?

            this.options          = _.extend({root: '/'}, this.options, options);

            this.root             = this.options.root;

            this._wantsHashChange = this.options.hashChange !== false;

            this._hasHashChange   = 'onhashchange' in window;

            this._wantsPushState  = !!this.options.pushState;

            this._hasPushState    = !!(this.options.pushState && this.history && this.history.pushState);

            this.fragment         = this.getFragment();



            // Add a cross-platform `addEventListener` shim for older browsers.

            var addEventListener = window.addEventListener || function (eventName, listener) {

                return attachEvent('on' + eventName, listener);

            };



            // Normalize root to always include a leading and trailing slash.

            // var routeStripper = /^[#\/]|\s+$/g;

            this.root = ('/' + this.root + '/').replace(rootStripper, '/');



            // Proxy an iframe to handle location events if the browser doesn't

            // support the `hashchange` event, HTML5 history, or the user wants

            // `hashChange` but not `pushState`.

            if (!this._hasHashChange && this._wantsHashChange && (!this._wantsPushState || !this._hasPushState)) {

                var iframe = document.createElement('iframe');

                iframe.src = 'javascript:0';

                iframe.style.display = 'none';

                iframe.tabIndex = -1;

                var body = document.body;

                // Using `appendChild` will throw on IE < 9 if the document is not ready.

                this.iframe = body.insertBefore(iframe, body.firstChild).contentWindow;

                this.navigate(this.fragment);

            }



            // Depending on whether we're using pushState or hashes, and whether

            // 'onhashchange' is supported, determine how we check the URL state.

            if (this._hasPushState) {

                addEventListener('popstate', this.checkUrl, false);

            } else if (this._wantsHashChange && this._hasHashChange && !this.iframe) {

                addEventListener('hashchange', this.checkUrl, false);

            } else if (this._wantsHashChange) {

                this._checkUrlInterval = setInterval(this.checkUrl, this.interval);

            }            

        },



        // Add a route to be tested when the fragment changes. Routes added later

        // may override previous routes.

        route: function(route, callback) {

            this.handlers.unshift({route: route, callback: callback});

        },        



        // Checks the current URL to see if it has changed, and if it has,

        // calls `loadUrl`, normalizing across the hidden iframe.

        checkUrl: function(e) {

            var current = this.getFragment();

            if (current === this.fragment && this.iframe) {

                current = this.getHash(this.iframe);

            }

            if (current === this.fragment) return false;

            if (this.iframe) this.navigate(current);

            this.loadUrl();

        },        

        // Attempt to load the current URL fragment. If a route succeeds with a

        // match, returns `true`. If no defined routes matches the fragment,

        // returns `false`.

        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;

                }

            });

        },

        // Save a fragment into the hash history, or replace the URL state if the

        // 'replace' option is passed. You are responsible for properly URL-encoding

        // the fragment in advance.

        //

        // The options object can contain `trigger: true` if you wish to have the

        // route callback be fired (not usually desirable), or `replace: true`, if

        // you wish to modify the current URL without adding an entry to the history.

        navigate: function(fragment, options) {

            if (!History.started) return false;

            if (!options || options === true) options = {trigger: !!options};



            var url = this.root + (fragment = this.getFragment(fragment || ''));



            // Strip the hash for matching.

            // var pathStripper = /#.*$/;

            fragment = fragment.replace(pathStripper, '');



            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);



            // If pushState is available, we use it to set the fragment as a real URL.

            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) {

                this._updateHash(this.location, fragment, options.replace);

                if (this.iframe && (fragment !== this.getHash(this.iframe))) {

                    // Opening and closing the iframe tricks IE7 and earlier to push a

                    // history entry on hash-tag change.  When replace is true, we don't

                    // want this.

                    if(!options.replace) this.iframe.document.open().close();

                    this._updateHash(this.iframe.location, fragment, options.replace);

                }



            // If you've told us that you explicitly don't want fallback hashchange-

            // based history, then `navigate` becomes a page refresh.

            } else {

                return this.location.assign(url);

            }

            if (options.trigger) return this.loadUrl(fragment);

        },

        // Update the hash location, either replacing the current entry, or adding

        // a new one to the browser history.

        _updateHash: function(location, fragment, replace) {

            if (replace) {

                var href = location.href.replace(/(javascript:|#).*$/, '');

                location.replace(href + '#' + fragment);

            } else {

                // Some browsers require that `hash` contains a leading #.

                location.hash = '#' + fragment;

            }

        }

    });



    Backbone.history = new History;

//Backbone.history.start()

//Backbone.history.navigate



    // Backbone.Router

    // ---------------



    // Routers map faux-URLs to actions, and fire events when routes are

    // matched. Creating a new one sets its `routes` hash, if not set statically.

    var Router = Backbone.Router = function(options) {

        options || (options = {});

        if (options.routes) this.routes = options.routes;

        this._bindRoutes();

        this.initialize.apply(this, arguments);

    };



    // Cached regular expressions for matching named param parts and splatted

    // parts of route strings.

    var optionalParam = /\((.*?)\)/g;

    var namedParam    = /(\(\?)?:\w+/g;

    var splatParam    = /\*\w+/g;

    var escapeRegExp  = /[\-{}\[\]+?.,\\\^$|#\s]/g;



    // Set up all inheritable **Backbone.Router** properties and methods.

    _.extend(Router.prototype, Backbone.Events, {

        // Initialize is an empty function by default. Override it with your own

        // initialization logic.

        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;

            Backbone.history.route(route, function(fragment) {

                var args = router._extractParameters(route, fragment);

                if (router.execute(callback, args, name) !== false) {

                    router.trigger.apply(router, ['route:' + name].concat(args));

                    router.trigger('route', name, args);

                    Backbone.history.trigger('route', router, name, args);

                }

            });

            return this;            

        },

        // Convert a route string into a regular expression, suitable for matching

        // against the current location hash.

        _routeToRegExp: function(route) {

            route = route.replace(escapeRegExp, '\\$&')  //把正则里面需要转移的字符进行转移

                .replace(optionalParam, '(?:$1)?')       //把捕获变成非捕获 并且变成惰性匹配

                .replace(namedParam, function(match, optional) {

                    return optional ? match : '([^/?]+)';

                })                                      //如果是:\w+格式转化成([^/?]+)  如果是非捕获格式(?: 则不进行转换

                .replace(splatParam, '([^?]*?)');       //把这种*\w+格式替换成  ([^?]*?)

            return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$');

        },



        // Simple proxy to `Backbone.history` to save a fragment into the history.

        navigate: function(fragment, options) {

            Backbone.history.navigate(fragment, options);

            return this;

        },

        // Execute a route handler with the provided parameters.  This is an

        // excellent place to do pre-route setup or post-route cleanup.

        execute: function(callback, args, name) {

            if (callback) callback.apply(this, args);

        },        

        // Bind all defined routes to `Backbone.history`. We have to reverse the

        // order of the routes here to support behavior where the most general

        // routes can be defined at the bottom of the route map.

        _bindRoutes: function() {

            if (!this.routes) return;

            this.routes = _.result(this, 'routes');

            var route, routes = _.keys(this.routes);

            while ((route = routes.pop()) != null) {

                this.route(route, this.routes[route]);

            }

        },



        // Given a route, and a URL fragment that it matches, return the array of

        // extracted decoded parameters. Empty or unmatched parameters will be

        // treated as `null` to normalize cross-browser behavior.

        _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;

            });

        }



    });



    Backbone.Router.extend = function(protoProps, staticProps) {

        var parent = this;

        var child;



        // The constructor function for the new subclass is either defined by you

        // (the "constructor" property in your `extend` definition), or defaulted

        // by us to simply call the parent's constructor.

        if (protoProps && _.has(protoProps, 'constructor')) {

            child = protoProps.constructor;

        } else {

            child = function(){ return parent.apply(this, arguments); };

        }



        // Add static properties to the constructor function, if supplied.

        //将静态方法和 parent上的静态方法一起扩展到child上面去

        _.extend(child, parent, staticProps);



        // Set the prototype chain to inherit from `parent`, without calling

        // `parent`'s constructor function.

        //创建一个新的构造含糊Surrogate ; 

        //this.constructor = child的意思是  Surrogate实例化后的对象  让对象的构造函数指向child

        // Surrogate的原型就是parent的原型

        // 然后实例化给child的原型,

        // 这里不是直接从new parent给child.prototype 而是创建一个新的构造函数,我也不知道为啥要这样

        var Surrogate = function(){ this.constructor = child; };

        Surrogate.prototype = parent.prototype;

        child.prototype = new Surrogate;



        // Add prototype properties (instance properties) to the subclass,

        // if supplied.

        // 把第一个参数上的属性扩展到child.prototype

        if (protoProps) _.extend(child.prototype, protoProps);



        // Set a convenience property in case the parent's prototype is needed

        // later.

        // 拿一个属性引用父的原型, 以免以后要用到.

        child.__super__ = parent.prototype;



        return child;

    }

})();

</script>

</body>

</html>
View Code

 

 

history怎么实现一个页面里面让浏览器前进后退了?用到了以下的方法

onhashchange   pushstate

onhashchange  给window绑定onhashchange事件,当描点变化的时候,触发事件,然后就可以改变页面了

但onhashchange是从ie8开始能够支持,如果要做le6,7的兼容则必须用其他的办法,backbone的实现是创建一个隐藏的iframe,同时改变浏览器的url上的锚点和iframe的锚点,用一个定时器不停的监听浏览器url的变化

pushstate,html5里面新方法,给window绑定onpopstate方法,查看history.state的值,如果自己通过history.pushState改变了history.state的值,就当做该url之前已经存在,如果history.state是undefined,表示是新地址,要用history.pushState加到history里面去。

下面3个小demo分别是实现用onhashchange,pushstate和隐藏iframe实现浏览器前进和后退

 

onhashchange方式

<!DOCTYPE html>

<html>

<head>

    <meta charset="utf-8"/>

    <title>Lottery Demo</title>

    <style type="text/css">

        *{padding:0;margin:0;}



    </style>

</head>

<body>

    <div id="wrap"></div>

    <script type="text/javascript" src="jquery.js"></script>

    <script type="text/javascript">



        var urlHash = {

            "one"   : "我是第一页",

            "two"   : "我是第二页",

            "three" : "我是第三页"

        } 

        function c(){

            var hash = location.hash.replace("#","");

            if(hash in urlHash){

                $("#wrap").html(urlHash[hash]);

            }else{

                $("#wrap").html("该锚点没有对应的页面");

            }

        }

        function n(fragment){

            location.hash = fragment;

        }

        window.attachEvent

            ? window.attachEvent('onhashchange', c)

            : window.addEventListener("hashchange",c,false); 

        n("one");

        //用n函数跳转描点

    </script>

</body>

</html>

 

pushstate

<!DOCTYPE html>

<html>

<head>

    <meta charset="utf-8"/>

    <title>Lottery Demo</title>

    <style type="text/css">

        *{padding:0;margin:0;}



    </style>

</head>

<body>

    <div id="wrap"></div>

    <script type="text/javascript" src="jquery.js"></script>

    <script type="text/javascript">



        var urlHash = {

            "one"   : "我是第一页",

            "two"   : "我是第二页",

            "three" : "我是第三页"

        } 

        function n(fragment){

            //history.pushState({lk:fragment,text:fragment}, "", fragment);

            var text =urlHash[fragment] || "没有对应的页面";

            history.pushState({lk:fragment,text:text}, "", fragment);

            $("#wrap").html(history.state.text);

        }

        window.addEventListener("popstate",function(){

            if(history.state){

                $("#wrap").html(history.state.text);

            }

        },false);

        n("one");

        //用n函数跳转描点

    </script>

</body>

</html>

 

用iframe的方式

<!DOCTYPE html>

<html>

<head>

    <meta charset="utf-8"/>

    <title>Lottery Demo</title>

    <style type="text/css">

        *{padding:0;margin:0;}



    </style>

</head>

<body>

    <div id="wrap"></div>

    <script type="text/javascript" src="jquery.js"></script>

    <script type="text/javascript">



        var urlHash = {

            "one"   : "我是第一页",

            "two"   : "我是第二页",

            "three" : "我是第三页"

        }



        var iframe = document.createElement('iframe') 

        iframe.src = 'javascript:0';

        iframe.style.display = 'none';

        iframe.tabIndex = -1;

        var iframeWindow = document.body.insertBefore(iframe, document.body.firstChild).contentWindow;

        var currFragment = null;



        function n(fragment){

            if(currFragment == fragment || fragment=="")return;

            currFragment = fragment;

            window.location.hash = '#' + fragment;

            window.iframeWindow.document.open().close();

            window.iframeWindow.location.hash = '#' + fragment;

            var text =urlHash[fragment] || "没有对应的页面";

            $("#wrap").html(text); 

        }



        

        setInterval(function(){

            var fragment = window.location.hash.replace("#","")

            n(fragment)

        }, 50);     

        //用n函数跳转描点

    </script>

</body>

</html>

 

Backbone.History

Backbone.History是一个类,Backbone.history才是Backbone.History实例化后的对象,即Backbone.history = new Backbone.History;

Backbone.History有以下比较重要的方法

start

要启动Backbone的路由功能,首先必须调用Backbone.history.start(),start方法做了以下一些事情 

首页设置 History.started=true 表示该路由功能已经启动

然后看当前浏览器是否支持onhashChange,参数入的参数是希望通过hash的方式还是希望通过pushstate的方式改变url

根据传入的root来设置url路径将怎么改变,默认以当前的路径

如果浏览器不支持onhashchange(不支持hashchange肯定也不会支持pushstate的,因为onhashchange出现的时候,还没有出现pushstate),创建一个iframe

如果是pushstate方式 绑定window的popstate事件去监听url改变

如果是hashchange方式 绑定window的hashchange事件去监听url改变

如果不支持onhashchange,则设置一个定时器为监听url的改变

var r = Backbone.history;

r.navigate("one",true);//这里锚点是不会改变的 因为没有调用start方法

r.start();

r.navigate("one",true);//锚点可以改变了

 

getFragment

获取当前的锚点或者是地址(地址,暂且这么说吧,因为pushstate方式就是后面那串地址不同)

 

route    (reg,callback)

添加对路由的监听事件,第一个参数是个正则,第2个参数是回调函数。以{route: route, callback: callback}的形式存在this.handlers中,当路由改变的时候,会遍历this.handlers,如果符合其中的route正则,则会执行相关回调

var r = Backbone.history;

r.route(/^a/,function(){alert("a")}); //监听描点如果是以a开头,则弹出a

r.route(/^[^a]/,function(){alert("not a")}) //监听描点如果不是以a开头,则弹出not a

r.start()

r.navigate("aasd/asdf/ss",true)

r.navigate("bxx/asdf/ss",true)

 

navigate  (fragment, [options])

第一个参数个地址,需要进入的地址,第2个参数可以是对象,也可以是布尔值,true,表示路由要改变,如果有对应的回调事件也要执行,false表示不需要执行对应的回调事件,第2个参数当是对象的时候,trigger值得意义和布尔值的时候一样,replace值表示修改路由,但是不触发相应的回调事件,replace是不会记录在history里面,返回的时候不会有替换之前的那个地址.在然后改变url地址,设置了pushstate的用pushstate方式,支持onhashchang的直接用location.href来改变描点,不支持onhashchange的,除了改变location.href还要改变iframe的location.href。最后在触发相关监听路由的回调事件

在调用navigate方法的时候就会调用一次回调事件了,监听的时候也会调用一次回调事件的,所以在监听的时候会判断是不是同一个路由,同一个就不执行回调了,监听执行额回调事件也是在浏览器的前进和后退的时候在会执行,只用调用navigate,监听是不会执行回调的,是navigate方法里面主动调用的

一些例子

要启用路由功能一定要先调用start

var r = Backbone.history;

r.start();

r.navigate("aaa",true)

//r.navigate("aaa",{trigger:true})  这种方式和上面那种方式是一样的

 

第二个参数如果设置成false,路由会改变,但是监听函数是不会触发的

var r = Backbone.history;

r.start();

r.route(/^a/,function(){alert("a")})

r.route(/^b/,function(){alert("b")})

r.navigate("aaaa",true)  //可以触发监听回调

r.navigate("aaaa",false) //触发不了监听回调

 

如果第二个参数是一个对象,且设置了replace为true,改变路由后,之前的路由是不会记录在history里面的,而且监听路由的回调也不会执行

var r = Backbone.history;

r.start();

r.route(/a^/,function(){alert(1)})

r.navigate("as",{trigger:true,replace:true}) //监听路由的回调是不会执行的,且history中不会记录之前的路由

所以第二个参数false和replace的却别就在于histroy中是否记录之前的路由,他们的相同点是都不会触发监听回调

 

Backbone.Router

Backbone.Router基本上对Backbone.history一些方法的封装了.

route router.route(route, name, [callback])

route方法就是添加路由,路由对应的回调,他有3个参数,第一个参数就是路由了,第2个参数是方法名,如果传递了,当匹配域名的时候就会从Router这个对象里面去掉用该方法名,第3个参数也是回调,不设置第2个参数的时候就会调用第3个参数的回调

route的执行过程如下,首先判断route参数是不是正则表达式,如果不是则转成正则表达式,转换过程下

 

首先,把字符串route里面需要转义的字符全部转义,转义规则如下

                var escapeRegExp  = /[\-{}\[\]+?.,\\\^$|#\s]/g;

                route = route.replace(escapeRegExp, '\\$&');

需要转义的字符有\,-,{,},[,],+,.,\,?  

然后把捕获变成非捕获,并且变成惰性匹配,因为这个正则后面要用到的是test方法,根本就不需要捕获,捕获会耗去更多的内存.

            var optionalParam = /\((.*?)\)/g;

            route = route.replace(optionalParam, '(?:$1)?')

然后在把 :\w+格式 转化成([^/?]+),路由上面的传参数的格式/:xxx,这个主要是把参数转换成([^/?]+),这个这则的意思就是不能是/和?的任意字符,因为参数的规则就是route/:arguments,如果带了/可能就路径错了,如果有?那就可能是url的参数

            var namedParam    = /(\(\?)?:\w+/g;            

            route = route.replace(namedParam, function(match, optional) {

                return optional ? match : '([^/?]+)';

            })  

在然后 把点*以及*后面的字符串转化成([^?]*?),惰性匹配非?字符串 这个替换是干嘛用的还没看懂

            var splatParam = /\*\w+/g;

            route = route.replace(splatParam, '([^?]*?)'); 

最后给route字符串加上开头的符号和结尾的符号'^' + route + '(?:\\?([\\s\\S]*))?$',意思是说开头必须也route开头,后面可以跟参数,然后实例化这个字符串为正则对象并且返回

处理完正则之后,通过Backbone.history.route方法来监听该路由正则,如果在前进后退的时候路由匹配该正则就是执行回调

 

 

 

navigate (fragment, [options])

这个直接调用的Backbone.history.navigate(fragment, options);

var r = new Backbone.Router;

r.route("aa/:a/:a",function(){

    alert(Array.prototype.join.apply(arguments))

})

Backbone.history.start()

r.navigate("aa/bb/cc",true)

//传入bb cc 两个参数,可以打印出来

 

 

routes _bindRoutes

如果设置了routes,可以用_bindRoutes一次性绑定对routes里面的路由监听,_bindRoutes里面就是遍历routes,调用this,route进行绑定

Backbone.Router = Backbone.Router.extend({

    alertA : function(){alert("a")}

})

var r = new Backbone.Router({

    routes : {

        "aa" : "alertA",

        "bb" : function(){alert("b")},

    }

})

Backbone.history.start()

//先扩展alertA方法   然后通过routes批量绑定,如果value对应的是字符串,则会在实例化的r上找该方法,如果是函数就执行该函数

r.navigate("aa",true);

r.navigate("bb",true)

 

你可能感兴趣的:(backbone)