前端如何理解单应用页面,实现思路是什么?

2,524 阅读5分钟

本文核心内容解决三个问题:

  1. 单页面应用【优点】在哪?
  2. 单页面应用的【核心】是什么?
  3. 如何【实现】以一个浏览器路由

什么是单页面应用?

单页面应用 简称 “SPA(Single Page Application)”,与多应用页面相对。

单页面的优点是什么?

传统多应用页面中,切换一个页面 分为五个阶段

  1. 浏览器URL发生改变,页面发送请求给服务端
  2. 服务端接收到请求,响应请求返回html
  3. 浏览器接收到html响应,开始加载html
  4. 加载html依赖的静态资源
  5. 加载完毕后将内容渲染到页面

单页面应用中,路由跳转分为两个阶段

  1. 浏览器URL发生改变,页面监听URL变化,发起对应URL的静态资源请求
  2. 加载完毕后,渲染内容

所以,在单页面应用跳转新页面过程中,其实并没有发起完整的新 HTML 页面请求,只是加载对应的资源,渲染修改指定的页面内容。所以,相比多页面应用,单页面应用,减少了服务端路由操作,也就减少了多个 HTTP 请求和响应的过程,最后就缩短了等待时间。

同时也说明单页面应用是通过浏览器层面控制URL(浏览器路由),代替服务端路由进行页面跳转,减少 HTTP 请求,提升页面切换速度。所以,单页面应用的核心能力就是浏览器路由能力

实现浏览器路由的两种方案

目前,浏览器实现类似功能有两种方式,一种是 URL 的 hash 值控制,另外是浏览器的 history 控制。

hash 模式

这里说的hash,是 URL 末尾带上 # 字符开头的字符,例如,#/user 就是 URL 里的 hash 值。

http://example.cn/#/user

传统 URL 的 hash 用于标志页面上的某个锚点,只要页面不存在锚点位置,页面就不会发生任何变化,也不会发生任何页面跳转。利用 hash 的非锚点特性,当 hash 发生变化的时候,通过监听 hash 变化,加载指定资源渲染页面,就不需要请求整个html页面;

const linkDom = document.querySelector('#links')
const viewDom = document.querySelector('#view')
const linkList = {
  '#/': '欢迎来到Morakes主页',
  '#/404': 'page not found',
  '#/home': '主页',
  '#/about': '关于页',
  '#/about/deep': '详情页',
}

// 渲染路由
const renderRouter = function () {
  const htmlList = []
  const linkKeys = Object.keys(linkList)
  linkKeys.forEach((key) => {
    htmlList.push(`<li><a class="${key === location.hash ? 'active' : ''}" href="${key}">
      ${key}</a></li>`
    )
  })
  linkDom.innerHTML = htmlList.join('')
}

// 渲染视图
const renderView = function () {
  const hash = window.location.hash
  const routePath = hash.split('?')[0]
  viewDom.innerHTML = linkList[routePath] || renderRouter['#/404']
}

renderRouter()
renderView()

// 监听 hash 值的变化
addEventListener('hashchange', () => {
  renderRouter()
  renderView()
})

通过监听 hashchange 变化,我们很容易就实现了一个浏览器路由。真正的开发中,实现起来肯定比刚才写的复杂多了,这里仅仅是作为一个思路的参考,不具备开发性质。

history 模式

基于浏览器的 history 特性,它也可以自定义浏览器 URL 变化,同时保持页面不会重新刷新加载。

