SPA 单页应用
# 单页应用
首先,在介绍单页应用之前,先来回顾一下传统的 web 应用开发方式。传统的 web 应用开发方式,就是多页应用。
在传统的多页应用模式下,对于大多数改变页面显示的行为,会将你导航到一个全新的页面。作为用户,我们这边是原来的页面被销毁,然后拿到一个新的页面。
这个交互方式在用户体验上是糟糕。例如用户点击“加入购物车”,新的页面和旧的页面的区别仅仅是购物车数量上面的区别,其他全部一模一样,但是整个页面也需要全部被销毁,然后重新渲染。
2005年,ajax的出现,让我们可以做到异步无刷新页面,让我们的用户体验大大的提升。因为 ajax 的出现,出现了一种新的交互模式,单页应用。英文全称:Single Page Application(SPA)。
单页应用的优点如下:
- 提供更加优秀的体验
传统的网站,许多的操作都会涉及到整张页面的刷新,需要在服务器组装整张页面,然后返回给浏览器,工作量很大,并且在绘制的时候,还有可能出现浏览器假死(未响应)或者浏览器闪烁的情况。但是如果是单页应用,只用请求页面片段,数据量就要小很多,用户体验也要好很多。
- 拥有和桌面应用一样的响应速度和体验
单页应用可以提供类似于桌面应用的用户体验。
单页应用的缺点如下:
- 对搜索引擎不友好
以前多页应用的时候,每个页面是什么内容,都是固定死了的,这样的页面对于搜索引擎来讲,就非常的友好,因为搜索引擎知道你这个页面是什么样的内容,但是单页应用恰好相反,一开始是一个空白页面,搜索引擎无法将你的页面抓取出来。
- 无法前进后退
传统的单页应用,可以使用浏览器的前进和后退按钮,但是单页应用就不行,因为单页应用一共就只有一个页面,内容都是动态加载的。
# 单页应用具体实现
单页应用的实现,实际上方式有很多种,但是都遵循一个原则,页面无刷新。
常见的单页应用的实现方式:
- 监听地址栏的 hash 的变化来驱动页面的变化
- 使用 pushstate 来记录浏览器的历史,从而驱动页面的变化
下面我们来演示一个使用 hash 模式实现的单页应用。
在一个完整 url 里面,可以有 hash部分
http://www.abc.com/index.html?id=1#abc
hash 除了做锚点以外,还有一个很重要的特点,锚点对应的值的改变,不会发送请求。
本来,URL 里面的任何一个字符的改变,都会导致浏览器重新请求服务器。但是,除了#后面的字符。换句话说,改变 # 后面的值,不会向服务器发送请求,但是会记录到浏览器的 history 里面。
所以,大多数的单页应用架构的网站,都是在 url 后面采用 # 来当作改变视图的一个信号。
例如:
abc.com/#index // 首页视图
abc.com/#list // 列表视图
abc.com/#list/1 // id 为 1 的项目视图
2
3
在 js 中,可以通过 window.location.hash 来获取 # 后面的值
示例代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
body {
height: 500px;
width: 100%;
margin: 0;
padding: 0;
}
div {
width: 100%;
height: 100%;
position: absolute;
font-size: 500px;
text-align: center;
display: none;
}
.a {
background-color: pink;
display: block;
}
.b {
background-color: red;
}
.c {
background-color: gray;
}
</style>
</head>
<body>
<div id="A" class="a">A</div>
<div id="B" class="b">B</div>
<div id="C" class="c">C</div>
</body>
<script>
function hashChanged(e) {
console.log(e); // 事件对象 HashChangeEvent
// 变化之后的 url
let newhash = e.newURL.split('#')[1];
// 变化之前的 url
let oldhash = e.oldURL.split('#')[1];
// 处理第一次 url
if (!oldhash) {
oldhash = "A";
}
// 将对应的 hash 下界面显示和隐藏
document.getElementById(oldhash).style.display = 'none';
document.getElementById(newhash).style.display = 'block';
}
//监听路由变化
window.onhashchange = hashChanged;
</script>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# 通过事件来实现单页
具体代码如下:
首先初始化项目,安装 express,然后搭建服务器
// 服务器文件
const express = require('express');
const app = express(); // 创建服务器实例
app.use(express.static(__dirname + '/public'));// 指定静态文件目录
app.listen(3000,function(){
console.log('服务器已经启动...');
})
2
3
4
5
6
7
8
接下来创建 public目录,创建index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="./index.js" type="module"></script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
接下来,创建 index.js,这是整个项目的入口文件
// 整个模块化的入口文件
import Nav from './modules/nav.js';
new Nav;
2
3
接下来,书写各个模块,如下:
// nav.js
// 导航模块
import Login from './login.js';
import Register from './register.js';
export default class Nav{
constructor(){
this.render(); // 调用 render 函数
this.handle(); // 绑定方法
}
// 渲染函数
render(){
let template = `
<div>
功能选择
<a href="#" id="login">登陆</a>
<a href="#" id="register">注册</a>
</div>
<div id="container"></div>
`;
// 接下来,我就需要将这个模板挂载到 index.html 里面
$('#app').html(template);
}
// 统一在 handle 方法里面绑定事件
handle(){
$('#login').click(function(){
new Login;
})
$('#register').click(function(){
new Register;
})
}
}
// login.js
// 登陆模块
export default class Login{
constructor(){
this.render();
}
// 渲染函数
render(){
let template = `
<div>
<h1>登陆</h1>
<div>
<input type="text" id="phone" placeholder="手机号">
</div>
<div>
<input type="password" id="password" placeholder="密码">
</div>
<div>
<input type="button" value="登陆">
<input type="button" value="注册">
</div>
</div>
`;
// 将模板进行挂载
$('#container').html(template);
}
}
// register.js
// 注册模块
export default class Register{
constructor(){
this.render();
}
// 渲染函数
render(){
let template = `
<div>
<h1>注册</h1>
<div>
<input type="text" id="phone" placeholder="手机号">
</div>
<div>
<input type="password" id="password" placeholder="密码">
</div>
<div>
<input type="password" id="password" placeholder="确认密码">
</div>
<div>
<input type="button" value="确认注册">
<input type="button" value="返回">
</div>
</div>
`;
// 将模板进行挂载
$('#container').html(template);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# 前端路由
# 什么事前端路由
路由这个概念,最早就是从后端出现的。最早的开发使用的是模板引擎,经常看到的路由长下面的样子:
http://www.abc.com/login
大致的流程:
(1)浏览器发出请求
(2)服务器监听请求,开始解析拿到的 url 地址
(3)根据服务器那边路由的配置,返回对应的数据(可能是网页、图片、css 等)
(4)浏览器拿到服务器返回的内容后,根据content-type来决定如何解析数据
但是现在,随着 ajax 的流行,单页应用出现了。单页应用的特点就是只有一个页面,这里就会有一个问题,无法前进后退以及刷新。为了解决这个问题,前端路由出现。前端路由决定你现在渲染哪个模块。
前端路由现在大致分为两种模式:
- hash 模式
- history 模式
# hash 模式
在 2014 年以前,大家一直都使用的是 hash 模式来实现的前端路由:
http://www.abc.com/#/login
http://www.abc.com/#/register
2
#
后面的内容的变化,不会导致浏览器向服务器发送请求,所以我们可以监听#
后面的变化,从而更新当前的模块。
# histroy 模式
2014 年后,html 5 标准发布了,多了两个 api,pushstate 和 replacestate,通过这两个 api,也可以改变 url但是不发送请求,所以也可以通过这种方式来监听 url 的变化。写出来的路由如下:
http://www.abc.com/login
http://www.abc.com/register
2
# 前端路由库 director.js
官网示例如下:
// 1. 定义匹配上的路由做什么事情
var author = function () { console.log("author"); };
var books = function () { console.log("books"); };
var viewBook = function (bookId) {
console.log("viewBook: bookId is populated: " + bookId);
};
// 2. 配置对应的路由规则
var routes = {
'/author': author,
'/books': [books, function() {
console.log("An inline route handler.");
}],
'/books/view/:bookId': viewBook
};
// 3. 初始化路由
Router(routes).init();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
接下来我们来改写示例,我们直接在 index.js 里面进行路由的配置,代码如下:
// 入口文件
// 我们在入口文件里面对路由进行初始化
import Nav from './modules/nav.js'
import Login from './modules/login.js'
import Register from './modules/register.js'
const routes = {
'/' : ()=>{ new Nav },
'/login' : ()=>{ new Login },
'/register' : ()=>{ new Register }
}
Router(routes).configure({
recurse : 'forward'
}).init();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
接下来在 nav 中,不再需要使用事件形式进行跳转,直接书写对应的路由即可,如下:
// nav.js
export default class Nav{
constructor(){
this.render(); // 调用 render 函数
}
// 渲染函数
render(){
let template = `
<div>
功能选择
<a href="#/login" id="login">登陆</a>
<a href="#/register" id="register">注册</a>
</div>
<div id="container"></div>
`;
// 接下来,我就需要将这个模板挂载到 index.html 里面
$('#app').html(template);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20