请你手写前端路由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之手写前端路由

相关推荐
CodeToGym3 分钟前
Webpack性能优化指南:从构建到部署的全方位策略
前端·webpack·性能优化
~甲壳虫4 分钟前
说说webpack中常见的Loader?解决了什么问题?
前端·webpack·node.js
~甲壳虫8 分钟前
说说webpack proxy工作原理?为什么能解决跨域
前端·webpack·node.js
Cwhat10 分钟前
前端性能优化2
前端
熊的猫1 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
瑶琴AI前端1 小时前
uniapp组件实现省市区三级联动选择
java·前端·uni-app
会发光的猪。1 小时前
如何在vscode中安装git详细新手教程
前端·ide·git·vscode
mosen8682 小时前
Uniapp去除顶部导航栏-小程序、H5、APP适用
vue.js·微信小程序·小程序·uni-app·uniapp
别拿曾经看以后~3 小时前
【el-form】记一例好用的el-input输入框回车调接口和el-button按钮防重点击
javascript·vue.js·elementui
我要洋人死3 小时前
导航栏及下拉菜单的实现
前端·css·css3