關於單頁應用(SPA)的經驗之談

bzt820801發表於2017-08-12
時下SPA單頁應用如火如荼,對前端乃至後端開發都帶來不小的衝擊和變革。筆者整理了下筆記,決定寫一下以前基於iframe做單頁部落格的一些經驗方法。
對於單頁應用,筆者沒有找到最官方的定義。在筆者看來,在使用者操作過程中,瀏覽器始終不會過載整個頁面的web應用,便可以稱為單頁應用。這裡不包括https://im.qq.com/這種宣傳單頁
例如coding.net、網易雲音樂播放、QQ空間、移動端大量的react案例(比如手Q健康、公眾號)等等

單頁站點優劣

單頁站點的優勢主要有三點     

傳輸資料少

單頁站點的重點是區域性重新整理,因此每次更新,傳輸的資料少,減少後端壓力,甚至對於完全前後端分離的SPA應用,只需要傳輸少量json資料即可。這一點在移動端顯得尤為重要,許多應用前端程式碼並不會頻繁更新,完全可以由前端直接快取起來,每次使用只需與後端互動少量資料,這樣既省流量也能讓使用者獲得接近native的體驗

服務可不中斷

一些特殊網站,如音樂播放、IM聊天等,不希望因為頁面全部過載造成服務中斷。單頁應用因為區域性重新整理,可以將這些服務放置在重新整理範圍之外,持續提供服務

前後端開發更規範

前端也可按照MVC的模式更好的模組化開發,而後端開發僅僅只需要開發資料操作介面,對前後端開發而言都是一種解放
 
但單頁站點也帶來了一些新的問題,比如

首次載入資料大耗時長

特別是目前基於angular或者react的純前後端分離的SPA,結合一些javascript方言,編譯出來js相當的大,筆者曾在內網親眼目睹10M級別的js檔案,即便以內網的網速首次開啟也需要3秒左右。為每個模組單獨編譯js是種辦法,但實際操作會可能發現,隨著專案越做越大,拆分成獨立模組編譯的成本會越來越大,最終不得不委曲求全整站使用一個js,除非從一開始就有良好的規範限制。

極差的SEO(搜尋引擎優化)

眾所周知,通過請求url即可獲取到大量頁面正文文字的頁面是對搜尋引擎最為友好的,雖然現在的爬蟲已經具備解析執行頁面js指令碼索引動態內容的能力,但是每個網站千奇百怪,爬蟲需要考慮觸發什麼事件、按什麼順序觸發才能獲取更多內容,索引動態內容的難度要遠遠大於索引靜態內容。而目前主流的單頁應用,幾乎都是以前端js模板引擎來渲染html頁面資料,直接通過url獲取到的內容極少,這對搜尋引擎非常不友好。SEO最差的單頁應用可能僅僅只有首頁能夠被搜尋引擎收錄。這成了制約單頁應用發展的一大障礙,即便現在又方案可以提高收錄,但效果還是不好

導航需要人為處理

瀏覽器對div以及早期瀏覽器對iframe都不記錄歷史記錄,因此需要開發對瀏覽器的前進後退做實現,通過修改hash或者history API來實現前進後退(後面會提到)

單頁應用的實現方式

筆者瞭解到的,目前主要兩種實現方式

iframe

其一,使用iframe的優點之一就是開發簡單,目前的瀏覽器都已經對iframe url發生修改產生歷史記錄。
其二,除了響應式問題的相容性不好之外(也正因此iframe很不適合用在移動端),iframe作為使用多年的瀏覽器技術之一,在許多方面的相容性要好許多,也是一些新技術在低版本瀏覽器上不可用時的替代解決方案,如contentEditable。
其三,iframe與父文件相對獨立,可以不受父文件的影響,想必這也是目前一些網站(網易雲音樂,QQ空間,各大郵箱)繼續使用iframe的主要原因。

ajax+div+historyapi

這種方式實現要更復雜,開發要自己實現url管理,以達到前進、後退跳轉等能力,不過目前都已經有成熟的路由庫可以使用,另外基於div模式的SPA,開發需要考慮全域性對區域性的影響,包括css、事件等。這種方式的優點是重新整理要更輕量,js庫和css樣式在首次載入即可,區域性頁面可以只載入少量的資料,並且基於div響應式效果在移動端要更好。因此這也成了目前流行的前端框架angular、react等選用的方案。

基於iframe製作單頁部落格

筆者的部落格製作於2015年,當時的PC瀏覽器應該不支援iframe歷史記錄,所以筆者選擇通過修改hash的方式實現歷史記錄(瀏覽器對hash的修改會記錄歷史記錄),選擇基於iframe製作基於兩個原因:一、希望瀏覽部落格時不論怎麼跳轉,可以不中斷播放音樂;二、iframe相對全站ajax+div而言要更簡單易行。部落格地址http://movesun.com,部落格佈局參考 http://www.kotonohanoniwa.jp/。
做法是繫結所有需要在iframe中開啟的a標籤的click事件,當點選a標籤時,將a標籤url中的path路徑修改為瀏覽器url的hash值。例如我想訪問的是 http://movesun.com/blog/list,則將/blog/list作為hash值設定到位址列 ,因此在瀏覽器位址列看到的地址就變為了http://movesun.com/#/blog/list,
 
