请你手写前端路由hash&history,你会不会写?

手写前端路由要解决的问题

在学习之前,我们要先知道我们要解决什么问题。


实现前端路由需要解决哪些问题?

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

着两个问题,就是我们手写前端路由需要解决的问题!

什么是路由?

前端本无路由!就像鲁迅先生说的:世界上本没有路,走的人多了也就成了路!

路由最早是服务端,服务器上的概念,和前端没有什么关系,当我们想要从服务器上读取某个盘的文件,拿到文件的路径就是我们所熟知的路由!路由本来是服务器端用于描述文件路径的一个概念

因为前端单页面的情景,前端也借鉴了路由这样一个概念,前端也需要这样一个映射关系,只要浏览器的url地址栏变化了,我就需要展示对应的代码片段!也就是展示相应的组件,实现一个url对应组件一一映射的关系!总结来说:前端借鉴路由的称呼来描述 url 和 组件(代码片段) 的映射关系

哈希Hash概念

什么是哈希?:哈希(Hash)是一种算法,它可以将任意长度的输入(也称为预映射或pre-image)通过特定的散列算法转换成固定长度的输出,这个输出就是散列值。这种转换过程是一种压缩映射,因为散列值的空间通常远小于输入的空间。这也意味着不同的输入可能会产生相同的输出,因此不可能通过散列值来唯一确定输入值。

通俗一点来说:哈希是按照某种规则,生成的一串值。通常可以用来代表一个唯一的文件,在文件名后面接一个哈希值,可以用于判断这个文件是否被修改过。

在浏览器中,也有哈希这个概念,在url中,接一个#,在#后方接的任意值,这个值都是哈希值,而哈希值的变更,不会引起浏览器的刷新!

这样在hash模式下,我们就解决了第一个问题!

通过修改url的哈希值,并且不会引起页面的刷新

下面,我们开始着手实现前端路由Hash模式

手写前端路由Hash模式

准备工作:新建一个hash.html,放置两个a标签,在a标签的href有值时,我们点击a标签必定会引起页面的刷新,但是如果我们在href中添加一个#,将url改为哈希值就不会引起页面的刷新!

html 复制代码
<!-- 加 # 号 就是hash模式 -->
<ul>
    <li>
        <a href="#/home">首页</a>
    </li>
    <li>
        <a href="#/about">关于</a>
    </li>
</ul>

<!-- 放一个代码片段,展示点击页面的内容 -->
<div id="routeView">

</div>

接下来,我们要实现的效果就是,通过点击首页 ,在routeView容器中展示首页页面的内容,点击关于,让容器展示关于页面的内容。实现一个单页应用!

思路:当我们点击首页时,将首页的代码片段塞到这个容器当中,点击关于时,清空容器当中的代码,再把关于页面的代码片段塞到这个容器当中,就可以实现这样一个效果!

我们先封装js,让代码片段进行一一映射。

html 复制代码
<script>
    const routes = [
        {
            path:'#/home',
            component:'首页页面内容'
        },
        {
            path:'#/about',
            component:'About page'
        }
    ]
</script>

实现了这样一个映射关系之后,点击首页,展示首页页面内容,点击关于,展示About page

接下来,我们就是获取url的变更,给a标签添加点击事件显然不合理。

这时候,js就自带了一个事件hashchange,可以自动监听哈希值的变更,在我们点击一个页面,哈希值发生变化,这个事件就会触发!

js 复制代码
window.addEventListener('hashchange', () => {
	console.log('changed')
})

这个问题解决之后,我们可以自行封装一个onHashChange函数,我们现在要拿到变化后的哈希值,我们可以这样尝试一下!location是js中的一个方法,本地的意思。

js 复制代码
window.addEventListener('hashchange', onHashChange)

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

我们到浏览器中看看打印结果!

我们可以看到,location是一个对象,描述的就是整个浏览器url地址栏中的详情!

我们可以看到在location对象里面有一个hash属性,这个不就是我们想要拿到的hash值(哈希值)吗?