history就是操作浏览器的历史记录,在浏览器里,访问历史就是记录或者控制 URL 的变化。按这个思路,history 能通过新增一个“访问历史”来影响当前浏览器的 URL 变化,同时,这个变化的 URL 也是可以自定义的。

  const linkDom = document.querySelector('#links')
  const viewDom = document.querySelector('#view')
  const linkList = {
    '/': '欢迎来到Morakes主页',
    '/404': 'page not found',
    '/home': '主页',
    '/about': '关于页',
    '/about/deep': '详情页',
  }

  // 渲染路由  将路由绑定到自定义属性data-href,控制href属性改变浏览器 URL
    const renderRouter = function () {
        const htmlList = []
        const linkKeys = Object.keys(linkList)
        linkKeys.forEach((key) => {
          htmlList.push(`<li><a class="${key === window.location.pathname ? 'active' : ''}" href="javascript: void(0)" data-href="${key}">${key}</a></li>`
          )
        })
        linkDom.innerHTML = htmlList.join('')
  }
  // 渲染视图
  const renderView = function () {
    const currentPath = window.location.pathname
    viewDom.innerHTML = linkList[currentPath] || linkList['/404']
  }
  renderRouter()
  
  // 人工注册 history 路由的 pushState 和 replaceState 监听 因为浏览器原生不支持
  // type = pushState || replaceState
  const registerHistoryListener = function (type) {
    const originFunc = window.history[type]
    // 自定义pushState || replaceState 事件,后续对它们进行监听
    const e = new Event(type);
    return function() {
      const result = originFunc.apply(this, arguments)
      e.arguments = arguments
      // 将事件派发到window,当触发history pushState || replaceState 事件的时候 addEventListener 可以监听到变化
      window.dispatchEvent(e)
      return result
    }
  }
  // 利用 history 的特性,给pushState&&replaceState进行赋值,之后我们就可以自己控制浏览器URL的跳转
  window.history.pushState = registerHistoryListener('pushState')
  window.history.replaceState = registerHistoryListener('replaceState')
  
  // 监听history.pushState
  window.addEventListener('pushState', () => {
    renderRouter()
    renderView()
  })
  // 监听history.replaceState
  window.addEventListener('replaceState', () => {
    renderRouter()
    renderView()
  })

  linkDom.addEventListener('click', (e) => {
    // 拦截事件,做history跳转操作
    const dataHref = e.target.dataset.href
    if (dataHref) {
      window.history.pushState({}, '', dataHref)
    }
  })

  renderRouter()
  renderView()

history 模式实现起来比 hash模式明显要更加复杂一些;

两种路由模式的对比

在实际开发中,hash 路由和 history 路由最大的差异就是浏览器兼容的差异。hash 路由是利用浏览器传统的 URL hash 锚点能力,这个特性,绝大部分浏览器都兼容。

image.png

(hash兼容性)

image.png

(history兼容性)

如果单页面应用的业务需求要兼容 IE8 等低版本浏览器,那你就只能选择 hash 路由,如果对浏览器兼容要求不高,两者都可以。

除了浏览器这个兼容性差异,它们对页面所在服务端的要求也不同。hash 路由只改变 URL 的 hash 值,当前页面强制浏览器刷新时,还能保持在当前页面。但是,history 路由是真的改变了浏览器 URL 的路径 path,当前页面强制浏览器刷新时,要走一遍服务器请求,查询当前 URL 是否存在,所以要保持 history 路由改变的 URL 存在,服务器路由就必须跟前端路由同步。

总结

单页面应用,目前有两种主流的技术实现方式,一种是通过 hash 路由来实现,另一种是通过 history 路由来实现:

  • hash 路由的浏览器兼容比较好,可以兼容到 IE8 浏览器,但是 URL 的格式就限制必须用 # 号的 hash 值来标识。
  • history 路由对浏览器有一定要求,同时如果浏览器强制刷新,就需要服务端做服务端层面的路由支持,但是history 路由的 URL 格式和正常的服务路由一致。

另外我们还总结了单页面路由的核心原理,你要重点看一下:

  • 利用 hash 或者 history 进行无刷新修改浏览器 URL。
  • 监听 URL 变化执行页面内容更新或切换。
  • 如果用 history 路由,就要考虑服务器路由的同步和浏览器的兼容。

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情