单页应用与前端路由实现(详解)

摘要

本文介绍了单页应用和路由的概念,并介绍了如何用两个技术:hash模式和history模式来实现单页应用的效果。

1. 内容

  • 什么是单页应用
  • 单页应用的特点
  • 用路由来实现单页应用
  • hash模式
  • history模式
  • 总结

2. 什么是单页应用

单页应用的英文是single page application,简称SPA。字面含义就是一个应用只有一个页面,这其实是一个很颠覆的概念,有点反常识。按我们一般意义上的理解,一个应用肯定得有多个页面呀。例如:一个新闻网站必须有主页,列表页,详情页等等。我们可能想象不到一个只有一个页面的应用是啥?“就一个主页”,“就一个静态的个人说明页面”。

肯定不是。我们多花点时间看看历史。

早先的网站都是多页的,也就是字面意义上的多页:主页可能就是index.html, 列表页就是list.html,详情页就是detail.html等等,这样的好处就是开发清晰,3P(asp, jsp, php)技术就典型的代表。随着移动设备的兴起,越来越多的页面时候通过移动设备的来访问的,那多页面的应用的天然缺陷就显示出来了:

  1. 页面跳转时不可避免的会出现白屏;2. 从a页面跳转到b页面时,难免有会有重复的部分(例如页头和页脚一般都是通用的),而这些重复部分显然也会造成不必要的流量浪费。这些问题在pc端都是不问题,但是在移动设备上就是大问题了,为了提升用户的体验,让用户更快的看到页面,节约流量,程序员们发明了单页应用技术。

单页应用的主要特点就是:用一个页面(页面的名字就是index.html)来实现所有(多个页面的)的功能。

其实,SPA、MPA这些名称都是随着技术发展应用而提出来的,就像智能手机和功能手机的这对概念,在诺基亚风靡世界时(2010年左右),它们并没有被创造出来,我们讨论的话题都是 多少和弦 上,但是当出来一批更强大,更聪明的手机时,我们要做区分呀,就给诺基亚们取了名字叫功能机。类似的概念还有服务器端渲染和客户端渲染。

3. 单页应用的特点

单页应用的特点很明显:地址栏的变化导致内容的变化,但是整个页面并不刷新。下面是一张示意图:

图片

它的好处是第一次获取到index.html这个页面之后,再也不需要请求其他的页面了,所有的功能操作都在这一个页面中完成。

4. 单页应用的实现方式-路由

现在我们抛开单页应用这名词放一边,把注意力放在如何实现具体功能上:地址栏中的内容变化了,主体内容有对应的更新,但是整体页面不去刷新。

就是我们说的前端路由技术。

路由是啥?可不是路由器哈。

路由就是一套规则:根据地址栏中地址来决定页面主体上的内容显示。例如:当地址是/list时,内容就是列表页的内容。当地址是/detail时,内容就是详情页的内容,依次类推。

实现路由功能由两种方式:

  • hash模式。它主要是监听浏览器的hashchange事件。
  • history模式。它主要是调用history的pushState,replaceState方法,监听popstate事件。

都是利用浏览器的相关api特性来达成目标,严格来讲,都不属于js的内容。下面我们来分别介绍这两种技术。

5. hash模式

相比较history模式,用hash模式来实现这个功能会比较简单。

5.1 原理

当页面中的地址栏从 index.html/#/detail  变成 index.html/#/list 时:

  • 页面并不会刷新。
  • 会触发hashChange事件。这将是我们代码实现的重点。
  • 会向浏览器中追加一条浏览记录。也就是说通过浏览器的前进和后退按钮可以切换内容。

1.gif

5.2 代码

<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Document</title>
  <style>
    body {
      background-color: #eee;
    }
    #app {
      background-color: #fff;
      margin: 2px auto;
      padding: 2em;
    }
    #content{padding:0.5em}
</style>
</head>
<body>
  <div id="app">
    <nav>
      <a href="#/">主页</a>
      <a href="#/list">列表页</a>
      <a href="#/detail">详情页</a>
    </nav>
    <button id="btn">跳到详情页</button>
    <div id="content">
    </div>
  </div>
  <script>
    let content = null
    // 页面加载完不会触发 hashchange,这里主动触发一次 hashchange 事件
    window.addEventListener('DOMContentLoaded', function(){
      content = document.querySelector("#content")
      onHashChange()
    })
    // 页面跳转:直接设置hash值的变化
    document.getElementById('btn').addEventListener('click',()=>{
      location.hash='/detail';
    })
    
    // 监听路由变化
    window.addEventListener('hashchange', onHashChange)
    function onHashChange(e) {
      console.log(e)
      switch (location.hash) {
        case "#/":
          content.innerHTML = "主页的内容"
          break;
        case "#/list":
          content.innerHTML = "列表页的内容"
          break;
        case "#/detail":
          content.innerHTML = "详情页的内容"
          break;
        default:
          content.innerHTML = "404"
          break;
      }
    }
</script>
</body>
</html>
复制代码

5.4 小结

hash模式在地址栏上有一个奇怪的符号 #,让地址看起比较奇怪。

6. history模式

6.1 原理

pushState方法 + popstate事件

pushState方法

它其实是history对象上的一个方法,其签名是:history.pushState(state,title,[url])  。它的作用是:

(1)向当前浏览器会话的历史堆栈中添加一个状态(state),它改变历史记录;

(2)改变地址栏的信息;