这个时候,我们就可以直接去数据中对path与这个hash属性进行遍历。进行匹配!

js 复制代码
function onHashChange(){
    console.log(location)
    const routeView = document.getElementById('routeView')
    routes.forEach((item,index)=>{
        if(item.path==location.hash){
            //innerHTML会重置容器内的内容,重新赋值
            routeView.innerHTML=item.component
        }
    })
}
  1. 这段代码,我们首先const routeView = document.getElementById('routeView')拿到页面中的容器
  2. 通过forEach遍历routes数组中的一条条数据,item.path==location.hash进行匹配!
  3. 最后通过routeView.innerHTML=item.component将代码片段装载到容器中进行展示!

innerHTML会重置容器内的内容,将新的值赋给这个容器

这样,其实点击页面的效果展示对应页面的单页应用!我们就实现了!

但是有一个问题的是:当我们初始刷新页面的时候,我们的页面不会默认展示,所以我们在页面初次加载的时候,命中了哪个路由就展示哪个页面。现在我们只能hashchange事件发生的时候才能展示页面。

所以我们可以通过一个事件DOMContentLoaded事件! DOMContentLoaded 会在页面加载完毕之后就会自动触发一次

所以我们再添加一个语句:

js 复制代码
window.addEventListener('DOMContentLoaded',onHashChange)

这样hash路由主要的功能我们就实现了!

最终,我们的hash.html代码如下:

html 复制代码
<body>
    <!-- 加 # 号 就是hash模式 -->
    <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:'About page'
            }
        ]

        // js有一个hashChange事件 解决了如何知道url改变了
        window.addEventListener('DOMContentLoaded',onHashChange)// DOMContentLoaded 会在页面加载完毕之后就会自动触发一次
        window.addEventListener('hashchange',onHashChange)

        function onHashChange(){
            // console.log(location)
            const routeView = document.getElementById('routeView')
            console.log(location.hash);//location就是描述整个url路径里面的情况 hostname就是域名
            routes.forEach((item,index)=>{
                if(item.path==location.hash){
                    //innerHTML会重置容器内的内容,重新赋值
                    routeView.innerHTML=item.component
                }
            })
        }
    </script>
</body>

我们来看看效果!

hash模式路由修改地址栏

  1. a标签
  2. 浏览器的前进和后退
  3. 修改windows.location

这三种方式都会导致url的变更而触发hashchange事件!这是我们实现hash路由的主要手段!

接下来,我们实现路由的history模式,在这个模式下,不是通过修改url的哈希值来展示页面的,那我们不引起页面的刷新该怎么解决呢??

注意!hash模式和history没有本质上的区别,仅仅url路径上是否有#,与美观度有关。

手写前端路由History模式

我们先来看一个history是一个什么东西?

来到我们的mdn字典的介绍:History - Web API 接口参考 | MDN (mozilla.org)

这个接口上具有一个pushState()方法!


pushState()方法

按指定的名称和 URL(如果提供该参数)将数据 push 进会话历史栈,数据被 DOM 进行不透明处理;你可以指定任何可以被序列化的 javascript 对象。请注意,除了 Safari 所有浏览器现在都忽略了 title 参数。更多的信息,请看使用 History API


History:pushState() 方法

HTML 文档中,history.pushState() 方法向浏览器的会话历史栈增加了一个条目。

该方法是异步的。为 popstate 事件增加监听器,以确定导航何时完成。state 参数将在其中可用。

这个方法可以修改url而不引起页面刷新!

浏览器维护了一个历史栈,这个历史栈可以维护我们的访问路径,通过这个栈,可以让我们按照栈的进出顺序进行前进和回退!

pushState()方法中,有一个popstate事件,可以通过监听popstate事件,来感知url的改变!当浏览器发生前进后退时,这个事件触发!

接下来,我们就可以开始实现了

同样的,我们还是准备两个li和一个容器routeView

