RubyLouvre / mmRouter

avalon的三柱臣之一( 路由,动画,AJAX)
119 stars 78 forks source link

mmRouter2 #10

Closed RubyLouvre closed 10 years ago

RubyLouvre commented 10 years ago
define(["mmHistory"], function(avalon) {

    function Router() {
        this.routingTable = {};
    }
    function parseQuery(path) {
        var array = path.split("#"), query = {}, tail = array[1];
        if (tail) {
            var index = tail.indexOf("?");
            if (index > 0) {
                var seg = tail.slice(index + 1).split('&'),
                        len = seg.length, i = 0, s;
                for (; i < len; i++) {
                    if (!seg[i]) {
                        continue;
                    }
                    s = seg[i].split('=');
                    query[decodeURIComponent(s[0])] = decodeURIComponent(s[1]);
                }
            }
        }
        return {
            pathname: array[0],
            query: query
        };
    }

    var optionalParam = /\((.*?)\)/g
    var namedParam = /(\(\?)?:\w+/g
    var splatParam = /\*\w+/g
    var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g
    Router.prototype = {
        error: function(callback) {
            this.errorback = callback
        },
        _pathToRegExp: function(path, params) {
            path = path.replace(escapeRegExp, '\\$&')
                    .replace(optionalParam, '(?:$1)?')
                    .replace(namedParam, function(match, optional) {
                        params.push(match.slice(1))
                        return optional ? match : '([^/?]+)'
                    })
                    .replace(splatParam, '([^?]*?)');
            return new RegExp('^' + path + '(?:\\?([\\s\\S]*))?$')
        },
        //添加一个路由规则
        add: function(method, path, callback) {
            var array = this.routingTable[method]
            if (!array) {
                array = this.routingTable[method] = []
            }
            var regexp = path, params = []
            if (avalon.type(path) !== "regexp") {
                regexp = this._pathToRegExp(regexp, params)
            }
            array.push({
                value: callback,
                regexp: regexp,
                params: params
            })
        },
        routeWithQuery: function(method, path) {
            var parsedUrl = parseQuery(path),
                    ret = this.route(method, parsedUrl.pathname);
            if (ret) {
                ret.query = parsedUrl.query;
                return ret;
            }
        },
        _extractParameters: function(route, path) {
            var array = route.regexp.exec(path) || []
            array = array.slice(1)
            var args = [], params = {}
            var n = route.params.length
            for (var i = 0; i < n; i++) {
                args[i] = decodeURIComponent(array[i])
                args[ route.params[i] || i  ] = args[i]
            }
            return {
                query: {},
                value: route.value,
                args: args,
                params: params,
                path: path
            }
        },
        route: function(method, path) {//判定当前URL与预定义的路由规则是否符合
            path = path.trim()
            var array = this.routingTable[method]
            if (array) {
                for (var i = 0, el; el = array[i++]; ) {
                    if (el.regexp.test(path)) {
                        return this._extractParameters(el, path)
                    }
                }
            }
        },
        getLastPath: function() {
            return getCookie("msLastPath")
        },
        setLastPath: function(path) {
            setCookie("msLastPath", path)
        },
        navigate: function(url) {//传入一个URL,触发预定义的回调
            var match = this.routeWithQuery("GET", url);
            if (match) {
                var fn = match.value;
                if (typeof fn === "function") {
                    return  fn.apply(match, match.args);
                }
            } else if (typeof this.errorback === "function") {
                this.errorback(url)
            }
        }
    };
    Router.prototype.getLatelyPath = Router.prototype.getLastPath
    Router.prototype.setLatelyPath = Router.prototype.setLastPath

    "get,put,delete,post".replace(avalon.rword, function(method) {
        return  Router.prototype[method] = function(path, fn) {
            return this.add(method.toUpperCase(), path, fn)
        }
    })
    function supportLocalStorage() {
        try {
            return 'localStorage' in window && window['localStorage'] !== null;
        } catch (e) {
            return false;
        }
    }
    if (supportLocalStorage()) {
        Router.prototype.getLatelyPath = function() {
            return localStorage.getItem("msLastPath")
        }
        Router.prototype.setLatelyPath = function(path) {
            localStorage.setItem("msLastPath", path)
        }
    }

    function escapeCookie(value) {
        return String(value).replace(/[,;"\\=\s%]/g, function(character) {
            return encodeURIComponent(character);
        });
    }
    function setCookie(key, value) {
        var date = new Date();//将date设置为10天以后的时间 
        date.setTime(date.getTime() + 60 * 60 * 24);
        document.cookie = escapeCookie(key) + '=' + escapeCookie(value) + ";expires=" + date.toGMTString()
    }
    function getCookie(name) {
        var result = {};
        if (document.cookie !== '') {
            var cookies = document.cookie.split('; ')
            for (var i = 0, l = cookies.length; i < l; i++) {
                var item = cookies[i].split('=');
                result[decodeURIComponent(item[0])] = decodeURIComponent(item[1]);
            }
        }
        return name ? result[name] : result
    }

    avalon.router = new Router
    // 先添加路由规则与对应的处理函数
    // router.add("GET","/aaa", function(){}) //{GET:{1:{aaa: function(){}}}}
    // router.add("GET","/aaa/bbb", function(){}) //{GET:{1:{aaa:{bbb: function(){}} }}}
    // router.add("GET","/aaa/:bbb", function(){}) //{GET:{1:{aaa: {"^n": "bbb", "^v": function(){}}}}}
    // router.add("GET","/aaa(/:bbb)", function(){}) //{GET:{1:{aaa: {"^n": "bbb", "^v": function(){}}}}}
    // 再启动历史管理器
    // require("ready!", function(avalon){
    //     avalon.history.start();
    // })

    return avalon
})
// http://kieran.github.io/barista/
// https://github.com/millermedeiros/crossroads.js/wiki/Examples
RubyLouvre commented 10 years ago

这是backbone的改写版

define(["avalon"], function(avalon) {
    avalon.bindingHandlers.view = function(data, vmodels) {
        var first = vmodels[0]
        data.element.innerHTML = "ddddddddddd"
        first.$watch("routeChangeStart", function() {
            data.element.innerHTML = new Date - 0
        })
    }

    var IEVersion = (function() {
        var mode = document.documentMode
        return mode ? mode : window.XMLHttpRequest ? 7 : 6
    })()
    var oldIE = window.VBArray && IEVersion <= 7
    var supportPushState = !!(window.history.pushState)
    var supportHashChange = !!('onhashchange' in window && (!window.VBArray || !oldIE))

    var History = function() {
        this.handlers = [];

        // Ensure that `History` can be used outside of the browser.
        if (typeof window !== 'undefined') {
            this.location = window.location;
            this.history = window.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 = /#.*$/;

    // Has the history handling already been started?
    History.started = false;

    // Set up all inheritable **Backbone.History** properties and methods.
    avalon.mix(History.prototype, {
        // 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] : '';
        },
        fireRouteChange: function(fragment) {
            var vs = avalon.vmodels
            for (var v in vs) {
                vs[v].$fire("routeChangeStart", fragment)
            }
        },
        // 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();
                }
            }
            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('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 = avalon.mix({root: '/'}, this.options, options);
            this.root = this.options.root;
            this._wantsHashChange = this.options.hashChange !== false;
            this._hasHashChange = supportHashChange
            this._wantsPushState = !!this.options.pushState
            this._hasPushState = !!(this.options.pushState && supportPushState);
            this.fragment = this.getFragment();

            // Normalize root to always include a leading and trailing slash.
            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) {
                this._checkUrl = avalon.bind(window, 'popstate', this.checkUrl);
                this.monitorMode = "popstate"
            } else if (this._wantsHashChange && this._hasHashChange && !this.iframe) {
                this._checkUrl = avalon.bind(window, 'hashchange', this.checkUrl);
                this.monitorMode = "hashchange"
            } else if (this._wantsHashChange) {
                this.monitorMode = "iframepool"
                this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
            }
            avalon.log(this)
            // Transition from hashChange to pushState or vice versa if both are
            // requested.
            if (this._wantsHashChange && this._wantsPushState) {

                // If we've started off with a route from a `pushState`-enabled
                // browser, but we're currently in a browser that doesn't support it...
                if (!this._hasPushState && !this.atRoot()) {
                    avalon.log("this.location.replace "+this.getPath())
                    this.location.replace(this.root + '#' + this.getPath());
                    // Return immediately as browser will do redirect to new url
                    return true;

                    // Or if we've started out with a hash-based route, but we're currently
                    // in a browser where it could be `pushState`-based instead...
                } else if (this._hasPushState && this.atRoot()) {
                    this.navigate(this.getHash(), {replace: true});
                }

            }

            if (!this.options.silent)
                return this.fireRouteChange();
        },
        stop: function() {
            //移除之前绑定的popstate/hashchange事件
            if (this._hasPushState) {
                avalon.unbind(window, 'popstate', this._checkUrl);
            } else if (this._wantsHashChange && this._hasHashChange && !this.iframe) {
                avalon.unbind(window, 'hashchange', this._checkUrl);
            }
            // 移除之前动态插入的iframe
            if (this.iframe) {
                document.body.removeChild(this.iframe.frameElement);
                this.iframe = null;
            }
            // 中断轮询
            if (this._checkUrlInterval)
                clearInterval(this._checkUrlInterval);
            History.started = false;
        },
        //用于添加路则规则及对应的回调
        route: function(route, callback) {
            this.handlers.unshift({route: route, callback: callback});
        },
        //比较前后的路径或hash是否发生改变,如果发生改变则调用navigate与fireRouteChange方法
        checkUrl: function(e) {
            var that = avalon.history
            var current = that.getFragment();
            if (current === that.fragment && that.iframe) {
                current = that.getHash(that.iframe);
            }
            if (current === that.fragment)
                return false;
            if (that.iframe)
                that.navigate(current);
            that.fireRouteChange();
        },
//    规则中的*(星号)会在Router内部被转换为表达式(.*?),表示零个或多个任意字符,
//    与:(冒号)规则相比,*(星号)没有/(斜线)分隔的限制,就像我们在上面的例子中定义的*error规则一样。
//  Router中的*(星号)规则在被转换为正则表达式后使用非贪婪模式,因此你可以使用例如这样的组合规则
//  :*type/:id,它能匹配#hot/1023,同时会将hot和1023作为参数传递给Action方法。
        //此方法用于修改地址栏的可变部分(IE67还负责产生新历史)
        navigate: function(fragment, options) {
            if (!History.started)
                return false;
            if (!options || options === true)
                options = {trigger: !!options};

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

            fragment = decodeURI(fragment.replace(pathStripper, ''));
            //fragment就是路由可变动的部分,被decodeURI过的
            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,那么就使用replaceState,pushState,API
            //注意replace是不会产生历史
            if (this._hasPushState) {
                this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url)
            } else if (this._wantsHashChange) {
                //更新当前页面的地址栏的hash值
                this._updateHash(this.location, fragment, options.replace)
                if (this.iframe && (fragment !== this.getHash(this.iframe))) {
                    //在IE67下需要通过创建或关闭一个iframe来产生历史
                    if (!options.replace)
                        this.iframe.document.open().close()
                    //更新iframe的地址栏的hash值
                    this._updateHash(this.iframe.location, fragment, options.replace)
                }
            } else {
                //直接刷新页面
                return this.location.assign(url)
            }
            if (options.trigger)
                return this.fireRouteChange(fragment)
        },
        //更新hash
        _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;
            }
        }

    });

    // Create the default Backbone.history.
    avalon.history = new History;
    return avalon
})
RubyLouvre commented 10 years ago

