前端猿一天不学习就没饭吃了,后端猿三天不学习仍旧有白米饭摆于桌前。IT行业的快速发展一直在推动着前端技术栈在不断地更新换代,前端的发展成了互联网时代的一个缩影。而单页面应用的发展给前端猿分了一杯羹。
认识SPA
最早单页面的应用无从知晓,在2004年,google的Gmail就使用了单页面。到了2010年,随着Backbone的问世之后,此概念才慢慢热了起来。随着后来React、Angular、Vue的兴起,单页面应用才成了前端圈里人人皆知的架构模式。接下来小生将通过对比传统页面应用和单页面应用来说明SPA具体是什么。
传统的页面应用
早期web应用的前后端交互模式是这样的,每个html作为一个功能元件,通过刷新、超链接、表单提交等方式,将页面组织起来后给用户提供交互。
后期很多流行的框架都是基于此模式进行设计的,比如 Ruby on Rails,Spring MVC,Express 等等
传统的web应用中,浏览器只是作为展示层,路由、服务调用、页面跳转都是服务端来处理的。也就是MVC的架构都是放在后端的,只有V这一层,将页面通过网络发送到浏览器端,渲染给用户。
传统的模式具有以下特点:
- 重服务端:浏览器只作为展示层,将MVC全置于后端,加重了服务端的体量,开发中主要以后端为主。
- 频繁刷新:页面展示依赖于不同的功能元件,所以必须依靠刷新页面,或者跳转路由来实现功能块的切换,这种方式严重耗费资源,同时用户体验很差。
单页面应用
和传统应用相比较,单页面应用就是将MVC个架构搬到了前端来实现
- 控制器:将处理路由的功能放在前端,当浏览器的路由发生变化时,由控制器来响应其变化,指向其对应的处理逻辑(组件),最终将页面展现给用户。
- 视图:这一层就是功能元件,也就是单个的组件,当路由发生变化的时候由组件来处理,只处理变化的那部分,最后组织成页面。
- 数据层:单页面应用有自己的数据层定义,简化了后端服务的复杂度,后端只要提供公共的数据接口即可,而数据层会对数据服务API进行进一步的封装,然后提供数据给视图层。
如此看来单页面应用很像移动客户端,后端的精力就是提供高质量的、可复用的Rest API服务。
世间万物皆有裂痕,哪又怎样?裂痕,那是光照进来的地方。
单页面应用的出现依然存在着争议性,我们该如何看待他的两面性呢?接下来小生给大家总结一下他的优缺点。
单页面应用的优势:
- 无刷新体验:没有了令人诟病的页面频繁刷新,同时节约浏览器资源,路由响应比较及时,提升了用户的体验。
- 共享组件:前端组件化是将独立完整的功能模块封装到一个组件中,代码结构更加规范,便于代码维护,同时模块化后的组件可以在不同的场景中进行复用,极大地加快了迭代开发的速度。这也是为什么主流的前端框架都提倡组件化编程的原因。
- 共享API:给后端减负,前端加码的好处就是,前端能有一点口粮吃了(开玩笑,前端那么牛怎么能没饭吃呢?),前端担起家务的事,后端就可以安心地处理业务逻辑了,于是才能写出高质量并可共享的API,供自己或者其他的合作伙伴使用。一个优秀的产品背后,一定有一群出色的前端(小生脸皮太厚)。
单页面应用的劣势:
- 抬高了前端门槛:SPA模式的流行,引领了前端技术的飞速发展,与此同时对前端人员在学习和使用上的能力就有了更高的要求,同时工作量也增加了,前端想活的更好就要付出的更多,所以不要再以为前端就是切切图,画画页面这么简单。too young, too naive。
- 首次加载大量资源:既然只有一个页面显示,那许多功能元件(组件)所依赖的静态资源就需要在初次时进行加载,加载时间相对比较长。
- 不利于SEO:单页面应用,数据都是在前端进行渲染的,所以就影响了SEO。
徒手实现SPA
随着SPA的流行,目前主流的框架都实现了SPA模式,包括我们夏洛克产品里面用到的Angular和Vue。但是作为一家爱折腾公司里面爱折腾的前端团队里面爱折腾的人,我们总想跟自己较劲来试试自己去实现简单的模式,这次小生也简单地实现了一把,于是将其分享于诸位,目前只是简单的模型,不能用于生产(主流框架都有,干嘛用我的?学习一下思想即可),除非你愿意折腾。在此之前需要介绍几个核心点:
- 路由:小生使用H5中的History API来管理路由的更新(地址栏URL更新、前进、后退)。
- 视图:小生还是使用原生的Document来操作,目前渲染的内容比较简单。
- 数据层:小生使用XMLHttpRequest写了一个Ajax服务,帮助请求后端数据(此服务较简单,不适用生产环境)。
H5 History API
关于H5 History API在此需要介绍一下,他是HTML5引入的操作浏览器路由历史堆栈的内容,其中两个主要的方法为history.pushState(stateObj, title, URL) 和 history.replaceState(stateObj, title, URL) 方法,它们分别可以添加和修改历史记录条目。这些方法通常与window.onpopstate 配合使用。三个参数分别为:
- 状态对象 — 状态对象是一个javascript对象,通过pushState () 创建新的历史记录条目。无论什么时候用户导航到新的状态,popstate事件就会被触发,且该事件的state属性包含该历史记录条目状态对象的副本。
- title — Firefox 目前忽略这个参数,但未来可能会用到。
- URL — 该参数定义了新的历史URL记录。注意,调用 pushState() 后浏览器并不会立即加载这个URL,但可能会在稍后某些情况下加载这个URL,比如在用户重新打开浏览器时。新URL不必须为绝对路径。如果新URL是相对路径,那么它将被作为相对于当前URL处理。新URL必须与当前URL同源,否则 pushState() 会抛出一个异常。该参数是可选的,缺省为当前URL。
小生结合window.onpopstate事件来监听浏览器前进和后退的动作来重新请求数据服务,更新视图。
每当处于激活状态的历史记录条目发生变化时, popstate事件就会在对应window对象上触发。 如果当前处于激活状态的历史记录条目是由history.pushState()方法创建, 或者由history.replaceState()方法修改过的, 则popstate事件对象的state属性包含了这个历史记录条目的state对象的一个拷贝。
调用history.pushState()或者history.replaceState()不会触发popstate事件. popstate事件只会在浏览器某些行为下触发, 比如点击后退、前进按钮(或者在JavaScript中调用history.back()、history.forward()、history.go()方法)。
目录结构
-- data
-- auto.json
-- contact.json
-- home.json
-- platform.json
-- sharplook.json
-- ajax.js
-- index.js
-- index.html
-- index.css
源码分享
data文件下是模拟的后端数据,数据的结构都与下面一样,比如home.json
{
"content": "上海擎创信息技术有限公司是专业服务于企业级客户的ITOA智能运营大数据分析解决方案提供商,专注于将人工智能技术赋予IT运维管理,创造具备分析和思考能力的IT管理软件,让每家企业都拥有自己的IT运维专家。"
}
ajax.js的代码如下:
function ajax() {
const ajaxData = {
type: arguments[0].type || ‘GET‘,
url: arguments[0].url || ‘‘,
async: arguments[0].async || ‘true‘,
data: arguments[0].data || null,
dataType: arguments[0].dataType || ‘text‘,
contentType: arguments[0].contentType || ‘application/x-www-form-urlencoded‘,
beforeSend: arguments[0].beforeSend || function () {},
success: arguments[0].success || function () {},
error: arguments[0].error || function () {}
}
ajaxData.beforeSend()
const xhr = _createxmlHttpRequest();
xhr.responseType = ajaxData.dataType;
xhr.open(ajaxData.type, ajaxData.url, ajaxData.async);
xhr.setRequestHeader(‘Content-Type‘, ajaxData.contentType);
xhr.send(_convertData(ajaxData.data));
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
ajaxData.success(xhr.response);
} else {
ajaxData.error();
}
}
}
}
function _createxmlHttpRequest() {
if (window.ActiveXObject) {
return new ActiveXObject(‘Microsoft.XMLHTTP‘);
} else if (window.XMLHttpRequest) {
return new XMLHttpRequest();
}
}
function _convertData(data) {
if (typeof data === ‘object‘) {
let convertResult = ‘‘;
for (let c in data) {
convertResult += `${c}=${data[c]}&`;
}
convertResult = convertResult.substring(0, convertResult.length - 1);
return convertResult;
} else {
return data;
}
}
index.html的代码如下:
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<head>
<title>SPA</title>
<link href="./index.css" rel="stylesheet" type="text/css" />
<script src="./ajax.js"></script>
<script src="./index.js"></script>
</head>
<body>
<main class="container">
<header class="bar">
<span class="title">SHARPLOOK 大数据运维监控平台</span>
</header>
<section class="body">
<ul id="nav">
<li><a href="/home">首页</a></li>
<li><a href="/sharplook">夏洛克</a></li>
<li><a href="/platform">自管理平台</a></li>
<li><a href="/auto">自动化安装</a></li>
<li><a href="/contact">联系我们</a></li>
</ul>
<div id="content-main">
<div id="content">
<p id="p"></p>
</div>
</div>
</section>
</main>
</body>
<script type="text/javascript">
const spa = new SPA();
spa.init();
</script>
</html>
index.js的代码如下:
class SPA {
constructor () {
this.elment = void 0;
this.menu = Array.from(document.getElementsByTagName(‘a‘));
}
getCurrentHash() {
return window.history.state ? window.history.state.hash : ‘/home‘;
}
isSupportH5History() {
return !!(window.history && window.history.pushState);
}
setElement(hash) {
if (!hash) { // 默认为根路由 ‘/’
this.elment = this.menu[0];
} else {
this.menu.forEach(item => {
if(item.getAttribute(‘href‘) === hash) {
this.elment = item;
}
});
}
}
renderData() {
const contentElement = document.getElementById(‘p‘);
this.loadData(contentElement, this.elment.getAttribute(‘href‘).split(‘/‘)[1]);
}
addHistory(hash, isReplace) {
const stateObj = { hash };
if(isReplace) {
window.history.replaceState(stateObj, null, hash);
} else {
window.history.pushState(stateObj, null, hash);
}
}
loadData(contentElement, type) {
ajax({
type: ‘get‘,
url: `/data/${type}.json`,
dataType: ‘json‘,
success: function(msg) {
console.log(msg);
contentElement.innerText = msg.content;
},
error: function() {
console.log(‘error‘)
}
})
};
popStateHandler(linkHash, isPopState = false) {
if(!linkHash) {// 刷新界面时候,默认获取刷新之前的路由信息
this.addHistory(this.getCurrentHash(), true);
} else {
if(!isPopState) this.addHistory(linkHash, false);
}
this.setElement(this.getCurrentHash());
this.renderData();
this.addActiveClass();
}
bindLiClick() {
const list = document.getElementsByTagName(‘li‘);
Array.from(list).forEach(item => {
item.onclick = (event) => {
const linkHash = item.childNodes[0].getAttribute(‘href‘);
this.popStateHandler(linkHash);
}
});
}
addActiveClass() {
this.menu.forEach(item => {
item.parentNode.classList.remove(‘active‘);
})
this.elment.parentNode.classList.add(‘active‘);
}
init() {
if(!this.isSupportH5History()) throw new Error(‘对不起!不支持 H5 History API!‘);
this.bindLiClick();
window.onpopstate = (event) => {
this.popStateHandler(event.state.hash, true);
}
// 首次默认首次进入页面
this.popStateHandler();
}
}
index.css的代码如下:
* {
margin: 0;
padding: 0;
}
html, body {
height: 100%;
}
.container {
height: 100%;
display: flex;
flex-direction: column;
}
.bar {
background-color: #213442;
color: white;
height: 60px;
display: flex;
align-items: center;
font-size: 20px;
}
.body {
display: flex;
height: calc(100% - 60px);
border-top: 1px solid #ccc;
}
#content {
border: 1px solid #ccc;
border-radius: 3px;
padding: 10px;
width: 600px;
box-shadow: 5px 5px 15px 0 #bbb;
}
#nav {
background-color: #213442;
width: 120px;
text-align: center;
}
a {
color: white;
text-decoration: none;
pointer-events: none;
}
#content-main {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}
.title {
padding-left: 20px;
}
li {
margin: 10px 0;
line-height: 30px;
cursor: pointer;
}
li:hover {
background-color: #00A5D5;
}
.active {
background-color: #00A5D5;
}
代码的具体逻辑不做过多介绍,特别需要注意的是请将代码部署到web服务器上查看效果,因为history api需要在同域里面才能使用,否则报错,爱学习的小伙伴请自行学习。 完整代码
效果图
小结
小生给大家介绍了目前web开发的SPA模式,希望诸君在使用主流框架时能进一步了解其原理,你我共勉。小生基于H5的History实现了一个简单的SPA模式,仅供学习之用,最后小生想说,身为后端转为前端的前端猿,感觉前端的技术栈应是最有活力的,因为一旦你不想动了,就如温水里的青蛙,距离另一个世界也就近了,祝君能像前端的发展势头一样,活力四射,不断进步。