html 复制代码
<ul>
    <li>
        <a href="/home">首页</a>
    </li>
    <li>
        <a href="/about">关于</a>
    </li>
</ul>

<!-- 放一个代码片段 -->
<div id="routerView">

</div>

接下来,我还是声明一个映射关系数组routes,不过此时的path不要再使用hash模式了!

html 复制代码
<script>
    const routes = [
        {
            path: '/home',
            component: '<h2>首页页面内容</h2>'
        },
        {
            path: '/about',
            component: 'About page'
        }
    ]
</script>

既然我们用的不是hash模式,a标签默认会有跳转刷新的效果,所以我们可以通过一段js把这个效果给它干掉!

js 复制代码
const links = document.querySelectorAll('li a')
links.forEach(a => {
                a.addEventListener('click', (e) => {
                    console.log(e);
                    e.preventDefault()//阻止了a标签的默认跳转行为
                })
            })

在事件参数e中有一个方法preventDefault,可以禁用a标签的默认跳转行为!

我们阻止了跳转行为之后,我们还要解决如何修改url而不引起刷新的问题,这里我们就需要使用到pushState

语法:

js 复制代码
pushState(state, unused)
pushState(state, unused, url)

接收三个参数

state:state对象是一个 JavaScript 对象,其与通过 pushState() 创建的新历史条目相关联。每当用户导航到新的 state,都会触发 popstate 事件,并且该事件的 state 属性包含历史条目 state 对象的副本。(一般我们不需要,可以放一个null)

unused:由于历史原因,该参数存在且不能忽略;传递一个空字符串是安全的,以防将来对该方法进行更改。

url:新历史条目的 url。

请注意,浏览器不会在调用 pushState() 之后尝试加载该 URL,但是它可能会在以后尝试加载该 URL,例如,在用户重启浏览器之后。新 URL 可以不是绝对路径;如果它是相对的,它将相对于当前的 URL 进行解析。新的 URL 必须与当前 URL 同;否则,pushState() 将抛出异常。如果该参数没有指定,则将其设置为当前文档的 URL。

因为url是我们需要根据不同的点击,而拿到的url值,我们需要拿到用户点击的a标签href中的地址!

这里我们可以通过

js 复制代码
a.getAttribute('href')

来获取a标签的href属性!

走到这里,我们就掌握了一个核心方法

js 复制代码
history.pushState(null, '', a.getAttribute('href'))

拿到用户点击的url地址!同时修改url地址,又不会引起刷新,就实现和哈希一样的效果!

代码就写成这样

js 复制代码
const links = document.querySelectorAll('li a')
links.forEach(a => {
    a.addEventListener('click', (e) => {
        console.log(e);
        e.preventDefault()//阻止了a标签的默认跳转行为
        history.pushState(null, '', a.getAttribute('href'))
    })
})

我们再优化一下代码,用一个函数onLoad来封装我们的主函数

js 复制代码
function onLoad() {
    const links = document.querySelectorAll('li a')
    links.forEach(a => {
        a.addEventListener('click', (e) => {
            e.preventDefault()//阻止了a标签的默认跳转行为
            //添加一种,可以修改url 又不造成页面刷新的
            history.pushState(null, '', a.getAttribute('href'))//核心方法 
            //用函数映射对应的dom
            onPopState()
        })
    })
}

接下来,我们要实现的效果是去根据url地址,来映射对应的代码片段,也就是展示对应的组件!

我们可以自己封装一个函数,来映射对应的dom

这个函数我们命名为onPopState(),并且在onLoad中调用!

js 复制代码
const routerView = document.getElementById('routerView')
function onPopState() {
    //拿到当前浏览器的地址
    console.log(location.pathname);
    routes.forEach(item => {
        if (item.path == location.pathname) {
            routerView.innerHTML = item.component
        }
    })
}

我们知道location描述的就是整个浏览器url地址栏中的详情,它其中有一个属性pathname展示就是当前浏览器的地址。

