深入理解 SPA 单页面应用
一、什么是 SPA
SPA(Single-Page application)单页面应用通过动态重写当前页面来与用户交互,避免了页面切换时的重新加载,从而提升用户体验,SPA 在页面初始化时加载必要代码,之后不需要重新加载,常见的 React、Vue、Angular 都属于 SPA。
优点:
- 具有桌面应用的即时性、网站的可移植性和可访问性;
- 用户体验好,内容改变时不需要重新加载整个页面;
- 良好的前后端分离,分工更明确。
缺点:
- 不利于搜索引擎的抓取;
- 首次渲染速度相对较慢。
二、SPA 与 MPA 的区别
SPA(Single-Page application)是单页面应用,而 MPA(MultiPage-Page application)是多页面应用,每个页面都是独立的,访问时都需要重新加载 HTML、CSS、JS 文件,具体区别如下:
单页面应用(SPA) | 多页面应用(MPA) | |
---|---|---|
组成 | 一个主页面和多个页面片段 | 多个主页面 |
刷新方式 | 局部刷新 | 整页刷新 |
url 模式 | 哈希模式 | 历史模式 |
SEO 搜索引擎优化 | 难实现,可使用 SSR 改善 | 易实现 |
页面间数据传递 | 容易 | 通过 url、cookie、localStorage 等方式传递 |
页面切换速度 | 速度快,用户体验良好 | 切换加载资源,速度慢,用户体验差 |
维护成本 | 相对容易 | 相对复杂 |
三、SPA 的原理及实现
SPA 的原理就是通过 JS 监听 URL 的变化,对页面进行处理。前端路由主要有 hash 和 history 两种实现方式。
1、hash 模式
hash 模式就是在 url 后加上 #
和指定的字符。例如 www.leophen.cn/#/pageA , 这里的 #/pageA 就是 hash。
hash 的改变不会导致浏览器发送请求,但会触发 hashchange ,浏览器的前进、后退也能操作 hash。基于这三点,通过监听 url 中的 hash
来进行路由跳转,实现如下:
- HashRouter 定义
- HashRouter 使用
class HashRouter {
// 存放路由
constructor() {
this.routes = {}
}
// 注册路由
registry(path, callback = () => { }) {
this.routes[path] = callback
}
// 更新视图
updateView() {
const hash = location.hash.slice(1) || '/'
this.routes[hash]?.()
}
// 初始化
init() {
window.addEventListener('load', this.updateView.bind(this), false)
window.addEventListener('hashchange', this.updateView.bind(this), false)
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>SPA Hash Router</title>
</head>
<style></style>
<body>
<a href="#/">Home</a>
<a href="#/pageA">Page A</a>
<a href="#/pageB">Page B</a>
<a href="#/pageC">Page C</a>
<div id="root"></div>
</body>
<script src="./hash-router.js"></script>
<script>
const router = new HashRouter()
const content = document.getElementById('root')
router.init()
router.registry('/', () => content.innerHTML = 'Home')
router.registry('/pageA', () => content.innerHTML = 'Page A')
router.registry('/pageB', () => content.innerHTML = 'Page B')
router.registry('/pageC', () => content.innerHTML = 'Page C')
</script>
</html>
实现效果:
2、history 模式
HTML5 之前主要使用 back、forward、go 等方法实现页面跳转,而 HTML5 之后提供了 history.pushState
和 history.replaceState
来添加或修改历史记录,history 模式就是利用 pushState
和 replaceState
来实现路由。
history 模式的核心:
history.pushState
:在保留现有历史记录的同时,将 url 加入到历史记录中;history.replaceState
:将历史记录中的当前页面历史替换为 url。
- HistoryRouter 定义
- HistoryRouter 使用
class HistoryRouter {
// 存放路由
constructor() {
this.routes = {}
}
// 注册路由
registry(path, callback = () => { }) {
this.routes[path] = callback
}
// 更新视图
updateView(path, type) {
if (type === 'init') {
history.replaceState({ path }, null, path);
} else {
history.pushState({ path }, null, path);
}
this.routes[path]?.()
}
// 初始化
init() {
window.addEventListener('load', () => this.updateView('/', 'init'), false)
window.addEventListener('popstate', () => {
this.updateView(window.location.pathname, 'init')
})
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>SPA History Router</title>
</head>
<style></style>
<body>
<a onclick="onRouter('/')">Home</a>
<a onclick="onRouter('/pageA')">Page A</a>
<a onclick="onRouter('/pageB')">Page B</a>
<a onclick="onRouter('/pageC')">Page C</a>
<div id="root"></div>
</body>
<script src="./history-router.js"></script>
<script>
const router = new HistoryRouter()
const content = document.getElementById('root')
router.init()
router.registry('/', () => content.innerHTML = 'Home')
router.registry('/pageA', () => content.innerHTML = 'Page A')
router.registry('/pageB', () => content.innerHTML = 'Page B')
router.registry('/pageC', () => content.innerHTML = 'Page C')
const onRouter = (path) => {
router.updateView(path, 'update')
}
</script>
</html>
实现效果:
注意 history 模式下刷新页面时会 404,需要后端配合匹配路由。
3、hash 与 history 模式的区别
- hash 模式的兼容性比 history 模式(基于 HTML5)高;
- hash 模式会在 url 中夹带
#
符; - hash 模式不需要后端配合匹配路由,而 history 模式需要。
五、如何给 SPA 做 SEO
利用 SSR 服务端渲染给 SPA 做 SEO,将组件在服务端直接渲染成 HTML,再返回给浏览器,例如 Next.js、Nuxt.js