手写前端路由要解决的问题
在学习之前,我们要先知道我们要解决什么问题。
实现前端路由需要解决哪些问题?
- 如何修改url,还不引起页面的刷新?
 - 如何知道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
        }
    })
}
        - 这段代码,我们首先
const routeView = document.getElementById('routeView')拿到页面中的容器 - 通过
forEach遍历routes数组中的一条条数据,item.path==location.hash进行匹配! - 最后通过
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模式路由修改地址栏
- a标签
 - 浏览器的前进和后退
 - 修改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()方法向浏览器的会话历史栈增加了一个条目。
这个方法可以修改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>
        最终效果!

最后
今天,我们针对实现前端路由的两个问题
- 如何修改url,还不引起页面的刷新?
 - 如何知道url发生了变化?
 
分别手搓了一下前端路由的两种模式:hash模式和history模式!
假如你也和我一样,在准备冲刺春招,欢迎大家加微信shunwuyun,我们这里有好多小伙伴和各种大佬可以相互鼓励,分析信息,模拟面试,共读源码,齐刷算法,手撕面经。加油!友友们!
如果,你觉得这篇文章有帮助的话,可以帮博主点赞+评论+收藏,三连一波!感谢!
往后,我还会持续输出vue相关的文章,如node相关的内置模块,koa的使用等等文章,感兴趣的小伙伴可以关注一波!
代码已经上传至个人Github:一个修远君的github之手写前端路由