美文网首页
前端中的 hash 和 history 路由

前端中的 hash 和 history 路由

作者: zhao_ran | 来源:发表于2020-08-28 14:03 被阅读0次

    前端路由有有 hash 路由和 history 路由两种路由方式,他们的原理是什么,又怎样实现一个简单的路由监听呢?

    我们在使用 Vue 或者 React 等前端渲染时,通常会有 hash 路由和 history 路由两种路由方式。

    1.hash 路由:监听 url 中 hash 的变化,然后渲染不同的内容,这种路由不向服务器发送请求,不需要服务端的支持;

    2.history 路由:监听 url 中的路径变化,需要客户端和服务端共同的支持;

    我们一步步实现这两种路由,来深入理解下底层的实现原理。我们主要实现以下几个简单的功能:

    1.监听路由的变化,当路由发生变化时,可以作出动作;

    2.可以前进或者后退;

    3.可以配置路由;

    1. hash 路由

    当页面中的hash发生变化时,会触发hashchange事件,因此我们可以监听这个事件,来判断路由是否发生了变化。

    window.addEventListener(
        'hashchange',
        function (event) {
            const oldURL = event.oldURL; // 上一个URL
            const newURL = event.newURL; // 当前的URL
            console.log(newURL, oldURL);
        },
        false
    );
    

    1.1 实现的过程

    对 oldURL 和 newURL 进行拆分后,就能获取到更详细的 hash 值。我们这里从创建一个 HashRouter 的 class 开始一步步写起:

    class HashRouter {
        currentUrl = ''; // 当前的URL
        handlers = {};
    
        getHashPath(url) {
            const index = url.indexOf('#');
            if (index >= 0) {
                return url.slice(index + 1);
            }
            return '/';
        }
    }
    

    事件hashchange只会在hash 发生变化时才能触发,而第一次进入到页面时并不会触发这个事件,因此我们还需要监听load事件。这里要注意的是,两个事件的event 是不一样的:hashchange事件中的 event对象有 oldURLnewURL两个属性,但 load 事件中的 event 没有这两个属性,不过我们可以通过 location.hash 来获取到当前的 hash 路由:

    class HashRouter {
        currentUrl = ''; // 当前的URL
        handlers = {};
    
        constructor() {
            this.refresh = this.refresh.bind(this);
            window.addEventListener('load', this.refresh, false);
            window.addEventListener('hashchange', this.refresh, false);
        }
    
        getHashPath(url) {
            const index = url.indexOf('#');
            if (index >= 0) {
                return url.slice(index + 1);
            }
            return '/';
        }
    
        refresh(event) {
            let curURL = '',
                oldURL = null;
            if (event.newURL) {
                oldURL = this.getHashPath(event.oldURL || '');
                curURL = this.getHashPath(event.newURL || '');
            } else {
                curURL = this.getHashPath(window.location.hash);
            }
            this.currentUrl = curURL;
        }
    }
    

    到这里已经可以实现获取当前的 hash 路由,但路由发生变化时,我们的页面应该进行切换,因此我们需要监听这个变化:

    class HashRouter {
        currentUrl = ''; // 当前的URL
        handlers = {};
    
        // 暂时省略上面的代码
    
        refresh(event) {
            // 当hash路由发生变化时,则触发change事件
            this.emit('change', curURL, oldURL);
        }
    
        on(evName, listener) {
            this.handlers[evName] = listener;
        }
        emit(evName, ...args) {
            const handler = this.handlers[evName];
            if (handler) {
                handler(...args);
            }
        }
    }
    const router = new HashRouter();
    rouer.on('change', (curUrl, lastUrl) => {
        console.log('当前的hash:', curUrl);
        console.log('上一个hash:', lastUrl);
    });
    

    1.2 调用的方式

    到这里,我们把基本的功能已经完成了。来配合一个例子就更形象了:

    // 先定义几个路由
    const routes = [
        {
            path: '/',
            name: 'home',
            component: <Home />,
        },
        {
            path: '/about',
            name: 'about',
            component: <About />,
        },
        {
            path: '*',
            name: '404',
            component: <NotFound404 />,
        },
    ];
    const router = new HashRouter();
    // 监听change事件
    router.on('change', (currentUrl, lastUrl) => {
        let route = null;
        // 匹配路由
        for (let i = 0, len = routes.length; i < len; i++) {
            const item = routes[i];
            if (currentUrl === item.path) {
                route = item;
                break;
            }
        }
        // 若没有匹配到,则使用最后一个路由
        if (!route) {
            route = routes[routes.length - 1];
        }
        // 渲染当前的组件
        ReactDOM.render(route.component, document.getElementById('app'));
    });
    

    hash 路由的样例

    html代码:

    <div class="container">
        <nav>
            <p><a href="#/">home</a></p>
            <p><a href="#/article">article</a></p>
            <p><a href="#/archive#a=1">archive#a=1</a></p>
            <p><a href="#/about">about</a></p>
        </nav>
        <div id="app"></div>
    </div>
    

    js代码:

    const { useState, useEffect, useMemo } = React;
    
    const Home = () => {
        const [count, setCount] = useState(0);
        const [nowtime, setNowtime] = useState(0);
        const sum = useMemo(() => ((1 + count) * count) / 2 + " ,random: " + Math.random(), [count]);
    
        return (
            <div>
                <h1>Home Page</h1>
                <p> count: {count}</p>
                <p> sum: {sum}</p>
                <p> nowtime: {nowtime}</p>
                <button onClick={() => setCount(count + 1)}> add 1 </button>
                <button onClick={() => setNowtime(Date.now())}> set now time </button>
            </div>
        );
    };
    const About = () => {
        return (
            <div>
                <h1>About Page</h1>
            </div>
        );
    };
    const NotFound404 = () => {
        return (
            <div>
                <h1>404 Page</h1>
            </div>
        );
    };
    // ReactDOM.render(<Home />, document.getElementById("app"));
    const routes = [
        {
            path: "/",
            name: "home",
            component: <Home />,
        },
        {
            path: "/about",
            name: "about",
            component: <About />,
        },
        {
            path: "*",
            name: "404",
            component: <NotFound404 />,
        },
    ];
    const router = new HashRouter();
    router.on("change", (currentUrl, lastUrl) => {
        console.log(currentUrl, lastUrl);
        let route = null;
        for (let i = 0, len = routes.length; i < len; i++) {
            const item = routes[i];
            if (currentUrl === item.path) {
                route = item;
                break;
            }
        }
        if (!route) {
            route = routes[routes.length - 1];
        }
        ReactDOM.render(route.component, document.getElementById("app"));
    });
    

    2. history 路由

    history 路由中,我们一定会使用window.history中的方法,常见的操作有:
    1.back():后退到上一个路由;

    2.forward():前进到下一个路由,如果有的话;

    3.go(number):进入到任意一个路由,正数为前进,负数为后退;

    4.pushState(obj, title, url):前进到指定的 URL,不刷新页面;

    5.replaceState(obj, title, url):用 url 替换当前的路由,不刷新页面;
    调用这几种方式时,都会只是修改了当前页面的 URL,页面的内容没有任何的变化。但前 3 个方法只是路由历史记录的前进或者后退,无法跳转到指定的 URL;而pushStatereplaceState可以跳转到指定的 URL。如果有面试官问起这个问题“如何仅修改页面的 URL,而不发送请求”,那么答案就是这 5 种方法。

    如果服务端没有新更新的 url 时,一刷新浏览器就会报错,因为刷新浏览器后,是真实地向服务器发送了一个 http 的网页请求。因此若要使用 history 路由,需要服务端的支持。

    2.1 应用的场景

    pushStatereplaceState 两个方法跟 location.hreflocation.replace 两个方法有什么区别呢?应用的场景有哪些呢?

    1.location.hreflocation.replace 切换时要向服务器发送请求,而 pushStatereplace 仅修改 url,除非主动发起请求;

    2.仅切换 url 而不发送请求的特性,可以在前端渲染中使用,例如首页是服务端渲染,二级页面采用前端渲染;

    3.可以添加路由切换的动画;

    4.在浏览器中使用类似抖音的这种场景时,用户滑动切换视频时,可以静默修改对应的 URL,当用户刷新页面时,还能停留在当前视频。

    2.2 无法监听路由的变化

    当我们用 history 的路由时,必然要能监听到路由的变化才行。全局有个popstate事件,别看这个事件名称中有个 state 关键词,但pushStatereplaceState被调用时,是不会触发触发 popstate 事件的,只有上面列举的前 3 个方法会触发。可以点击【popState 不会触发 popstate 事件】查看。
    针对这种情况,我们可以使用window.dispatchEvent添加事件:

    const listener = function (type) {
        var orig = history[type];
        return function () {
            var rv = orig.apply(this, arguments);
            var e = new Event(type);
            e.arguments = arguments;
            window.dispatchEvent(e);
            return rv;
        };
    };
    window.history.pushState = listener('pushState');
    window.history.replaceState = listener('replaceState');
    

    然后就可以添加对这两个方法的监听了:

    window.addEventListener('pushState', this.refresh, false);
    window.addEventListener('replaceState', this.refresh, false);
    

    2.3 完整的代码

    class HistoryRouter {
        currentUrl = '';
        handlers = {};
    
        constructor() {
            this.refresh = this.refresh.bind(this);
            this.addStateListener();
            window.addEventListener('load', this.refresh, false);
            window.addEventListener('popstate', this.refresh, false);
            window.addEventListener('pushState', this.refresh, false);
            window.addEventListener('replaceState', this.refresh, false);
        }
        addStateListener() {
            const listener = function (type) {
                var orig = history[type];
                return function () {
                    var rv = orig.apply(this, arguments);
                    var e = new Event(type);
                    e.arguments = arguments;
                    window.dispatchEvent(e);
                    return rv;
                };
            };
            window.history.pushState = listener('pushState');
            window.history.replaceState = listener('replaceState');
        }
        refresh(event) {
            this.currentUrl = location.pathname;
            this.emit('change', location.pathname);
            document.querySelector('#app span').innerHTML = location.pathname;
        }
        on(evName, listener) {
            this.handlers[evName] = listener;
        }
        emit(evName, ...args) {
            const handler = this.handlers[evName];
            if (handler) {
                handler(...args);
            }
        }
    }
    const router = new HistoryRouter();
    router.on('change', function (curUrl) {
        console.log(curUrl);
    });
    

    相关文章

      网友评论

          本文标题:前端中的 hash 和 history 路由

          本文链接:https://www.haomeiwen.com/subject/gjqlsktx.html