面试官:谈谈前端路由的实现原理【hash&history】

7,671 阅读8分钟

vue-router是前端路由,但是前端路由不是vue-router,这是个包含关系

路由

路由一词最早来自服务器,和前端没有关系。当你想要从服务器中读取某个盘的文件,这个文件的路径就是路由。也就是说路由是服务器端用来描述路径的,或者是说url和文件的映射关系

后来因为前端的SPA单页应用,前端也借鉴了路由这个概念。浏览器的url变了需要映射到页面的某个组件,url变了需要展示某个组件。/home和Home.vue,/about和About.vue就是一一映射的关系。前端借鉴路由的称呼来描述url和组件的映射关系。这个时候你就想起来router中index.js文件中,一个path对应一个component,也就是一个路径对应一个组件

实现路由需要解决的问题

  1. 如何修改url还不引起页面的刷新
  2. 如何知道url变化了

若是能解决这两个问题就可以实现前端路由了。

哈希Hash

哈希是一种值,按照某种规则生成的一串值,用来代表一个唯一的文件,文件名后加一个哈希值,可以看到文件是否被修改过。

在浏览器中也有hash这个概念,url中接一个##后的值就是哈希值,按道理url变了,页面一定会刷新,但是哈希是个特例,放个哈希值就是不会刷新页面,这样,我们就解决了第一个问题,修改url不引起页面的刷新

在浏览器url后加个哈希值,哈希值的变更不会引起浏览器页面的刷新

下面利用哈希模式实现路由

哈希手搓一个路由

我们新建一个hash.html文件,放两个a标签,但是a标签有个机制,就是点击必定会引起页面的刷新。但浏览器的机制是哈希值的变更不会引起页面刷新,所以地址放哈希值可以解决这一问题

<ul>
    <li><a href="#/home">首页</a></li> 
    <li><a href="#/about">关于</a></li>
</ul>

<div id="routeView">
        <!-- 放一个代码片段 点击首页首页代码片段生效,反之关于生效-->

</div>

现在模拟一个场景,如果点击首页,routeView容器展示首页的内容,点击关于routeView容器展示关于页面的内容,如果能够实现,路由就可以实现了

自行封装一个路由,先写一个路由的映射关系

<script>
    const routes = [
        {
            path: '#/home',
            component: '首页内容'
        },
        {
            path: '#/about',
            component: '关于页面内容'
        }
    ]
</script>

点击首页,展示首页内容,点击关于,展示关于首页内容

接下来的事情就是点击url,我们需要知道url的变化。我们不可能给按钮添加一个点击事件,如果项目大起来,按钮很多,每次点击一个按钮都判断一次url的变化,会非常的不优雅。

js自带一个hashchange事件,它可以自动监听hash值的变更。当我们点击首页的时候,下面的代码都会执行一次,因为hash值变了

window.addEventListener('hashchange', () => {
	console.log('changed')
})

这样,第二个问题我们已经解决了。非常之简单!

我们现在把监听器的回调函数写出来,拿到当前的哈希值,去对应component。

window.addEventListener('hashchange', onHashChange)

function onHashChange() {
	console.log(location)
}

location代表window窗口的url,我们运行打印这个location看看

1.png

看到没有,里面刚好有个hash值,我们可以把这个拿出来去对应component!

这个时候直接去数组中匹配就可以,forEach遍历

function onHashChange() {
    console.log(location) 
    routes.forEach((item, index) => {
        if(item.path === location.hash) {
            routeView.innerHTML = item.component
        }
    })
}

当然,记得拿到routeView的dom结构

这样写会有个问题,就是页面刚加载完毕的时候不会去加载当前的路由,想要hashchange在页面初次加载的时候触发一次,那就给一个监听dom结构的事件,dom一出来就会执行,也就是说页面加载完毕就调用一次hashchange

window.addEventListener('DOMContentLoaded', onHashChange)

好了,最终的hash.html如下

<body>
    <!-- 模拟单页页面应用 -->
    <ul>
        <li><a href="#/home">首页</a></li> 
        <li><a href="#/about">关于</a></li>
        <!-- 判断url的变化,绑定点击事件不好,页面过多就很累赘,有个hashchange的官方方法 -->
    </ul>

    <div id="routeView">
        <!-- 放一个代码片段 点击首页首页代码片段生效,反之关于生效-->

    </div>
    <script>
        const routes = [
            {
                path: '#/home',
                component: '首  容'
            },
            {
                path: '#/about',
                component: '关于页面内容'
            }
        ]
        
        const routeView = document.getElementById('routeView')
        window.addEventListener('DOMContentLoaded', onHashChange) // 与vue的声明周期一个道理,dom一加载完毕就触发
        window.addEventListener('hashchange', onHashChange)
        
        function onHashChange() {
            console.log(location) // url详情,里面就有个hash值  liveserver可以帮你把html跑成服务器
            routes.forEach((item, index) => {
                if(item.path === location.hash) {
                    routeView.innerHTML = item.component
                }
            })
        }
    </script>
</body>

其实这就是vue-router中两种模式之一哈希模式,哈希模式就是这样是实现的。