因此在父文件中有這樣一段js
 1 $('a[target="contentFrame"]').click(function(){
 2     var $this = $(this),
 3         url = $this.attr('href'),
 4         mainHost = location.host,
 5         i = url.indexOf(mainHost);
 6     $active.removeClass('active');
 7     $active = $this.parent('li');
 8     $active.addClass('active');
 9     if(i !== -1){
10         url = url.substr(i + mainHost.length);
11     }
12     window.location.hash = '#' + url;
13     return false;
14 });

在iframe頁面(子頁面)中,也有一段類似的js,為iframe中的頁面超連結繫結事件

 1 $('a').click(function(){
 2     var url = $(this).attr('href')
 3     if(url && url != '#' && url.indexOf('http') == 0){
 4         var mainHost = window.parent.location.host,
 5                 i = url.indexOf(mainHost);
 6         if(i !== -1){
 7             url = url.substr(i + mainHost.length);
 8         }
 9         window.parent.location.hash = '#' + url;
10     }
11     return false;
12 });

注意這兩段程式碼,修改的都是父文件(頂層視窗)位址列的hash值。所以,只需要在父文件中監聽onhashchange事件,在事件響應中設定iframe的src 進而load子頁面。

1 $container = $('div.page-body > iframe');
2 window.onhashchange = function(){
3     $container.attr('src',location.hash.substring(1));
4 };

 iframe高度不能根據內容自適應,需要在子頁面load之後重新整理iframe的高度

 1 var refreshHeight = function(){
 2     var $this = $container,
 3         minHeight = $('.page-right').height() - $('.top-menu').height() - 20,
 4         contentHeight = $this.contents().find('body').height() + 10;
 5     $this.height(contentHeight < minHeight ? minHeight : contentHeight);
 6 };
 7  
 8 $container.load(function(){
 9     refreshHeight();
10 });
11 // 首次重新整理,否則載入過程中會看到白框
12 refreshHeight();

到這裡基本已經實現任意跳轉、回退、前進頁面不再重新整理整個頁面。下面的程式碼是為了解決當開啟多個頂層文件時(開多個視窗),音樂不重複播放,方法也很簡單,在localStorage中記錄頂層文件的數量,每開一個新視窗加1,關閉時減1,只要記錄數大於1便不自動播放。 

 1 if(window.localStorage){
 2     var $window = $(window);
 3     $window.on('beforeunload',function(){
 4         console.log('-1');
 5         localStorage.framePage = localStorage.framePage - 1;
 6     });
 7  
 8     window.addEventListener("storage", function(e){
 9         console.log("oldValue: "+ e.oldValue + " newValue:" + e.newValue)
10     });
11 }
12 var autoplay =  location.host !== 'movesun.qq.com';
13 if(window.localStorage){
14     if(Number(localStorage.framePage) >= 1){
15         autoplay = false;
16     }
17  
18     if(isNaN(localStorage.framePage) || Number(localStorage.framePage) <= 0) localStorage.framePage = 1;
19     else {
20         localStorage.framePage = Number(localStorage.framePage) + 1;
21     }
22 }

部落格依然有兩個問題需要解決

1、目前的瀏覽器已經支援記錄iframe變更的歷史記錄,通過hash記錄歷史就顯的沒有必要了。目前網站每次跳轉實際產生了兩條歷史記錄。後面找時間移出hash記錄或者禁用iframe歷史記錄

 

2、考慮到搜尋引擎收錄的超連結應該是非hash模式的url,比如使用者看到的是movesun.com/#/blog/list ,而實際收錄的卻是movesun.com/blog/list,通過site:movesun.com指令搜尋也可以看到

直接訪問這類url地址,將直接開啟iframe裡的內容,所以,當使用者直接點選搜尋引擎的結果進入部落格時,應該將使用者跳轉到hash模式,頁面才能正常顯示,但這樣對搜尋引擎而言,會陷入一個無限迴圈,影響搜尋引擎收錄。

前端因直接面向使用者,使得技術也更新迭代的頻繁,前端開發人員也需要不斷學習以追趕時代的潮流。而反觀後臺技術,十年來都沒什麼及其巨大的變化,很多技術經久不衰,後端開發完全有一招鮮吃遍天的架勢。這也是的前端人員比較搶手,在一些公司都存在前端與後臺人力嚴重不平衡的現像,十幾位後臺搭配一位前端的事情,也不是沒有,奇貨可居,優秀的前端是非常吃香的。

相關文章