实现前端路由hash、history模式没那么简单

1,427 阅读6分钟

作为单身狗一枚,实在不知道情人节干什么😂,哎算了,踏踏实实写文章吧,如果觉得文章不错就给个赞吧。

hash模式:


这个模式比较简单,当我们修改一个html文件的hash时,也就是URL地址后面加一个#value浏览器是不会发送请求的,且即使发送请求(如刷新页面)也不会携带上hash,利用这个特性我们可以通过监听hash的变化从而根据#后面的参数操作DOM完成相应的页面跳转。

这里着重要掌了解三个地方:

  • 修改hash通过a标签就可以完成,<a href="#1">显示1</a>
  • 监听hash的改变通过hashchange事件监听
  • 通过window.location.hash获取hash


逻辑代码:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        #page1,#page2,#page3,#page4,#page404 {
            display: none;
        }

    </style>
    
</head>
<body>
    <a href="#1">显示1</a>
    <a href="#2">显示2</a>
    <a href="#3">显示3</a>
    <a href="#4">显示4</a>
    <hr>
    <div id="app">
    </div>
    <div id="page1">1</div>
    <div id="page2">2</div>
    <div id="page3">3</div>
    <div id="page4">4</div>
    <div id="page404">404 Not Found</div>

    <script>
        // 获取所有的页面
        let page1 = document.querySelector('#page1');
        let page2 = document.querySelector('#page2');
        let page3 = document.querySelector('#page3');
        let page4 = document.querySelector('#page4');
        // 建立hash表
        const routeTable = {
            '1': page1,
            '2': page2,
            '3': page3,
            '4': page4
        }

        function route() {
            // 获取当前hash
            let number = window.location.hash.slice(1);
            number = number || 1;
            // 根据hash拿到当前页面
            let page = routeTable[number.toString()];
            page = page || document.querySelector('#page404');
            page.style.display = 'block';
            // 拿到容器
            let app = document.querySelector('#app');
            if(app.children.length > 0) {
                app.innerHTML = '';
            }
            // 渲染页面
            app.appendChild(page);
        }


        // 初始渲染主页面
        route();
        // 监听hash的变化
        window.addEventListener('hashchange',function(){
            route();
        })
    </script>
</body>
</html>


其兼容性是比较好的,但是由于修改hash不会向浏览器发送请求,所以SEO不友好。

history模式:


在之前我了解到的history模式无非就是通过history.pushState或者history.replaceState修改URL,然后根据MDN文档给出的会触发popstate事件,我们只要对popstate事件进行监听获取location.pathname像上面一样根据对应的参数修改页面的内容就好了。

之后我在网上搜索了一下,对于修改URL但不发送请求的方法主要是window.history上面的:

  • back():后退到上一个路由;
  • forward():前进到下一个路由,如果有的话;
  • go(number):进入到任意一个路由,正数为前进,负数为后退;
  • pushState(obj, title, url):前进到指定的 URL,不刷新页面;
  • replaceState(obj, title, url):用 url 替换当前的路由,不刷新页面;


区别是前三个主要是利用浏览器的历史记录,并不能生成新的URL,但后面两个会让浏览器将新的URL存入历史记录。还有就是pushState是IE10以上才可以使用<。br />
但是还要注意history模式必须与后端相配合,因为虽然history.pushState在修改URL后页面不会重新加载,但如果我们刷新页面还是会用新的URL去发送请求的,如果此时后端的URL还没有更新那么便会返回404

接下来详细说一下history.pushState这个API,其作用是在浏览器中添加一条新的历史记录,但不刷新页面。且其是在同源情况下进行的,也就是说其只会在当前URL后添加一下新的内容,这里是MDN的文档大家可以详细去看一下,上面也明确的说了history.pushState会触发popstate这个事件。

忽略前两个参数,我们主要需要关注第三个参数url便好,这是比较基本的用法:

window.history.pushState(state,title,url)

  • state:需要保存的数据,这个数据在触发popstate事件时,可以在event.state里获取
  • title:标题,基本没用,一般传null
  • url:设定新的历史纪录的url。新的url与当前url的origin必须是一样的,否则会抛出错误。url可以时绝对路径,也可以是相对路径。

//如当前url是 https://www.baidu.com/a/,
//执行history.pushState(null, null, './qq/'),
//则变成 https://www.baidu.com/a/qq/,
//执行history.pushState(null, null, '/qq/'),
//则变成 https://www.baidu.com/qq/