(3)它不会刷新页面去发请求。

你现在可以任意打开一个浏览器,然后在控制台中,依次写入如下三句代码,并敲回车运行

history.pushState(null,null,'/a')
history.pushState(null,null,'/b')
history.pushState(null,null,'/c')
复制代码

效果图如下:

2.gif

请注意如下几点细节:

  1. 地址栏中有变化。从 baidu.com/a ->baidu.com/b ->baidu.com/c

  2. 页面并没有刷新。

  3. 浏览器的前进后退按钮可以使用了。

但是,千万别点击刷新按钮,因为这会导致浏览器真的去请求 baidu.com/a 这个页面,而这个页面是不存的。

replaceState方法

它的功能与pushState类似,不同的地方在于 replace(替换) 效果。 举个例子来说:如果地址的变化是依次用pushState方法从按a,b,c的顺序来修改地址栏,此时,历史记录就是:/a->/b->/c ,此时通过 前进后退按钮就可以在这三条记录中切换。但是,如果 此时使用replaceState,跳转到/d,就会将当前的访问记录替换掉,历史记录就是 /a->/b->/d ,最近那条记录/c 就被/d替换了。具体效果见下图:

3.gif

事件-popstate

当活动历史记录条目更改时,将触发popstate事件。

window.addEventListener('popstate', ()=>{console.log('popState事件',location.pathname)})
复制代码

我们通过上面的代码就可以去监听popstate事件了。那什么情况下会触发这个事件呢?有如下两类操作:

  1. 点击浏览器的前进,回退按钮
  2. 调用history.back(), history.forward(), history.go()等操作历史记录的方法。

需要注意的是调用 history.pushState() 不会触发 popstate 事件。

如果被激活的历史记录条目是通过对history.pushState()的调用创建的,或者受到对history.replaceState()的调用的影响,popstate事件的state属性包含历史条目的状态对象的副本。

不同的浏览器在加载页面时处理popstate事件的形式存在差异。页面加载时Chrome和Safari通常会触发(emit )popstate事件,但Firefox则不会。

6.2 目标

有了上面的理论基础之后,我们来看看我们将要实现的目标。具体如下

4.gif

它的效果是点击了a标签之后,产生地址栏的变化,同时主体内容也要跟着变化。注意哈,这过程中地址栏中并没有#号。

6.3 思路

可以分成两个部分来完成这个目标:

  1. 劫持a标签的跳转动作,在回调中:

    调用pushState,它会修改地址栏的地址(可以通过location.pathname来获取)
    根据location.pathname的值来展示内容
    复制代码
  2. 监听popstate事件,当浏览器的前进后退动作发生时,能根据当前的pathname的内容来展示内容。

window.addEventListener('popstate', 
   ()=> { 根据location.pathname的变化来展示内容}
)
复制代码

6.4 代码

<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Document</title>
  <style>
    body {
      background-color: #eee;
    }
    #app {
      background-color: #fff;
      margin: 2px auto;
      padding: 2em;
    }
    #content{padding:0.5em}
</style>
</head>
<body>
  <div id="app">
    <nav>
      <a href="/">主页</a>
      <a href="/list">列表页</a>
      <a href="/detail">详情页</a>
    </nav>
    <button id="btn">跳到详情页</button>
    <div id="content">
    </div>
  </div>
  <script>
    let content = null
    window.addEventListener('DOMContentLoaded', onLoad)
    // 直接跳转页面
    document.getElementById('btn').addEventListener('click',()=>{
      history.pushState(null,null,'/detail')
      onPopStateChange()
    })
    function onLoad() {
      // 更新页面内容
      content = document.querySelector("#content")
      onPopStateChange()
            
      // 拦截a标签
      var aLinks = document.querySelectorAll('a[href]')
      aLinks.forEach((a,idx)=> {
        a.addEventListener('click',e => {
          e.preventDefault()
          history.pushState(null,null, e.target.getAttribute('href'))
          onPopStateChange()
        })
      })
    }
    
    // 监听popstate事件变化
    window.addEventListener('popstate', onPopStateChange)
        // 实现具体的路由功能:根据地址完成页面内容切换
    function onPopStateChange(e) {
      console.log(e,Date.now())
      switch (location.pathname) {
        case "/":
          content.innerHTML = "这里是主页的内容"
          break;
        case "/list":
          content.innerHTML = "这里是列表页的内容"
          break;
        case "/detail":
          content.innerHTML = "这里是详情页的内容"
          break;
        default:
          content.innerHTML =  `您访问的${location.pathname}不存在`
          break;
      }
    }
</script>
</body>
</html>
复制代码

6.5 小结

history模式下,地址栏中没有了#,地址看起来比较正常。它用到了一些H5中相对高级的API(pushState),要注意浏览器的兼容性问题,同时,它可经不住刷新页面,因为刷新时,浏览器就真的按这个地址会去请求这个页面资料,而很可能服务器上并没有这个页面资源!

7. 总结

单页应用是在移动互联时代诞生的,它的目标是不刷新整体页面,通过地址栏中的变化来决定内容区域显示什么内容。要达成这个目标,我们要用到前端路由技术,具体来说有两种方式来实现:hash模式和history模式。hash模式是通过监听hashChange事件来实现的,history模式是通过pushState方法+popstate事件来实现的。

转载自 mp.weixin.qq.com/s/yTq-yZYAk…

猜你喜欢

转载自juejin.im/post/7030771313979441160