资讯专栏INFORMATION COLUMN

SPA那点事

lijinke666 / 2700人阅读

摘要:单页面应用的出现依然存在着争议性,我们该如何看待他的两面性呢接下来小生给大家总结一下他的优缺点。单页面应用的优势无刷新体验没有了令人诟病的页面频繁刷新,同时节约浏览器资源,路由响应比较及时,提升了用户的体验。

前端猿一天不学习就没饭吃了,后端猿三天不学习仍旧有白米饭摆于桌前。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的代码如下:






  SPA
  
  
  



  
SHARPLOOK 大数据运维监控平台

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模式,仅供学习之用,最后小生想说,身为后端转为前端的前端猿,感觉前端的技术栈应是最有活力的,因为一旦你不想动了,就如温水里的青蛙,距离另一个世界也就近了,祝君能像前端的发展势头一样,活力四射,不断进步。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/116618.html

相关文章

  • SPA点事

    摘要:单页面应用的出现依然存在着争议性,我们该如何看待他的两面性呢接下来小生给大家总结一下他的优缺点。单页面应用的优势无刷新体验没有了令人诟病的页面频繁刷新,同时节约浏览器资源,路由响应比较及时,提升了用户的体验。 前端猿一天不学习就没饭吃了,后端猿三天不学习仍旧有白米饭摆于桌前。IT行业的快速发展一直在推动着前端技术栈在不断地更新换代,前端的发展成了互联网时代的一个缩影。而单页面应用的发展...

    PumpkinDylan 评论0 收藏0
  • SPA点事

    摘要:单页面应用的出现依然存在着争议性,我们该如何看待他的两面性呢接下来小生给大家总结一下他的优缺点。单页面应用的优势无刷新体验没有了令人诟病的页面频繁刷新,同时节约浏览器资源,路由响应比较及时,提升了用户的体验。 前端猿一天不学习就没饭吃了,后端猿三天不学习仍旧有白米饭摆于桌前。IT行业的快速发展一直在推动着前端技术栈在不断地更新换代,前端的发展成了互联网时代的一个缩影。而单页面应用的发展...

    Lsnsh 评论0 收藏0
  • 移动端键盘和光标的兼容点事

    摘要:解决方法如果使用页面数据不超过一屏禁止滚动,那么即使变成了页面也不会有什么变化。 作者:@micky思 @wupq @yewq 在H5的开发中,个人的制作页面布局习性不同,多多少少会产生在真机上input的光标和键盘的弹出会出现的各种BUG,文中整理了部分遇到的问题,欢迎新增 ios移动端输入框上浮导致输入位置偏移 问题原因:遮罩层定位为fixed,当键盘弹起时,ios11以及以下...

    XboxYan 评论0 收藏0
  • 移动端键盘和光标的兼容点事

    摘要:解决方法如果使用页面数据不超过一屏禁止滚动,那么即使变成了页面也不会有什么变化。 作者:@micky思 @wupq @yewq 在H5的开发中,个人的制作页面布局习性不同,多多少少会产生在真机上input的光标和键盘的弹出会出现的各种BUG,文中整理了部分遇到的问题,欢迎新增 ios移动端输入框上浮导致输入位置偏移 问题原因:遮罩层定位为fixed,当键盘弹起时,ios11以及以下...

    Kerr1Gan 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<