本文核心内容解决三个问题:
- 单页面应用【优点】在哪?
- 单页面应用的【核心】是什么?
- 如何【实现】以一个浏览器路由
什么是单页面应用?
单页面应用 简称 “SPA(Single Page Application)”,与多应用页面相对。
单页面的优点是什么?
传统多应用页面中,切换一个页面 分为五个阶段
- 浏览器URL发生改变,页面发送请求给服务端
- 服务端接收到请求,响应请求返回html
- 浏览器接收到html响应,开始加载html
- 加载html依赖的静态资源
- 加载完毕后将内容渲染到页面
单页面应用中,路由跳转分为两个阶段
- 浏览器URL发生改变,页面监听URL变化,发起对应URL的静态资源请求
- 加载完毕后,渲染内容
所以,在单页面应用跳转新页面过程中,其实并没有发起完整的新 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 锚点能力,这个特性,绝大部分浏览器都兼容。
(hash兼容性)
(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 天,点击查看活动详情