SPA那点事

lemon

前端猿一天不学习就没饭吃了,后端猿三天不学习仍旧有白米饭摆于桌前。IT行业的快速发展一直在推动着前端技术栈在不断地更新换代,前端的发展成了互联网时代的一个缩影。而单页面应用的发展给前端猿分了一杯羹。

认识SPA

最早单页面的应用无从知晓,在2004年,google的Gmail就使用了单页面。到了2010年,随着Backbone的问世之后,此概念才慢慢热了起来。随着后来React、Angular、Vue的兴起,单页面应用才成了前端圈里人人皆知的架构模式。接下来小生将通过对比传统页面应用和单页面应用来说明SPA具体是什么。

传统的页面应用

早期web应用的前后端交互模式是这样的,每个html作为一个功能元件,通过刷新、超链接、表单提交等方式,将页面组织起来后给用户提供交互。
tradition
后期很多流行的框架都是基于此模式进行设计的,比如 Ruby on Rails,Spring MVC,Express 等等
mvc
传统的web应用中,浏览器只是作为展示层,路由、服务调用、页面跳转都是服务端来处理的。也就是MVC的架构都是放在后端的,只有V这一层,将页面通过网络发送到浏览器端,渲染给用户。

传统的模式具有以下特点:

  • 重服务端:浏览器只作为展示层,将MVC全置于后端,加重了服务端的体量,开发中主要以后端为主。
  • 频繁刷新:页面展示依赖于不同的功能元件,所以必须依靠刷新页面,或者跳转路由来实现功能块的切换,这种方式严重耗费资源,同时用户体验很差。

单页面应用

和传统应用相比较,单页面应用就是将MVC个架构搬到了前端来实现
spa

  • 控制器:将处理路由的功能放在前端,当浏览器的路由发生变化时,由控制器来响应其变化,指向其对应的处理逻辑(组件),最终将页面展现给用户。
  • 视图:这一层就是功能元件,也就是单个的组件,当路由发生变化的时候由组件来处理,只处理变化的那部分,最后组织成页面。
  • 数据层:单页面应用有自己的数据层定义,简化了后端服务的复杂度,后端只要提供公共的数据接口即可,而数据层会对数据服务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需要在同域里面才能使用,否则报错,爱学习的小伙伴请自行学习。 完整代码

效果图

demo

小结

小生给大家介绍了目前web开发的SPA模式,希望诸君在使用主流框架时能进一步了解其原理,你我共勉。小生基于H5的History实现了一个简单的SPA模式,仅供学习之用,最后小生想说,身为后端转为前端的前端猿,感觉前端的技术栈应是最有活力的,因为一旦你不想动了,就如温水里的青蛙,距离另一个世界也就近了,祝君能像前端的发展势头一样,活力四射,不断进步。

阅读 7.7k

我大EOI前端
[链接] 虽然公司尚小,但是志向不小

只要你golang

293 声望
10 粉丝
0 条评论

只要你golang

293 声望
10 粉丝
文章目录
宣传栏