原生前端路由的原理与实现



  • 传统路由

    传统的网页根据用户访问的不同的地址,浏览器从服务器获取对应页面的内容展示给用户。这样造成服务器压力比较大,而且用户访问速度也比较慢。在这种场景下,出现了单页应用。

    浏览历史(History)

    Window 对象的 history 属性引用的是该窗口的 History 对象。History 对象是用来把窗口的浏览历史用文档和文档状态列表的形式表示。

    History 对象有 back()、forward()、go()三种方法;back()、forward()方法与浏览器的“前进”、“后退”按钮一样,可以使浏览器在浏览历史中前后跳转一格。go()方法接受一个整数参数,可以在历史列表中向前或向后跳过任意多个页。

    单页应用模式(Single-Page-Application)

    单页应用,就是只有一个页面,用户访问一个网址,服务器返回的页面始终只有一个,不管用户改变了浏览器地址栏的内容或者在页面内发生了跳转,服务器不会重新返回新的页面。换句话来说,单页应用不需要通过刷新的方式改变页面,而是根据 URL 的变化直接动态的将当前页面的部分内容清除,并将下一个页面的内容挂载到当前页上。相比于传统多页面应用,单页应用每次跳转路由不需要刷新就能够改变页面内容,大大提升了用户体验。而实现这个功能就主要由前端实现,称为前端路由。

    单页应用原理

    js 会感知到 URL 的变化,通过监听 URL 的变化将与每个 URL 相应的前端组件挂载到当前页面上。同时在改变 URL 的时候不能触发页面的刷新,为了实现这个效果我们就有 hash 和history 两种前端路由的模式。

    前端路由模式:hash 和 history

    说到前端路由,首先就要提到浏览历史记录的问题。Web 浏览器会记录在一个窗口中载入的所有文档,同时提供了“后退”和“前进”按钮,允许用户在这些文档之间切换浏览。但是如今前端路由的出现让页面在无须刷新的情况下就可以显示新的应用状态,这时候如果还想用“后退”和“前进”按钮直观的切换应用状态,就必须自己处理应用的历史记录管理(因为这个时候浏览器不再向服务器请求新的文档)。HTML5 定义了两种用于历史记录管理的机制。他们分别是 hash 和 history。

    其中 hash 机制相对来说更加简单,我们就先说 hash 机制:

    hash 模式

    hash 模式利用的是片段标识符的特性。纯粹的片段标识符是相对 URL 的一种类型,他不会让浏览器载入新的文档,但只会让他滚动到文档的某个位置,这被称为“页面锚点”。

    hash 属性和页面锚点

    hash 属性,简单来说就是页面 URL 中“#”后面的字符串,一般接在 URL 的最后面。hash 属性原本是用来定位寻找页面中的特定内容的。这个内容就被称为“锚点”。举个例子:

    设置一个锚点链接

    <a href="#miao">去找喵星人</a>;
    

    在页面中需要的位置设置锚点

    <a name="miao"></a>
    

    a 标签中一般不写内容,只是为了标记一个页面中的位置。还有一种添加锚点的方式,就是不用单独添加一个a标签来专门设置锚点 ,只在需要的位置的标签中添加一个 ID 即可。

    <h3 id="miao">喵星人基地</h3>;
    

    这样当 URL 的后面加上了“#miao”的时候,浏览器就会自动帮我们滚动定位到 name 为 miao 的 a 标签的位置或者 ID 为 miao 的元素的位置。这整个过程是不需要经过页面的刷新的,正是它不用刷新的特点让后面的 hash 路由得以产生。

    hash 用于前端路由跳转

    hash 机制主要就是利用 location.hash 和 hashchange 事件。location 是 Window 对象的一个属性,它引用的是 Location 对象,表示该窗口中当前显示的文档的 URL。设置 location.hash 属性会更新显示在地址栏中的 URL,同时会在浏览器的历史记录中添加一条记录。在前面我们说 hash 通常适用于指定要滚动到的文档中某一部分的 ID,但是 location.hash 不一定非设置成一个元素的 ID:它可以设置成任何的字符串。如果能将应用状态编码成一个字符串,就可以使用该字符串作为片段标识符。

    现在我们能够用“后退”和“前进”按钮来改变不同 hash 值的 URL 了,但是页面并不会因此发生改变,因为这个过程并没有向后台请求数据,应用也无法感知到 URL 的变化。这个时候就需要通过设置 window.onhashchange 为一个处理程序函数,这样当每次切换历史记录时触发 hashchange 事件,都会调用该处理函数对改变后的 location.hash 的值进行解析,然后重新渲染应用。

    下面我们用原生 js 简单实现一下 hash 路由:

    <body>
      <ul>
        <!-- 定义路由 -->
        <li><a href="#/home">home</a></li>
        <li><a href="#/about">about</a></li>
    
        <!-- 渲染路由对应的 UI -->
        <div id="routeView"></div>
      </ul>
    </body>
    

    这是 react 主界面的一个经典内容,当我们点击链接“home”或“about”的时候,相应的内容会渲染在 ID 为 routeView 的区域,就像投影在一个大屏幕一样。

    avatar

    图片 Hash Router 上面的部分不在本示例 html 代码内。

    下面我们通过 js 写路由逻辑:

    
    window.addEventListener('DOMContentLoaded', onLoad)
    // 监听路由变化,DOMContentLoaded 表示页面加载结束时触发
    window.addEventListener('hashchange', onHashChange)
    
    // 路由视图
    var routerView = null
    
    function onLoad () {
      routerView = document.querySelector('#routeView')
      onHashChange()
      // 页面加载完不会触发 hashchange,这里主动触发一次 hashchange 事件,就会先在 routeView 区域上把“Home”显示出来
    }
    
    // 路由变化时,根据路由渲染对应 UI
    function onHashChange () {
      switch (location.hash) {
        case '#/home':
          routerView.innerHTML = 'Home'
          return
        case '#/about':
          routerView.innerHTML = 'About'
          return
        default:
          return
      }
    }
    

    hash 路由的优缺点

    优点:

    • 兼容性更好,兼容性达到了 ie8
    • 绝大数框架的框架都基本支持hash路由方式
    • 除了会发送ajax和资源加载之外不会发送其他请求
    • 不需要在服务端进行任何设置和开发

    换句话说,hash 只是我们前端自己玩的路由,跟服务端没有任何关系,服务端也根本拿不到 hash 的内容。当然,这也带来了一些缺点:

    缺点:

    • 服务端无法准确捕获路由的信息
    • 对于需要锚点功能的需求会与当前路由机制发生冲突
    • 对于需要重定向的操作,后段无法获取url全部内容,导致后台无法得到url数据,典型的例子就是微信公众号的oauth验证
    • 由于 hash 对于服务端不可见,所以对于 SEO 不友好
    • 还有一点,就是 URL 里用“#”号比较丑

    相对于 hash 机制,HTML5 还定义了一个相对更加复杂和强健的历史记录管理方法,就是下面要讲的 history 机制:

    history 模式

    history 机制包含 history.pushState()方法和 popstate 事件。当一个 Web 应用进入一个新的状态的时候,他会调用 history.pushState()方法将该状态添加到浏览器的浏览历史记录中。除了 pushState() 方法之外,History 对象还定义了 replaceState()方法,该方法和 pushState()方法接受同样的参数,但是他并不会将新的状态添加到浏览历史记录当中,而是用新的状态代替当前的历史状态。pushState()和replaceState()方法都不会造成页面刷新,只是用新的 URL 和 state 替换当前的 URL 和 state(pushState()方法还会将当前的 URL 和 state 加入 History 中)。

    注意,如果 pushState 的 URL 参数设置了一个新的 hash 值,并不会触发 hashchange 事件。

    pushState() 和 replaceState()参数

    pushState() 和 replaceState()都可以接受三个参数:

    第一个参数是一个对象,该对象包含用于恢复当前文档状态所需的所有信息。当我们触发 popState 事件时,该对象就会被传入回调函数,这样我们就可以恢复原来网址相关的状态信息;

    第二个参数是一个普通的文本字符串作为新页面的标题,但是目前几乎所有浏览器都会忽略这个值,所以一般这个参数就填 null;

    第三个参数是一个 URL,这个 URL 可以是任何与当前 URL 同源的 URL,但一般使用中都会指定相对 URL。相对的 URL 都是以文档的当前位置作为参照,通常该 URL 都是简单的指定 URL 的片段标识符部分(也就是他会被拼接在原 URL 的最后),将这样的一个 URL 与状态关联,可以允许用户将应用的内部状态作为数千添加到浏览器中,并当在 URL 中包含足够信息的时候,应用可以在从书签中载入的时候就恢复他的状态。

    简单实现 history 路由

    当用户通过“后退”和“前进”按钮浏览保存历史状态时,浏览器会在 Window 对象上触发一个 popstate 事件。而 popstate 事件的触发就是我们监听 URL 的变化从而改变页面内容的方式。与 hash 相比,history 模式中的 pushState()和replaceState()方法是 HTML5 定义的不会触发浏览器刷新而改变路由的办法,它并不需要在 URL 中添加“#”号,并且设置新的 URL 可以是任意与当前URL同源(协议、域名、端口相同)的URL,而 hash 只能改变“#”后面的内容,因此只能设置与当前 URL 同文档的 URL。

    在实现上,由于 popstate 事件只在前进(history.forward())、后退(history.back())、跳转(history.go())的时候才会触发,也就是说在点击 a 标签的时候仍然会刷新页面并向后台服务器请求,我们必须采用一些别的办法拦截这些请求。

    这里要补充说明一点:popState 事件的触发是有条件的,具体为一下两条:

    1. 处在同一个文档(即同一个 html 页面)

    2. 文档的浏览历史(即 History 对象)发生改变

    这个实现的例子中我们采用 event.preventDefault() 方法阻止向后台发出的请求。简单实现代码如下:

    html部分:

    <body>
      <ul>
        <li><a href='/home'>home</a></li>
        <li><a href='/about'>about</a></li>
    
        <div id="routeView"></div>
      </ul>
    </body>
    

    JavaScript部分:

    // 页面加载完不会触发 popState,这里主动触发一次 popState 事件
    window.addEventListener('DOMContentLoaded', onLoad)
    // 监听路由变化
    window.addEventListener('popstate', onPopState)
    
    // 路由视图
    var routerView = null
    
    function onLoad () {
      routerView = document.querySelector('#routeView')
      onPopState()
    
      // 拦截 <a> 标签点击事件默认行为, 点击时使用 pushState 修改 URL并手动更新 UI,从而实现点击链接更新 URL 和 UI 的效果。
      var linkList = document.querySelectorAll('a[href]')
      linkList.forEach(el => el.addEventListener('click', function (e) {
        e.preventDefault()
        history.pushState(null, '', el.getAttribute('href'))
        onPopState()
      }))
    }
    
    // 路由变化时,根据路由渲染对应 UI
    function onPopState () {
      switch (location.pathname) {
        case '/home':
          routerView.innerHTML = 'Home'
          return
        case '/about':
          routerView.innerHTML = 'About'
          return
        default:
          return
      }
    }
    

    说明一点,这里在调用 pushState()之后还调用了一次 onPopState()是因为我们这个案例中并没有将页面显示的内容与 state 参数相关联,而是完全依赖 URL 的变化调整 UI。所以既然 pushState()不会触发 popState 事件,我们就要手动的触发它的回调函数 onPopState()。关于 pushState()的作用,他只是简单的向浏览历史中添加了一条记录,实际上并没有改变页面的内容,仅仅改变了 URL 并且将其与一个 state 相关联,具体要展现页面的内容还是要通过其他的方法。

    history 路由的优缺点

    优点:

    • pushState()设置的新的 URL 可以是任何与当前 URL 同源的 URL,而 hash 只能改变 # 后面的内容,因此只能设置与当前URL同文档的 URL;

    • pushState()设置的URL与当前 URL 一模一样时也会被添加到历史记录栈中,而hash模式中,# 后面的内容必须被修改才会被添加到新的记录栈中;

    • pushState()可以通过第一个 state 参数添加任意类型的数据到记录中,而hash只能添加短字符串;

    • pushState()可额外设置 title 属性供后续使用;这里由于浏览器一般会忽略这个参数,所以目前还不能算作是一个很大的优点;

    • 没有 # 号, URL 更~美~了~

    缺点:

    • hash 模式下,只有 # 符号之前的内容才会包含在请求中被发送到后端,也就是说虽然后端没有对路由全覆盖,但是不会返回 404 错误;但 history 模式下,前端的 URL 必须和向发送请求后端 URL 保持一致,否则会报 404 错误。

    参考链接:

    history对象详解及单页面路由实现:http://www.fly63.com/article/detial/2063

    前端路由各种实现方案的对比:https://juejin.im/post/5b5ec5dd6fb9a04fc564b72d

    前端路由原理解析和实现:https://www.cnblogs.com/lguow/p/10921564.html


 

Copyright © 2018 bbs.dian.org.cn All rights reserved.

与 Dian 的连接断开,我们正在尝试重连,请耐心等待