努力让学习成为一种习惯,自信来源于充分的准备
如果你觉得该文章对你有帮助,欢迎大家点赞、关注和分享
前言
SPA是当前主流的Web应用程序开发方式,诸如Angular、React、Vue等面向网页浏览器的JavaScript框架均采用了SPA的理念 ,SPA的核心关键点之一便是路由
很多Javascript框架都有专门对应的路由库如:react-router、vue-router,本文内容不涉及这些路由库原理、源码解析。而是从前端历史发展的角度开始,一步一步剖析前端路由本身。了解其本质和原理
AJAX
引入mdn的定义
AJAX(Asynchronous JavaScript And XML )是一种在 Web 应用中通过异步发送 HTTP 请求向服务器获取内容,并使用这些新内容更新页面中相关的部分,而无需重新加载整个页面的 Web 开发技术。这可以让网页更具有响应性,因为只请求了需要更新的部分
其中AJAX的最为核心技术为XMLHttpRequest,在这项技术出现之前。人们认为Web应用就是由一系列连续切换的页面组成的。整个Web应用被划分成了大量的页面,其中大部分是一些很小的页面。浏览器向服务器发起一个或多个 HTTP 请求以获取显示网页所需的文件,然后服务器响应请求的文件。如果你访问另一个页面,浏览器会请求新的文件,服务器则会响应这些请求,用户大部分的交互都需要切换并且刷新整个页面,而在这个过程中(下一个页面完全显示出来之前),用户只能呆呆地等着,什么事都做不了
但有一个致命的问题,在同一个页面内,浏览器向服务器提交数据只能通过HTML表单的提交,从服务器获取数据只能通过点击一个超链接,这也意味着每次从服务端获取资源数据都会刷新整个页面(哪怕我们仅仅是需要更新页面的极小一部分)
XMLHttpRequest为运行在浏览器中的JavaScript脚本提供了一种在页面之内与服务器通信的手段。页面内的 JavaScript可以在不刷新页面的情况下从服务器获取数据,或者向服务器提交数据。实现了页面的局部数据动态更新,用户体验大增。这也为后续SPA的繁荣埋下了种子
SPA与MPA
SPA
SPA (Single Page Application,单页面应用程序)是一种Web应用程序开发模式。整个web应用有且只有一个 index.html文件,网页初次加载所需要的文件资源(js、css、图片等)后,剩下的都基于javascript动态获取资源数据、修改页面
我们拿vue项目举例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
所有视图层的渲染更新(页面跳转、数据变更)、交互都是发生在id为app的dom元素内(无需刷新整个页面,利用JavaSctipt更新局部内容)
简而言之:SPA一个大特点是通过在浏览器中动态重写当前页面,而非从服务器加载整个新页面,来实现应用程序的快速响应和无刷新加载。从而大大提升用户体验
MPA
MPA(Multi-Page Application,多页面应用程序)是传统的Web应用程序开发模式,和SPA形成对比,它具有以下特点
- 每个页面都对应一个独立的HTML文件,用户在浏览时需要等待整个页面刷新
- 页面之间的跳转需要从服务器重新加载整个新页面
- 页面逻辑和数据都由服务器端代码(如PHP、Java、Python等)处理和渲染
- 客户端主要负责展示由服务器返回的HTML页面内容
- 页面之间的状态和数据不能实现无刷新的更新,需要全页面刷新
- 用户体验相对单一,页面切换时会有明显的等待时间
两者对比
| SPA | MPA | |
|---|---|---|
| 页面组成 | 一个整体html页面➕多个局部dom节点 | 多个html页面 |
| 刷新方式 | 局部dom节点刷新变化 | 整个html页面刷新 |
| 数据传递 | 方式多种 | 更多依赖浏览器本身(cookie、localStorage,url路径等) |
| SEO | 不友好,可使用SSR改善 | 对搜索引擎友好,每个页面都可以被独立索引 |
| 路由控制 | 前端控制 | 主要依赖后端 |
| 用户体验 | 应用初始加载相对慢些,但是页面切换迅速,用户体验好 | 每一次页面的切换都需要整体刷新页面,加载资源。耗时长,体验不好 |
总的来说,MPA是一种传统的Web应用程序开发模式,适合某些特定场景下的应用。随着前端技术的发展,SPA正逐渐成为主流的Web应用程序开发方式
路由
AJAX技术出现后,慢慢才有了SPA,不管是向服务器发送请求获取资源数据,还是页面跳转导致的dom更新。
都不需要重新刷新整个页面,只是局部更新,大大提升了用户体验。但前端页面的url始终没有变化,这会导致以下问题
- 没法记住用户的操作记录,当我们后退、前进、刷新页面的时候。无法复现、满足用户当时的场景(比如我想要通过一个具体外链访问某个业务功能页)
- 对SEO不友好,因为从始至终只有一个url
路由最早是由后端提出的概念,我们在浏览器输入一串url,本质就是在向某个具体的服务器地址获取资源(文件(html、js、css、图片等),json等)
引入mdn对URL的描述
URL 代表着是统一资源定位符(Uniform Resource Locator)。URL 无非就是一个给定的独特资源在 Web 上的地址。理论上说,每个有效的 URL 都指向一个唯一的资源。这个资源可以是一个 HTML 页面,一个 CSS 文档,一幅图像
为何不借鉴后端路由的思想呢,一个后端路由地址映射的是一个存放在服务器中的资源。那么一个前端路由映射的就是一块页面内容(更准确点是一块DOM),数据结构类似下面这种
const route = [{
path: '/a',
component: '我是a页面'
},
{
path: '/b',
component: '我是b页面'
}
]
整体思路有了,实现前端路由有两个需要处理的问题
- 修改url怎么不刷新页面
- 怎么监听url的变化
hash模式
hash模式即是采用url的hash部分作为前端路由
拿http://www.example.com/index.html#print 举例。其hash值为#print
hash的特点是:#是用来指导浏览器动作的,对服务器端完全无用。所以,HTTP请求中不包括#。第一个#后面出现的任何字符,都会被浏览器解读为位置标识符。这意味着,这些字符都不会被发送到服务器端,hash值的改变不会导致页面的重新刷新(额外提一下除了hash,其他Location对象的任意属性发生改变都会向服务器发送请求,网页会重新刷新)
当hash值发生变化时,浏览器会将其记录在浏览历史中。这意味着用户可以通过浏览器的前进和后退按钮来导航到之前访问过的不同hash值对应的页面状态,同时我们可以通过hashChange事件来监听hash的变化
接下来我们一起来实现个简易的hash模式路由
页面结构
<ul>
<li><a href="#/a">11</a></li>
<li><a href="#/b">22</a></li>
</ul>
<div id="router-view"></div>
定义路由映射表
const route = [
{
path: '#/a',
component: '我是a页面'
},
{
path: '#/b',
component: '我是b页面'
}
]
监听hash值的变化,获取当前hash值,从映射表获取对应组件,修改dom
const routerViewEl = document.querySelector('#router-view')
window.addEventListener('hashchange', hashChangeHandler)
function hashChangeHandler () {
const hash = location.hash
const r = route.find(r => r.path === hash)
if (r && r.component) {
routerViewEl.innerHTML = r.component
}
}
另外有一个细节点,初次页面加载完成的时候,并不会触发hashchange事件,因此我们需要手动设置一次
window.addEventListener('DOMContentLoaded', hashChangeHandler)
hash模式有着天然的优势,但它有以下缺点
- 不美观,#符号怎么看都像是锚点。不像是一个正式的网页地址
- 不利于SEO,因为本质上和服务器请求的url始终没变
在HTML5history模式出现之前,基本都是使用 hash 模式来实现前端路由
history模式
HTML5 引入了 History API,它提供了一种新的管理浏览器历史记录的方式,称为 History 模式。这种模式相比于使用 location.hash 来管理历史记录有以下优点:
-
更优雅的URL结构:使用 location.hash 会在 URL 中出现 # 符号,这可能并不理想。 History 模式可以使用普通的 URL 路径,看起来更加自然
-
更好的SEO支持:搜索引擎通常不会索引 location.hash 之后的内容,这可能影响 SEO。 History 模式下的 URL 可以被完整地索引,有利于 SEO
-
更好的用户体验:用户可以在地址栏中看到正确的 URL,更容易理解当前页面的状态。 用户可以使用浏览器的前进后退按钮来导航,感觉更加自然
可以使用pushState或replaceState方法来更新浏览器历史记录(url会发生改变,但不会发送请求从而刷新页面),另外可以通过popState事件来监听浏览器的前进与后退(并没有原生事件支持监听pushState、replaceState)
接下来我们来实现一个简易的history模式
<ul>
<li><a href="/a">11</a></li>
<li><a href="/b">22</a></li>
</ul>
<div id="router-view"></div>
const routerViewEl = document.querySelector('#router-view')
const route = [{
path: '/a',
component: '我是a页面'
},
{
path: '/b',
component: '我是b页面'
}
]
const ulEl = document.querySelector('ul')
ulEl.addEventListener('click', (e) => {
// 必须阻止a链接点击的默认行为
e.preventDefault()
const href = e.target.getAttribute('href')
history.pushState(null, '', href)
render(href)
})
window.addEventListener('popstate', (e) => {
render(location.pathname)
})
function render(path) {
const r = route.find(r => r.path === path)
if (r && r.component) {
routerViewEl.innerHTML = r.component
}
}
需要额外注意:使用history模式,刷新页面的时候会404(向服务器发送请求获取资源,显然服务器并没有该路由),要解决这个问题,我们需要进行后端配置,具体可以参考vue-router
最后
我们简单总结下:AJAX技术的横空出世带使前端大部分网页采用SPA的模式。同时基于SPA自身的一些缺点需要引入前端路由。本质需要解决的问题为:url更换不能刷新页面,监听url的变化
前端路由的模式有两种:hash模式和history模式。各有各的特点,条件允许的情况下,更推荐使用 history模式
到这里,就是本篇文章的全部内容了
如果你觉得该文章对你有帮助,欢迎大家点赞、关注和分享
如果你有疑问或者出入,评论区告诉我,我们一起讨论