修改地址栏

  1. a标签
  2. 浏览器前进后退
  3. window.location

以上方式导致url变更都会触发hashchange事件。

那问题来了,history模式没有哈希是如何实现的呢?没有哈希值a标签一定会引起页面的刷新,如何解决?我们继续看下去

history用得更多,二者没有本质区别,仅仅是因为哈希模式的url多了个#很丑,所以用的少

history手搓一个路由

我们先看下history在mdn中的介绍

History - Web API 接口参考 | MDN (mozilla.org)

文档中介绍:history接口允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录

我们重点看一个history自带的方法pushState

2.png

它可以修改url且不引起页面的刷新

浏览器中有个会话历史栈,它可以维护你的访问路径,有了这个你返回就可以按照栈的顺序进行前进回退。

pushState提到了popState,他是靠popState监听url的改变的,并且仅当浏览器前进后退时生效

既然如此,我们现在开始手搓

同样是上面的情景,两个a标签,一个首页,一个关于页面。

<ul>
    <li><a href="/home">首页</a></li> 
    <li><a href="/about">关于</a></li>
</ul>

<div id="routeView">

给个url和组件的对应关系数组,已经不用哈希了

<script>
    const routes = [
        {
            path: '/home',
            component: '首页内容'
        },
        {
            path: '/about',
            component: '<h1>关于页面内容</h1>'
        }
	]
</script>

a标签有个默认的页面跳转效果,既然现在不用哈希,我们就需要自己把a标签的页面跳转刷新效果干掉

先拿到所有的a标签

const links = document.querySelectorAll('li a')

再去禁用掉默认的跳转行为,它跳转一定会带来刷新,要干掉它

links.forEach(a => {
	a.addEventListener('click', (e) => {
		console.log(e)
		e.preventDefault() // 阻止a的跳转行为
	})
})

我们可以打印看看这个事件参数,顺着原型链找到event对象,里面有个preventDefault,这个就是禁用a标签默认的跳转行为

3.png

接下来添加一个可以修改url又不引起页面刷新的方法,就是pushState,具体用法查看mdn

他有三个参数,第一个参数是JavaScript对象,一般不需要,给个null就好,第二个参数由于历史原因,写个空字符,不写会有问题,第三个参数是新的url

新的url肯定是点了什么放什么url,所以我需要读取到a标签的href值

a.getAttribute('href')

以上方法是核心,这里已经实现了哈希一样的效果,并且没有难看的#pushState的核心原理就是它会往浏览器的历史栈中塞一个值进去,让浏览器显示新的值,并且不引起页面的刷新

接下来就是要去感知到url的变化,去一一对应组件的展示

我们写一个函数,来实现这个功能。还是一样的,先拿到当前的浏览器地址

location.pathname

然后再进行遍历,去添加组件

routes.forEach((item) => {
    if(item.path === location.pathname) {
        routeView.innerHTML = item.component
    }
})

同样的,我们需要在页面初次加载的时候调用函数

但是浏览器的前进后退没有触发上面的遍历函数,popState刚好填补这个空缺

window.addEventListener('popState', onPopState)

好了,最终的history.html如下

<body>
    <ul>
        <li><a href="/home">首页</a></li> 
        <li><a href="/about">关于</a></li>
    </ul>

    <div id="routeView">

    </div>

    <script>
        const routes = [
            {
                path: '/home',
                component: '首页内容'
            },
            {
                path: '/about',
                component: '<h1>关于页面内容</h1>'
            }
        ]
        
        const routeView = document.getElementById('routeView')

        window.addEventListener('DOMContentLoaded', onLoad)
        window.addEventListener('popstate', onPopState)

        function onLoad() {
            const links = document.querySelectorAll('li a') // 获取所有的li下的a标签
            // console.log(links)
            links.forEach((a) => {
                // 禁用a标签的默认跳转行为
                a.addEventListener('click', (e) => {
                    console.log(e)
                    e.preventDefault() // 阻止a的跳转行为
                    history.pushState(null, '', a.getAttribute('href')) // 核心方法  a.getAttribute('href')获取a标签下的href属性
                    // 映射对应的dom
                    onPopState()
                })
            })
        }

        function onPopState() {
            console.log(location.pathname)
            routes.forEach((item) => {
                if(item.path === location.pathname) {
                    routeView.innerHTML = item.component
                }
            })
        }
    </script>
</body>

效果如下

GIF 2023-12-25-星期一 23-45-48.gif

最后

本期文章主要介绍了路由这个概念,以及重点讲前端实现路由的两种模式,哈希刚好就是浏览器承认他,接了哈希值改变url不会引起页面的刷新,然后通过location.hash得知哈希值;history就是有个方法,可以改变url不引起页面刷新的pushState,通过location.pathname得知url,这个模式下前进回退需要通过popState事件来触发。

另外有不懂之处欢迎在评论区留言,如果觉得文章对你学习有所帮助,还请”点赞+评论+收藏“一键三连,感谢支持!

本次学习代码已上传至本人GitHub学习仓库:github.com/DolphinFeng…