聊聊前端路由的实现原理hash/history

0 阅读4分钟

前言

大家好,这里是我不是程序猿kk。今天想和大家聊一聊前端路由的实现原理,注意: 是一个大的概念,前端,而不仅仅是vue/react

本文将从是什么到为什么最后落到怎么做进行详细,以大白话的方式介绍。

前端路由

是什么

举个例子,当我们用vue创建一个项目,打开后在you did it!页面会看见两个按钮,一个about,一个home。当点击某个按钮,就会进行路由的跳转。

那么什么是前端路由,路由存在的意义是什么?

像vue这种SPA,当我们修改了url地址栏后,接下来要做的就是想办法把某个代码片段(组件)挂到这一份html的某个位置。所以路由的意义其实就是当用户修改了url后,我们给用户加载对应的组件。

曾经我们开发多页应用时,从某个html跳转到其他html,用a标签的href属性,这不属于路由的概念,这是路径的跳转。

现在单页应用的时代下,如何在一份html中打开另一份html?事实上不需要,我们只需要这一份html,不断在这一份html中替换其他的代码片段,这个功能就要依靠路由了。

再回到刚刚的例子,点击about按钮后,就会挂载about页面,但是在这个过程中我们可以看见,页面是不会刷新的,细心的同学会发现,如果我们去访问百度的首页,点击图片按钮,这个时候会刷新,跳转到另一个页面,这属于多页应用。

所以前端路由的概念其实就是url和页面的映射关系。前端本无路由,最早路由的概念是用来称呼服务器上面的资源路径,比如服务器上的一个图片在某个文件的某个文件夹的某个文件上,这个完整路径称为路由。后来路由的概念才被广泛借鉴到各个领域中使用,包括前端、后端、node等等。

并且要满足:

  1. 当url修改了,页面要更新
  2. 浏览器不能刷新(刷新了就不叫路由了,路由这个概念出现在SPA中,如果不是SPA也不需要路由这个概念了)

思考: 什么行为可以更改url?这一点很简单,用户输入、js控制...,但是,我们如何知道url更改了?

hash(浏览器天生支持)

url地址中,也能放hash值,www.baidu.com#abc 井号xxx,后面接了井号包括井号后面的东西,都会被浏览器识别成hash值,那么我们可以去尝试一下,当我们在后面接abcdefg,然后回车,页面会刷新吗?——不会

因此,这就是前端路由实现的第一套方案(有什么办法修改url还不引起页面的刷新),这也是前端路由中大名鼎鼎的一套方案,例如vue中创建路由,createWebHashHistory

如何实现? 我们来创建一份hash.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <ul>
        <li><a href="#/home">首页</a></li>
        <li><a href="#/about">关于</a></li>
    </ul>
</body>
</html>

image.png

点击按钮,url更改了,但是页面不刷新,接下来要干的就是页面的映射,在vue中是这样的

<nav>
    <RouterLink to="/">Home</RouterLink>
    <RouterLink to="/about">About</RouterLink>
</nav>

<RouterView />

也就是说需要一个路由入口,将需要展示的结构展示到这个地方,那我们也来试一试

hash.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <ul>
        <li><a href="#/home">首页</a></li>
        <li><a href="#/about">关于</a></li>
    </ul>

    <!--    展示页面-->
    <div id="routerView"></div>


</body>
<script>
    let routerView = document.getElementById('routerView')

    const routes = [
        {
            path: '/home',
            component: () => {
                return '<h2>首页</h2>'
            }
        },
        {
            path: '/about',
            component: () => {
                return '<h2>关于页</h2>'
            }
        }
    ]

    window.addEventListener('DOMContentLoaded', ()=>{
        renderView(location.hash);
    })
    // 监听页面上的hash值的变更
    window.addEventListener('hashchange', () => {
        console.log('hashChange')
        // 当前hash值
        console.log(location.hash)

        renderView(location.hash)
    })
    function renderView(locationHash) {
        const index = routes.findIndex(item => {
            return '#' + item.path === locationHash
        })
        console.log(index)
        routerView.innerHTML = routes[index].component()
    }
</script>
</html>

效果

image.png

history

相比于hash,history就要复杂一些了,毕竟浏览器天生就能识别hash这种模式,hash又天生不会让页面刷新,天生有hashchange事件给你用,还有缓存栈,当点击回退,页面会回退。

html提供的history对象拥有pushState和replaceState两个方法,他们可以用来修改url,不会引起页面的刷新

当a标签被点击时,通过阻止a的默认跳转行为,人为的使用pushState修改url,再通过监听事件popstate来控制浏览器的前进后退 history.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>

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

    <div id="routerView">

    </div>


</body>
<script>
    let routerView = document.getElementById('routerView')
    window.addEventListener('DOMContentLoaded', ()=>{
        onLoad()
    })
    window.addEventListener('popstate', ()=>{ // 监听浏览器的回退按钮
        renderView(location.pathname)
    })

    const routes = [
        {
            path: '/home',
            component: () => {
                return '<h2>首页</h2>'
            }
        },
        {
            path: '/about',
            component: () => {
                return '<h2>关于页</h2>'
            }
        }
    ]

    function renderView(pathName) {
        const index = routes.findIndex(item => {
            return item.path === pathName
        })
        console.log(index)
        routerView.innerHTML = routes[index].component()
    }

    function onLoad(){
        let linkList = document.querySelectorAll('a[href]')
        linkList.forEach(el => {
            el.addEventListener('click', (e)=>{
                e.preventDefault() // 阻止默认行为 阻止页面跳转
                history.pushState(null,'',el.getAttribute('href')) // 不进入浏览器的缓存栈
                // renderView(el.getAttribute('href'))
                renderView(location.pathname)
            })
        })
    }
</script>
</html>

结语

本文着重介绍了前端路由是什么,有哪些模式,模式的原理是什么,并以简化源码的形式让大家明白,底层是通过哪些手段实现的,分别存在什么问题。

如有疑问,欢迎私信讨论。创作不易,如有帮助,烦请一键三连~