一個簡單粗暴的先後端分離方案

項目背景
     剛剛參加完一個項目,背景:後端是用java,後端服務已經開發的差很少了,如今要經過web的方式對外提供服務,也就是B/S架構。後端專一作業務邏輯,不想在後端作頁面渲染的事情,只向前端提供數據接口。因而協商後打算將先後端徹底分離,頁面上的全部數據都經過ajax向後端取,頁面渲染的事情徹底由前端來作。另外還有一個緊急的狀況,項目要緊急上線,整個web站點的開發時間只有兩週,兩週啊!因而在這樣的背景下,決定開始一次先後端徹底分離的嘗試。
     以前開發都是同步渲染和異步渲染混搭的,有些東西能夠有後端PHP幫你編譯好,如通用的頁面模板,後端傳回的頁面參數等。提早預感到此次徹底分離可能會遇到一些困難,可是項目上線要緊,也不能深刻搞架構,因而打算就用jQuery+handlebars,jQuery來完成頁面邏輯和DOM操做,用handlebars來完成頁面渲染,這個方案是如此的簡單粗暴,但好處能最穩妥的保證項目定期完成。其實先後端分離並非一件容易的工做,這麼作會有諸多不完善之處,後面再談。
 
淺談先後端分離
     所謂的先後端分離,究竟是分離什麼呢?其實就是頁面的渲染工做,以前是後端渲染好頁面,交給前端來顯示,分離後前端須要本身拼裝html代碼,而後再顯示。前端來管理頁面的渲染有不少好處,好比減小網絡請求量,製做單頁面應用等。事情聽起來簡單,但這麼一分離又會牽扯到不少問題,好比:
  •  資源的按需加載。尤爲是在單頁應用中。
  • 頁面展示邏輯。分離讓前端的邏輯陡增,須要有一個良好的前端架構,如mvc模式。
  • 數據校驗。由於頁面數據都是從後端請求來的,必須校驗要展現的數據是否合法,避免xss或其餘安全問題。
  • 短暫白屏。由於頁面不是同步渲染的,在請求數據完畢以前,頁面是白屏的,體驗很很差。
  • 代碼的複用。衆多的模板、邏輯模塊須要良好組織實現可複用。
  • 路由控制。無刷新的前端體驗同時毀掉了瀏覽器的後退按鈕,前端視圖須要有一套路由機制。
  • SEO。服務端再也不返回頁面,前端根據不一樣的邏輯呈現不一樣的視圖(並不是頁面),要對搜索引擎友好須要作不少額外的工做。
     以上每個問題都夠棘手,要處理好須要有設計精良又符合實際項目的方案。如今已經有不少框架能夠幫咱們作這些事情,Backbone, EmberJS, KnockoutJS, AngularJS, React, avalon等等,利用它們能夠架構起一個富前端。但框架畢竟是框架,要利用到實際項目中,仍是須要有本身的設計,框架並不能解決全部的問題。
     以前也有看過淘寶團隊的實踐,利用nodejs作一箇中間層,處理頁面渲染、路由控制、SEO等事情,將先後端的分界線進行了從新定義。我的感受這應該是一個正確的方向,有點顛覆的感受,前端走向工程化,將變成真正的全棧式大前端。不知如今這種架構是否在淘寶全面鋪開,真有點期待看看效果。
 
     以上的框架,還有淘寶的實踐,畢竟都是大牛之做,我這個小輩也只是參考學習過,未能在實際項目中使用。低頭看看本身如今手頭的項目,1個前端,2周時間,要完成一個完整的web項目,仍是用最穩妥最低級的方式來搞吧~
 
基本結構
     項目總體並非一個單頁應用,但有些模塊須要作成局部的單頁操做,像這種須要分步完成的操做,只需局部加載子頁面便可。
  
     所以,一個模塊有一個主html頁面,初始只有一些基本的骨架,有一個名字相同的js文件,該模塊邏輯都在此js文件中,有一個名字相同的css文件,該模塊的全部樣式都定義在此css文件中。
     須要異步加載的子頁面,像上圖中每一個步驟的頁面,我都使用jQuery的$.load()方法來加載,此方法能在頁面某個容器中加載內容,並可指定回調函數,使用起來很方便。被異步加載的子頁面我都用_開頭,如_step1.html,用於作區分。
     爲了確保瀏覽器的前進後退按鈕可用,我使用了hash來作路由標記,頁面地址如:publish.html#step2。有個缺陷是hash並不會發送給服務器,因此SEO就廢了。事實上使用history API也能夠更優雅的解決問題,但須要考慮兼容性,還有額外工做要作,考慮時間因素,退而求其次,何況本項目也無需作SEO。或者像淘寶的方案那樣,nodejs層與瀏覽器層統一路由,SEO問題能夠迎刃而解。但又明顯不在本人的實力範圍以內,汗--!
     除了用$.load異步加載的子頁面,剩餘的局部頁面就是用handlebars提供的模板渲染了,我使用了handlebars的預編譯功能,不得不說很強大,一來節約了頁面加載階段所需的編譯時間(編譯handlebars模板),二來編譯後的模板(js文件)方便複用。
     接下來就是前端邏輯如何組織,由於沒有用mv*框架,因此只能靠本身來寫一個便於開發的結構。如上面所述,每一個模塊有一個主js文件,文件內容結構以下:
var publish = {
     //該模塊初始化入口
     init : function(){
          this.renderData(param);
          this.initListeners();
     },
     //內部所用的函數
     renderData : function(param){
          //渲染數據。。
     },
     //統一綁定監聽器
     initListeners : function(){
          $(document.body).delegates({
               '.btn' : function(){
                    //點擊事件
               },
               '.btn2' : function(){
                    //點擊事件2
               },
               '.checkbox' : {
                    'change' : function(){
                         //change事件
                    }
               }
          });
     }
}
     每一個模塊給一個命名空間,全部的方法都掛在上面,js文件中只作函數的定義,不當即執行任何東西,而後在html文件中調用入口方法:publish.init()。業務邏輯都封裝到函數中,如上面的renderData,而後供其餘地方調用。頁面的事件監聽器統一都註冊在body元素上,用事件代理來完成,爲了不寫太多的on、click之類代碼,爲jQuery擴展了一個delegates方法,用來以配置的方式統一綁定監聽器,用法如上所示。把delegates定義的代碼也放出來吧:
//以配置的方式代理事件
$.fn.delegates = function(configs) {
     el = $(this[0]);
     for (var name in configs) {
          var value = configs[name];
          if (typeof value == 'function') {
               var obj = {};
               obj.click = value;
               value = obj;
          };
          for (var type in value) {
               el.delegate(name, type, value[type]);
          }
     }
     return this;
}
     基本的結構就是這樣,沒有什麼新技術,只是把現有的東西作了一下組合。但工做到此還遠遠沒有結束,在實際應用中還會有一些東西須要處理,下面來詳細說說:
 
公共頭部底部的引用
     這是一個比較棘手的問題,通常通用的頭部和底部會放一些公共的代碼,如頁面外層結構html代碼,站點使用的庫如jQuery、handlebars,站點通用js和css文件。在傳統的開發中,一般是寫一個單獨的文件如head.html,在其餘頁面中用後端代碼如include語句引入,由此來進行復用。
     如今先後端分離後,沒法依靠後端來給你渲染,因此得在前端作了。既然用了handlebars,很容易想到把公用部分寫成一個模板,而後預編譯出來,生成一個header.js文件,而後在其餘頁面引用。然而在實際操做中發現了一個問題,handlebars是靜態模板,編譯後生成的字符串經過innerHTML的方式插入到頁面,在通常的模板中這樣是沒問題的。如今有個問題是header中有一些<script>標籤,外鏈着要使用的庫, 經過innerHTML插入<scirpt>標籤,瀏覽器並不會發送請求加載對應的js文件,因此就出問題了。
     搜索、嘗試了多種方法後,最終的方案定爲:用document.write()將編譯結果寫到頁面,這樣<script>標籤可以正常加載。因此每一個頁面使用頭部的代碼就變成這樣:
<script src="static/js/tpl/head.js"></script>
     <div id="header">
          <script src="static/js/includeHead.js"></script>
     </div>
  includeHead.js中的代碼以下:
function includeHead(){
     var header = document.getElementById('header');
     var compileHead = Handlebars.templates['head'];
     var head = compileHead({});
     document.write(head);
}
includeHead();
  看着是有點彆扭,不過爲了實現功能,目前也就只能這樣了。
 
----------補充於 2015.1.27---------------
  雖然用原生的innerHTML沒法加載<script>標籤中的內容,可是jQuery的$().html()方法進行了優化,能夠查找到<script>標籤而且執行裏面的代碼,因此用$().html()是能夠完成上面的工做的。
  這麼一看,這個蹩腳的方案就能夠替換了。  
 
路由控制
     如上面所述,jQuery的$.load()方法能夠知足加載子頁面的需求,如今須要解決的問題是,無論用戶刷新頁面仍是前進後退,咱們都得根據hash值來渲染對應的視圖,其實就是路由控制。這個時候就須要監聽hashchange事件了,我定義了一個loadPage方法用來加載子頁面,而後綁定監聽器以下:
window.onhashchange = this.loadPage;
     在loadPage方法中,根據hash的值來調用$.load()方法,子頁面的初始化工做,在$.load()的回調函數中指定。
     這樣作還有一個便捷之處,咱們切換視圖沒必要手動調loadPage方法,只須要修改頁面的hash就能夠了,hash發生變化被監聽到,自動加載對應的子頁面。例如,點擊下一步進入步驟二:
'.next' : function(){
        location.href = '#step2';
}
    如此便實現了一個簡單的路由控制,因爲不是整站單頁面,也沒有多級路由,這樣徹底能夠知足需求。至於SEO,就只能呵呵了,正好項目也不須要作SEO,不然此方法得做罷。
     另外想說的一點就是頁面的緩存,異步加載來的內容能夠存在localStorage中,也能夠放在頁面上進行顯隱控制,這樣用戶在頻繁切換視圖的時候無需再次請求,回到上一步的時候以前填好的表單數據也不會消失,體驗會很是好。
 
頁面間參數傳遞
     有時候咱們須要給訪問的頁面傳參數,好比訪問一個設備的詳細信息頁,要把設備id給傳過去,detail.html?id=1,這樣detail頁面能夠根據id去請求對應的數據。傳統由後端渲染的頁面,url中的參數會發送到服務端,服務端接收後能夠再渲染到頁面上供js使用。咱們如今不行了,請求頁面壓根不跟後端打交道,但這個參數是必不可少的,因此須要前端有一套傳遞參數的機制。
     其實很是簡單,經過location.href能夠拿到當前的url地址,而後進行字符串匹配,把參數提取出來就能夠了。看上去挺土鱉的,但工做起來良好,另外也有考慮過用cookie來傳遞,感受有點麻煩。
     因爲這些參數一般是寫在<a>標籤上的,而<a>標籤又是根據動態數據渲染出來的(由於是動態參數),咱們不可能在頁面渲染完後,用js修改全部<a>標籤的href值,給它追加一個參數。怎麼辦呢?這時候handlebars就派上用場了,咱們可使用handlebars萬能的helper,在渲染頁面的時候直接查詢url中的參數,而後輸出在編譯好的代碼中。我在handlebars中註冊了一個helper,以下:
Handlebars.registerHelper('param', function(key, options){
    var url = location.href.replace(/^[^?=]*\?/ig, '').split('#')[0];
    var json = {};
    url.replace(/(^|&)([^&=]+)=([^&]*)/g, function (a, b, key , value){
        try {
            key = decodeURIComponent(key);
        } catch(e) {}

        try {
            value = decodeURIComponent(value);
        } catch(e) {}

        if (!(key in json)) {
            json[key] = /\[\]$/.test(key) ? [value] : value;
        }
        else if (json[key] instanceof Array) {
            json[key].push(value);
        }
        else {
            json[key] = [json[key], value];
        }
    });
    return key ? json[key] : json;
});
    這個名爲param的helper能夠輸出你所要查詢的參數值,而後能夠直接寫在模板中,如:
<a href="detail.html?id={{param id}}">設備詳細信息</a>
     這樣就方便多了!可是這麼作有沒有問題呢?實際上是有些不完美的,若是你考慮「性能」二字的話。一個url中參數的值是固定的,而你每次使用這個helper都會計算一遍,白白作了多餘的事情。若是handlebars能夠在模板中定義常量就行了,惋惜我找遍文檔沒發現有這個功能。只能爲了方便犧牲性能了,也正印證了我標題中所說的「簡單粗暴」,呵呵。
 
數據的校驗和處理
     因爲數據是由後端傳來的,有不少不肯定性,數據可能不合法,或者結構有錯,或者直接是空的。所以前端有必要對數據作一個合法性的校驗。藉助handlebars,能夠很方便的進行數據校驗。沒錯,就是利用helper。handlebars內置的helper如if、each都支持else語句,出錯信息能夠在else中輸出。若是須要個性化的校驗,咱們能夠本身定義helper來完成,關於如何自定義helper,我以前研究了下,寫過一篇文章: http://www.cnblogs.com/lvdabao/p/handlebars_helper.html。總之自定義helper很強大,能夠完成你所需的任何邏輯。
     數據的格式化,如日期、數字等,也能夠經過helper來完成。
     另一方面,前端還應對數據進行html轉義,避免xss,因爲handlebars已經給作了html轉義,因此咱們能夠直接忽略此項了。
 
總結
     本文是我剛剛參加完一個項目後所寫,記錄一下整個過程遇到的問題及處理方式,其餘的一些細碎點如表單異步提交什麼的,不是本文重點,不寫了。這是我第一次實踐先後端徹底分離的項目,整個前端全由我來設計、開發。2周時間,憑着這套方案,項目定期開發完成,並且還提早完成了,預留出一天多的時間測試了一遍。
     雖然開發任務是完成了,可是回頭看一下整個方案,並非很優雅也沒有什麼技術含量,文章開頭提到的幾個問題都沒有解決。因此命題爲簡單粗暴的方案,都是爲了趕工期啊。
     最後,若是給我再來一次的機會,而且時間充足,我必定要嘗試用mv*方案來搞一下,或angular,或avalon。
相關文章
相關標籤/搜索