通过这个我们可以发现pushState的第三个参数可以是相对的地址

说到这里我想对于history模式大家应该有一个大致的了解了,以为文章就这么欢快的结束了?但是如果一切如愿也就没有发这篇文章的意义了。

接着我去尝试着向上面的hash模式一样写一个简单的案例,我知道自己没有后端配合,但是我想只要调用pushState方法可以出发popstate事件便好了,证明这样是可以完成任务的,但是。。。

	history.pushState(null,null,'#333');
        window.addEventListener('popstate',function(){
            console.log(location.pathname);
        })


我写了一个小小的案例,按道理如果我通过pushState修改的hash的话,刷新页面后不是会发送请求的但会触发popload事件,从而获取到对应的pathname。但是我蒙了,控制台啥也没有输出???

  image.png
我想这难道就是新年的第一个BUG吗?于是我只能在网上继续进行苦逼的搜索,终于找到了一篇文章和我遇见了同样的问题。

原来虽然popstate事件也带着一个state但是,其是无法通过pushStatereplaceState触发的,但是go(),back(),forward()却可以触发。

于是我立刻去进行了实验:

        window.addEventListener('popstate',function(){
            console.log(location.pathname);
        })
        setTimeout(function(){
            history.back();
        },1000)


  image.png

发现这回真的成功了,但是如果popstate无法监听pushState对URL的修改那么,我们究竟如何实现对pushState的监听呢?接下来真正的重点来了。。。

我们需要创建一个自定义事件new Event('pushState'),当我们调用pushState方法时去手动触发我们自定义的事件,然后对自定义事件进行监听就可以取代popstate事件了。

对自定义事件不了解的同学可以看MDN Event,其实将其理解为我们自己定义的一个事件就可以了,像click之类的,有了事件之后我们就可以将其绑定在一个元素上了,这个例子我先通过addEventListener绑定到window上。

绑定之后我们便需要触发绑定的事件了,我们绑定到了什么上我们就需要通过什么元素来触发,触发事件的API是dispatchEvent。其和我们操作DOM触发事件不同的地方在于其是同步的。具体也可以看MDN dispatchEvent

        let oBox = document.querySelector('.box');
        const andyevent = new Event('andyEvent')
        window.addEventListener('andyEvent',function(){
            console.log('andy创建的事件触发了');
        })
        setTimeout(function(){
            window.dispatchEvent(andyevent);
        },1000)


说了这么多这下回到正题,上面这些只是方便大家来理解new Event()dispatchEvent()两个API的作用,之后我们通过重写history.pushState方法,内部创建一个pushState自定义事件,当调用pushState方法时触发我们的自定义事件,在从而在外部监听便好,这是完成的代码。

        // 重写pushState与replaceState方法
        function addStateListener(){
            function listener(type){
                let origin = history[type];
                return function(){
                    let newOrigin = origin.apply(this,arguments);
                    let stateEvent = new Event(type);
                  	// 添加arguments的原因是可以在监听到事件触发时拿到传递给事件的参数
                    stateEvent.arguments = arguments;
                    window.dispatchEvent(stateEvent);
                    return newOrigin;
                }
            }
            history.pushState = listener('pushState');
            history.replaceState = listener('replaceState');
        }
        addStateListener()


实际测试

	addStateListener()
        // 监听pushState与replaceState
        window.addEventListener('pushState',function(e){
            console.log(location.pathname,e.arguments[2]);
        })
        window.addEventListener('replaceState',function(e){
            console.log(location.pathname,e.arguments[2]);
        })
        // 使用pushState与replaceState修改URL
        history.pushState(null,null,'#333')
        history.replaceState(null,null,'#666')

  

  image.png

好了到这里就真的结束了,后面我们根据location.pathname去截取字符串,或者直接向上面一样通过arguments来获取URL被修改部分的参数,来控制页面某些部分的显示与隐藏便好,这里就不再像hash那样具体的展示了。

最后:

最后文章首发于我的微信公众号【南橘前端】,这里还有很多前端精彩文章,欢迎大家多多关注,关于文章内容方面的问题我们也可以多多交流😉。

参考:


小蚊子:深入理解前端中的 hash 和 history 路由
大黑豹:hash和history实现以及区别