SPA那點事

前端猿一天不學習就沒飯吃了,後端猿三天不學習仍舊有白米飯擺於桌前。IT行業的快速發展一直在推進着前端技術棧在不斷地更新換代,前端的發展成了互聯網時代的一個縮影。而單頁面應用的發展給前端猿分了一杯羹。javascript

認識SPA

最先單頁面的應用無從知曉,在2004年,google的Gmail就使用了單頁面。到了2010年,隨着Backbone的問世以後,此概念才慢慢熱了起來。隨着後來React、Angular、Vue的興起,單頁面應用才成了前端圈裏人人皆知的架構模式。接下來小生將經過對比傳統頁面應用和單頁面應用來講明SPA具體是什麼。css

傳統的頁面應用

早期web應用的先後端交互模式是這樣的,每一個html做爲一個功能元件,經過刷新、超連接、表單提交等方式,將頁面組織起來後給用戶提供交互。
tradition
後期不少流行的框架都是基於此模式進行設計的,好比 Ruby on Rails,Spring MVC,Express 等等
mvc
傳統的web應用中,瀏覽器只是做爲展現層,路由、服務調用、頁面跳轉都是服務端來處理的。也就是MVC的架構都是放在後端的,只有V這一層,將頁面經過網絡發送到瀏覽器端,渲染給用戶。html

傳統的模式具備如下特色:前端

  • 重服務端:瀏覽器只做爲展現層,將MVC全置於後端,加劇了服務端的體量,開發中主要之後端爲主。
  • 頻繁刷新:頁面展現依賴於不一樣的功能元件,因此必須依靠刷新頁面,或者跳轉路由來實現功能塊的切換,這種方式嚴重耗費資源,同時用戶體驗不好。

單頁面應用

和傳統應用相比較,單頁面應用就是將MVC個架構搬到了前端來實現
spajava

  • 控制器:將處理路由的功能放在前端,當瀏覽器的路由發生變化時,由控制器來響應其變化,指向其對應的處理邏輯(組件),最終將頁面展示給用戶。
  • 視圖:這一層就是功能元件,也就是單個的組件,當路由發生變化的時候由組件來處理,只處理變化的那部分,最後組織成頁面。
  • 數據層:單頁面應用有本身的數據層定義,簡化了後端服務的複雜度,後端只要提供公共的數據接口便可,而數據層會對數據服務API進行進一步的封裝,而後提供數據給視圖層。

如此看來單頁面應用很像移動客戶端,後端的精力就是提供高質量的、可複用的Rest API服務。web

世間萬物皆有裂痕,哪又怎樣?裂痕,那是光照進來的地方。ajax

單頁面應用的出現依然存在着爭議性,咱們該如何看待他的兩面性呢?接下來小生給你們總結一下他的優缺點。編程

單頁面應用的優點:json

  • 無刷新體驗:沒有了使人詬病的頁面頻繁刷新,同時節約瀏覽器資源,路由響應比較及時,提高了用戶的體驗。
  • 共享組件:前端組件化是將獨立完整的功能模塊封裝到一個組件中,代碼結構更加規範,便於代碼維護,同時模塊化後的組件能夠在不一樣的場景中進行復用,極大地加快了迭代開發的速度。這也是爲何主流的前端框架都提倡組件化編程的緣由。
  • 共享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模式,僅供學習之用,最後小生想說,身爲後端轉爲前端的前端猿,感受前端的技術棧應是最有活力的,由於一旦你不想動了,就如溫水裏的青蛙,距離另外一個世界也就近了,祝君能像前端的發展勢頭同樣,活力四射,不斷進步。

相關文章
相關標籤/搜索