经典面试题 | 前端路由概念及实现

“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

秋招将近,在应聘前端的岗位的时候,相信不少同学都被面试官问到前端路由以及它的实现方法,那笔者在学习之后来浅浅来谈谈自己对于以上问题的看法及理解。

前言

提到路由,我们不得不提及SPA单页面应用,即整个项目就只有一个html的文件,所有的页面数据的转换以及内容的跳转都是由路由实现的。即匹配不同的 url 路径,进行解析,然后动态的渲染出区域 html 内容。

前端路由

描述 url 和 UI(页面)之间的映射关系,我们称之为前端路由,属于单向映射, url 改变时导致UI 改变(渲染不同的html内容)。

如何实现前端路由

  1. 如何检测 url 发生了变化?
  2. 如何改变 url 却不引起页面的刷新?

前端路由的实现模式

对于现在大多数的项目都是SPA项目,当项目稍微复杂一点时,都需要用到前端路由,而在当下最受欢迎的两大前端框架中的vue中,vue-router就是vue的路由标配,并且其中有两种模式:hashhistory

下面我们来详细分析下两种模式是如何实现前端路由的。

hash模式

首先我们要知道浏览器的哈希值(hash)就是指 url 后面的#号以及后面的字符 ,由于hash值的改变不会导致浏览器向服务器发出请求,而且hash改变会触发onhashchange事件。hash虽然出现url中,但不会被包含在HTTP请求中,对后端完全没有影响,因此改变hash不会重新加载页面。

hash的实现核心:

1. 浏览器的hash值改变,不会导致浏览器向服务器发出请求,不会刷新UI页面。

2.使用浏览器自带的hashchange方法可以监听浏览器hash指的改变。

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

    <!-- 渲染对应的ui -->
    <div id="routerView"></div>


    <script>
        let routerView = document.getElementById('routerView');   // 获取插入html的dom结构

        window.addEventListener('load',onHashchange)

        window.addEventListener('hashchange', onHashchange)// 浏览器自带的监听哈希值改变的方法:hashchange
         
        // 控制渲染对应的 UI 
        function onHashchange() {
            // console.log(location.hash);
            switch (location.hash) { //  location.hash为哈希值
                case '#/home': 
                  routerView.innerHTML = 'Home'
                  return
                case '#/about':
                  routerView.innerHTML = 'About'
                  return
                default:
                  return
            }
        }

        routerView.innerHTML = 'Home'
    </script>
</body>
</html>
复制代码

load 事件在整个页面及所有依赖资源如样式表和图片都已完成加载时触发。先记录一次浏览器的url,当点击两个a标签中任意一个都可以触发hashchange事件,而我们写的onHashchange函数,就是模拟页面匹配 url 去渲染相应的 HTML 内容,例子中我们使用的是向一个div容器中插入html内容 。

  • hash模式所有的操作都是在前端完成的,不需要向后端(服务器)发出请求。
  • 通过监听URL中hash部分的变化,从而做出对应的渲染逻辑。
  • 缺点是url中带有 '#' 影响美观。

简单了解了hash模式的原理及实现方式,我们来聊聊history模式的原理及实现方式。

history模式

history模式的实现主要是因为HTML5中提供的history全局对象中的一些方法,比如:

  • window.history.go(pageNum) 可以跳转到浏览器会话历史中的指定的某一个记录页
  • window.history.forward() 指向浏览器会话历史中的下一页,跟浏览器的前进按钮相同
  • window.history.back() 返回浏览器会话历史中的上一页,跟浏览器的回退按钮功能相同
  • window.history.pushState(stateData, title, url) 可以将给定的数据压入到浏览器会话历史栈中
  • window.history.replaceState(stateData, title, url) 将当前的会话页面的url替换成指定的数据

而history模式的核心就是利用了 window.history.pushState(stateData, title, url)window.history.replaceState(stateData, title, url),因为这两种方法用于修改 url 时,不会刷新页面。

pushState是压入浏览器的会话历史栈中,会使得history.length加1,而replaceState是替换当前的这条会话历史,因此不会增加history.length。

第一个问题:如何改变 url 却不引起页面的刷新?history模式使用以上两种方法就可以实现,但是第二个问题,如何监听 url 发生改变?

其实官方提供了一个事件 Window: popstate event,当用户导航会话历史记录时活动历史记录条目发生更改时,会触发[Window](Window: popstate event - Web APIs | MDN (mozilla.org)) 界面的 popstate 事件。

但是官方也说明了

image.png

其中关键就是仅调用 history.pushState 或者 history.replaceState 方法时,不会触发popstate事件,但是单击浏览器前进或后退按钮会触发popstate事件(或者在js中调用history.forward()或者history.back()

意思就是不能用 popstate 来监听 history.forward()或者history.back()

在笔者查阅文章资料的时候发现了,我们可以重写 history.forward()history.back()两个方法,使其暴露在window全局,这样我们就可以监听重写的两个方法了。

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

  <!-- 渲染对应的UI -->
  <div id="routerView"></div>

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

    window.addEventListener('DOMContentLoaded', onLoad)


    // window.addEventListener('popstate', onPopState)  // 浏览器的前进后退能匹配
    

    function onLoad() {
      onPopState()

      let links = document.querySelectorAll('li a[href]')
      // 拦截a标签的默认跳转行为
      links.forEach(a => {
        a.addEventListener('click', (e) => {
          e.preventDefault() // 阻止a标签的href行为
          
          history.pushState(null, '', a.getAttribute('href'))  // 跳转
          onPopState()
        })
      })
    }


    function onPopState() {
      // console.log(location.pathname);
      switch (location.pathname) {
        case '/home':
          routerView.innerHTML = '<h2>home page</h2>'
          return
        case '/about':
          routerView.innerHTML = '<h2>about page</h2>'
          return
        default:
          return
      }
    }
  </script>
</body>
</html>
复制代码

笔者的例子中是采用了preventDefault()阻止a标签的默认href跳转行为,再利用document.querySelectorAll获取到a标签上的href属性,在此基础上再监听a标签的点击事件,在点击时间内,利用history.pushState 或者 history.replaceState 方法来实现模拟路由跳转。想监听history.pushState 或者 history.replaceState 可参考 (# 面试官为啥总是喜欢问前端路由实现方式?)

history 致命的缺点就是当改变页面地址后,强制刷新浏览器时,(如果后端没有做准备的话)会报错,因为刷新是拿当前地址去请求服务器的,如果服务器中没有相应的响应,会出现 404 页面。

结语

所以我们前端路由的选择取决于我们的使用场景,以及要求等,两者都可实现,各有优劣,以上就是我对于前端路由的一些个人见解,有任何问题都可在评论区讨论,感谢您的观看,谢谢!

点赞.jpg

猜你喜欢

转载自juejin.im/post/7143059639196712997
今日推荐