这是backbone的深度改写版,简化配置,与angular靠近

define(["avalon"], function(avalon) {
    avalon.bindingHandlers.view = function(data, vmodels) {
        var first = vmodels[0]
        data.element.innerHTML = "&nbsp;"
        first.$watch("routeChangeStart", function(fragment) {
            data.element.innerHTML = fragment || new Date - 0
        })
    }

    var IEVersion = (function() {
        var mode = document.documentMode
        return mode ? mode : window.XMLHttpRequest ? 7 : 6
    })()
    var oldIE = window.VBArray && IEVersion <= 7
    var supportPushState = !!(window.history.pushState)
    var supportHashChange = !!('onhashchange' in window && (!window.VBArray || !oldIE))

    var History = function() {
        this.handlers = []
        this.started = false
        this.location = window.location
        this.history = window.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 = /#.*$/

    avalon.mix(History.prototype, {
        interval: 50,
        atRoot: function() {
            var path = this.location.pathname.replace(/[^\/]$/, '$&/')
            return path === this.root && !this.location.search
        },
        getHash: function(window) {
            var match = (window || this).location.href.match(/#(.*)$/)
            return match ? match[1] : ''
        },
        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)
        },
        //根据monitorMode调用getPath或getHash
        getFragment: function(fragment) {
            if (fragment == null) {
                if (this.monitorMode === "popstate") {
                    fragment = this.getPath();
                } else {
                    fragment = this.getHash();
                }
            }
            return fragment.replace(routeStripper, "")
        },
        start: function(options) {
            if (options === true) {
                options = {
                    html5Mode: true
                }
            }
            if (this.started)
                throw new Error('history has already been started')
            this.started = true
            this.options = avalon.mix({root: '/'}, this.options, options)
            this.root = this.options.root
            this.supportPushState = supportPushState
            this.supportHashChange = supportHashChange
            this.monitorMode = this.options.html5Mode ? "popstate" : "hashchange"

            if (!this.supportPushState) {
                this.monitorMode = "hashchange"
            }
            if (!this.supportHashChange) {
                this.monitorMode = "iframepool"
            }

            this.fragment = this.getFragment()
            avalon.log("start monitoring")
            //确认前后都存在斜线, 如"aaa/ --> /aaa/" , "/aaa --> /aaa/", "aaa --> /aaa/", "/ --> /"
            this.root = ('/' + this.root + '/').replace(rootStripper, '/');
            //   alert(this.root)
            // 支持popstate 就监听popstate
            // 支持hashchange 就监听hashchange
            // 否则的话只能每隔一段时间进行检测了
            switch (this.monitorMode) {
                case "popstate":
                    this._checkUrl = avalon.bind(window, 'popstate', this.checkUrl)
                    break
                case  "hashchange":
                    this._checkUrl = avalon.bind(window, 'hashchange', this.checkUrl);
                    break
                case "iframepool":
                    var iframe = document.createElement('iframe');
                    iframe.src = 'javascript:0'
                    iframe.style.display = 'none'
                    iframe.tabIndex = -1
                    this.iframe = true
                    var body = document.body || document.documentElement;
                    this.iframe = body.insertBefore(iframe, body.firstChild).contentWindow;
                    this.navigate(this.fragment);
                    avalon.ready(function() {
                        body.appendChild(iframe).contentWindow;
                    })
                    this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
                    break;
            }

            if (!this.options.silent) {
                this.fireRouteChange()
            }
        },
        stop: function() {
            //移除之前绑定的popstate/hashchange事件
            switch (this.monitorMode) {
                case "popstate":
                    avalon.unbind(window, "popstate", this._checkUrl)
                    break
                case "popstate":
                    avalon.unbind(window, "hashchange", this._checkUrl)
                    break
                case  "iframepool":
                    // 移除之前动态插入的iframe
                    if (this.iframe) {
                        document.body.removeChild(this.iframe.frameElement)
                        this.iframe = null
                    }
                    clearInterval(this._checkUrlInterval)// 中断轮询
                    break;
            }
            this.started = false
        },
        //用于添加路则规则及对应的回调
        route: function(route, callback) {
            this.handlers.unshift({route: route, callback: callback})
        },
        //比较前后的路径或hash是否发生改变,如果发生改变则调用navigate与fireRouteChange方法
        checkUrl: function() {
            var that = avalon.history
            var current = that.getFragment();
            if (current === that.fragment && that.iframe) {
                current = that.getHash(that.iframe)
            }
            if (current === that.fragment)
                return false;
            if (that.iframe)
                that.navigate(current)
            avalon.log("checkUrl")
            that.fireRouteChange(current)
        },
        fireRouteChange: function(fragment) {
            var vs = avalon.vmodels
            for (var v in vs) {
                vs[v].$fire("routeChangeStart", fragment)
            }
        },
//    规则中的*(星号)会在Router内部被转换为表达式(.*?),表示零个或多个任意字符,
//    与:(冒号)规则相比,*(星号)没有/(斜线)分隔的限制,就像我们在上面的例子中定义的*error规则一样。
//  Router中的*(星号)规则在被转换为正则表达式后使用非贪婪模式,因此你可以使用例如这样的组合规则
//  :*type/:id,它能匹配#hot/1023,同时会将hot和1023作为参数传递给Action方法。
        //此方法用于修改地址栏的可变部分(IE67还负责产生新历史)
        navigate: function(fragment, options) {
            if (!this.started)
                return false;
            if (!options || options === true)
                options = {trigger: !!options};

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

            fragment = decodeURI(fragment.replace(pathStripper, ''));
            //fragment就是路由可变动的部分,被decodeURI过的
            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,那么就使用replaceState,pushState,API
            //注意replace是不会产生历史
            if (this._hasPushState) {
                this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url)
            } else if (this._wantsHashChange) {
                //更新当前页面的地址栏的hash值
                this._updateHash(this.location, fragment, options.replace)
                if (this.iframe && (fragment !== this.getHash(this.iframe))) {
                    //在IE67下需要通过创建或关闭一个iframe来产生历史
                    if (!options.replace)
                        this.iframe.document.open().close()
                    //更新iframe的地址栏的hash值
                    this._updateHash(this.iframe.location, fragment, options.replace)
                }
            } else {
                //直接刷新页面
                return this.location.assign(url)
            }
        },
        //更新hash
        _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;
            }
        }

    })

    //创建一个History单例
    avalon.history = new History
    return avalon
})