拿到之后,我们还是同样的操作forEach遍历routes数组,找到url匹配的path,我们再拿到routerView的dom结构,使用innerHTML装载对应的代码片段(组件)

我们可以在初次进入页面时,就加载一次onLoad函数!

js 复制代码
window.addEventListener('DOMContentLoaded',onLoad)

但是,我们不点击还是不会展示页面,并且刷新,这个url地址不会被浏览器承认!因为现在不是hash模式下的hash值,现在url是真实的路径,只有当我们点击浏览器才会认为这个路径是存在的!这个是live server存在的一个现象!

并且前进后退,不会触发我们的方法!所以,这里我们就可以用到上面提到的官方事件,popstate刚好可以补全这个不足。

所以我们可以再添加一个事件监听!

js 复制代码
window.addEventListener('popstate',onPopState)

只要popstate这个事件触发,就会调用一次onPopState,就是把浏览器的前进和后退包括再内,

所以最终history.html代码如下:

html 复制代码
<body>
    <!-- 加 # 号 就是hash模式 -->
    <ul>
        <li>
            <a href="/home">首页</a>
        </li>
        <li>
            <a href="/about">关于</a>
        </li>
    </ul>

    <!-- 放一个代码片段 -->
    <div id="routerView">

    </div>

    <script>
        const routerView = document.getElementById('routerView')

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

        const routes = [
            {
                path: '/home',
                component: '<h2>首页页面内容</h2>'
            },
            {
                path: '/about',
                component: 'About page'
            }
        ]

        function onLoad() {
            const links = document.querySelectorAll('li a')
            console.log(links);
            links.forEach(a => {
                a.addEventListener('click', (e) => {
                    // console.log(e);
                    e.preventDefault()//    阻止了a标签的默认跳转行为
                    //添加一种,可以修改url 又不造成页面刷新的
                    history.pushState(null, '', a.getAttribute('href'))//核心方法 a.getAttribute('href')获取a标签下面的href属性   获取dom结构的属性
                    //用函数映射对应的dom
                    onPopState()
                })
            })
        }


        function onPopState() {
            //拿到当前浏览器的地址
            console.log(location.pathname);
            routes.forEach(item => {
                if (item.path == location.pathname) {
                    routerView.innerHTML = item.component
                }
            })
        }
    </script>

</body>

最终效果!

最后

今天,我们针对实现前端路由的两个问题

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

分别手搓了一下前端路由的两种模式:hash模式和history模式!

假如你也和我一样,在准备冲刺春招,欢迎大家加微信shunwuyun,我们这里有好多小伙伴和各种大佬可以相互鼓励,分析信息,模拟面试,共读源码,齐刷算法,手撕面经。加油!友友们!

如果,你觉得这篇文章有帮助的话,可以帮博主点赞+评论+收藏,三连一波!感谢!

往后,我还会持续输出vue相关的文章,如node相关的内置模块,koa的使用等等文章,感兴趣的小伙伴可以关注一波!

代码已经上传至个人Github:一个修远君的github之手写前端路由

相关推荐
嘉琪coder4 分钟前
React的两种状态哲学:受控与非受控模式
前端·react.js
木胭脂沾染了灰15 分钟前
策略设计模式-下单
java·前端·设计模式
Eric_见嘉19 分钟前
当敦煌壁画遇上 VS Code:我用古风色系开发了编程主题
前端·产品·visual studio code
拉不动的猪34 分钟前
刷刷题28(http)
前端·javascript·面试
IT、木易1 小时前
大白话 CSS 中transform属性的常见变换类型(平移、旋转、缩放等)及使用场景
前端·css·面试
1024小神2 小时前
更改github action工作流的权限
前端·javascript
Epicurus2 小时前
JavaScript无阻塞加载的方式
前端·javascript
1024小神2 小时前
tauri程序使用github action发布linux中arm架构
前端·javascript
ahhdfjfdf2 小时前
最全的`Map` 和 `WeakMap`的区别
前端