項目背景javascript
剛剛參加完一個項目,背景:後端是用java,後端服務已經開發的差很少了,如今要經過web的方式對外提供服務,也就是B/S架構。後端專一作業務邏輯,不想在後端作頁面渲染的事情,只向前端提供數據接口。因而協商後打算將先後端徹底分離,頁面上的全部數據都經過ajax向後端取,頁面渲染的事情徹底由前端來作。另外還有一個緊急的狀況,項目要緊急上線,整個web站點的開發時間只有兩週,兩週啊!因而在這樣的背景下,決定開始一次先後端徹底分離的嘗試。css
以前開發都是同步渲染和異步渲染混搭的,有些東西能夠有後端PHP幫你編譯好,如通用的頁面模板,後端傳回的頁面參數等。提早預感到此次徹底分離可能會遇到一些困難,可是項目上線要緊,也不能深刻搞架構,因而打算就用jQuery+handlebars,jQuery來完成頁面邏輯和DOM操做,用handlebars來完成頁面渲染,這個方案是如此的簡單粗暴,但好處能最穩妥的保證項目定期完成。其實先後端分離並非一件容易的工做,這麼作會有諸多不完善之處,後面再談。html
所謂的先後端分離,究竟是分離什麼呢?其實就是頁面的渲染工做,以前是後端渲染好頁面,交給前端來顯示,分離後前端須要本身拼裝html代碼,而後再顯示。前端來管理頁面的渲染有不少好處,好比減小網絡請求量,製做單頁面應用等。事情聽起來簡單,但這麼一分離又會牽扯到不少問題,好比:前端
以上每個問題都夠棘手,要處理好須要有設計精良又符合實際項目的方案。如今已經有不少框架能夠幫咱們作這些事情,Backbone, EmberJS, KnockoutJS, AngularJS, React, avalon等等,利用它們能夠架構起一個富前端。但框架畢竟是框架,要利用到實際項目中,仍是須要有本身的設計,框架並不能解決全部的問題。java
以前也有看過淘寶團隊的實踐,利用nodejs作一箇中間層,處理頁面渲染、路由控制、SEO等事情,將先後端的分界線進行了從新定義。我的感受這應該是一個正確的方向,有點顛覆的感受,前端走向工程化,將變成真正的全棧式大前端。不知如今這種架構是否在淘寶全面鋪開,真有點期待看看效果。node
以上的框架,還有淘寶的實踐,畢竟都是大牛之做,我這個小輩也只是參考學習過,未能在實際項目中使用。低頭看看本身如今手頭的項目,1個前端,2周時間,要完成一個完整的web項目,仍是用最穩妥最低級的方式來搞吧~web
項目總體並非一個單頁應用,但有些模塊須要作成局部的單頁操做,像這種須要分步完成的操做,只需局部加載子頁面便可。ajax
所以,一個模塊有一個主html頁面,初始只有一些基本的骨架,有一個名字相同的js文件,該模塊邏輯都在此js文件中,有一個名字相同的css文件,該模塊的全部樣式都定義在此css文件中。json
須要異步加載的子頁面,像上圖中每一個步驟的頁面,我都使用jQuery的$.load()方法來加載,此方法能在頁面某個容器中加載內容,並可指定回調函數,使用起來很方便。被異步加載的子頁面我都用_開頭,如_step1.html,用於作區分。後端
爲了確保瀏覽器的前進後退按鈕可用,我使用了hash來作路由標記,頁面地址如:publish.html#step2。有個缺陷是hash並不會發送給服務器,因此SEO就廢了。事實上使用history API也能夠更優雅的解決問題,但須要考慮兼容性,還有額外工做要作,考慮時間因素,退而求其次,何況本項目也無需作SEO。或者像淘寶的方案那樣,nodejs層與瀏覽器層統一路由,SEO問題能夠迎刃而解。但又明顯不在本人的實力範圍以內,汗–!
除了用$.load異步加載的子頁面,剩餘的局部頁面就是用handlebars提供的模板渲染了,我使用了handlebars的預編譯功能,不得不說很強大,一來節約了頁面加載階段所需的編譯時間(編譯handlebars模板),二來編譯後的模板(js文件)方便複用。
接下來就是前端邏輯如何組織,由於沒有用mv*框架,因此只能靠本身來寫一個便於開發的結構。如上面所述,每一個模塊有一個主js文件,文件內容結構以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
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定義的代碼也放出來吧:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
//以配置的方式代理事件
$.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>標籤可以正常加載。因此每一個頁面使用頭部的代碼就變成這樣:
1
2
3
4
|
<script src=
"static/js/tpl/head.js"
></script>
<div id=
"header"
>
<script src=
"static/js/includeHead.js"
></script>
</div>
|
includeHead.js中的代碼以下:
1
2
3
4
5
6
7
|
function
includeHead(){
var
header = document.getElementById(
'header'
);
var
compileHead = Handlebars.templates[
'head'
];
var
head = compileHead({});
document.write(head);
}
includeHead();
|
看着是有點彆扭,不過爲了實現功能,目前也就只能這樣了。
如上面所述,jQuery的$.load()方法能夠知足加載子頁面的需求,如今須要解決的問題是,無論用戶刷新頁面仍是前進後退,咱們都得根據hash值來渲染對應的視圖,其實就是路由控制。這個時候就須要監聽hashchange事件了,我定義了一個loadPage方法用來加載子頁面,而後綁定監聽器以下:
1
|
window.onhashchange =
this
.loadPage;
|
在loadPage方法中,根據hash的值來調用$.load()方法,子頁面的初始化工做,在$.load()的回調函數中指定。
這樣作還有一個便捷之處,咱們切換視圖沒必要手動調loadPage方法,只須要修改頁面的hash就能夠了,hash發生變化被監聽到,自動加載對應的子頁面。例如,點擊下一步進入步驟二:
1
2
3
|
'.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,以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
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能夠輸出你所要查詢的參數值,而後能夠直接寫在模板中,如:
1
|
<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。