關於前端的模板引擎,我用一個公式來解釋html
模板引擎 模板 + 數據 ========> html頁面
模板引擎就像是html的解析生成器,將對應的模板填充完數據以後生成靜態的html頁面。它能夠在瀏覽器端(好比angular中指令所用的模板)也能夠在服務器端執行,不過通常用於服務器端。由於它的一個做用是抽象公共頁面來重用,若是在服務端填充數據,能夠減小回填數據給頁面的ajax請求,從而提高瀏覽器端總體頁面渲染速度。前端
接觸過的模板引擎不算多,最先應該是jsp,本質上也是一種模板引擎,再到功能稍微強大的freemarker,這兩種都是屬於java語系的。js語系的jade和ejs我都有所接觸,不過不經常使用,jade那種類python的語法規則以及較低的解析效率都讓我不敢興趣,Express框架也只是早起將其做爲模板引擎。後來換成了強大的ejs,不管是功能仍是寫法上都接近jsp了。直到最新的Express4發佈,默認改成了弱邏輯的比較簡潔的模板引擎handlebars。java
我使用handlebars有如下幾個緣由:node
運行環境:Express四、hbs4 未接觸Express或hbs的能夠先看這裏python
數據:git
{ title: 'Express', obj:{ version: 'v4.3', category: 'node', "date~": '2016' } }
模板:github
<p>{ {title} }</p> <p>{ {obj.version} }</p> <p>{ {obj/category} }</p> <p>{ {obj.date~} }</p>
html頁面:ajax
Express v4.3 node
handlebars中變量都添加雙花括號來表示(相似Angular),對比ejs的"<%%>「來講看起來沒什麼區別,其實這是很人性化的,想一下你鍵盤上的位置,再考慮按這幾個字符的難易程度你就懂了。 其中要訪問變量的屬性值時能夠用相似json格式的」.",也能夠用"/"。express
其中變量名不可包含如下字符。若是包含則不被解析,如上的"{{obj.date~}}"。編程
空格 ! " # % & ' ( ) * + , . / ; < = > @ [ \ ] ^ ` { | } ~
但能夠用 " , ’ , [] 來轉譯這些特殊字符。
這一條規則意味着 「&&」,"||","!"這類邏輯判斷是不能出如今表達式中的! (看着這一條是否是以爲弱爆了,要否則怎麼叫若邏輯模板引擎呢~哈哈,不過固然有另外的解決辦法)。
英語水平有限,實在找不到一個恰當的詞來翻譯它了。能夠理解爲它是注入到模板中的一個函數,用來接收參數並進行邏輯處理。
{ {#if author} } <h1>{ {firstName} } { {lastName} }</h1> { {else} } <h1>Unknown Author</h1> { {/if} }
{ {#if isActive} } <img src="star.gif" alt="Active"> { {else if isInactive} } <img src="cry.gif" alt="Inactive"> { {/if} }
和通常的編程語言的 if-else 代碼塊是差很少的,不過再次重申因爲上面提到的特殊字符,因此if條件中是不能有邏輯表達式的,只能是變量或者值。
仍是由於上面提到的那些字符,handlebars不支持邏輯非("!"),因此又有了一個與if相反的helper
{ {#unless license} } <h3 class="warning">WARNING: This entry does not have a license!</h3> { {/unless} }
上面這段代碼就等價於
{ {#if license} } { {else} } <h3 class="warning">WARNING: This entry does not have a license!</h3> { {/if} }
都知道each至關於for循環。不過有些地方須要注意:
as |xxx|
的形式給變量起別名,循環中經過別名能夠引用父級變量值。固然也能夠經過相對路徑的方式引用父級變量。{ {#each passage} } { {#each paragraphs} } { {@../index} }:{ {@index} }:{ {this} }</p> { {else} } <p class="empty">No content</p> { {/each} } { {/each} }
{ {#each array as |value, key|} } { {#each child as |childValue, childKey|} } { {key} } - { {childKey} }. { {childValue} } { {/each} } { {/each} }
同時也能夠用來遍歷對象,這時@key表示屬性名,this表示對應的值
{ {#each object} } { {@key} }: { {this} } { {/each} }
相似js中的with,能夠配合分頁使用,限定做用域。
{ {#with author as |myAuthor|} } <h2>By { {myAuthor.firstName} } { {myAuthor.lastName} }</h2> { {else} } <p class="empty">No content</p> { {/with} }
這個用於如下這種並列數組的狀況,能夠按照索引來找兄弟變量對應的值。理解起來有些困難,直接看代碼
{ groups: [ {id: 1, title: "group1"}, {id: 2, title: "group2"}, ], users: [ {id:1, login: "user1", groupId: 1}, {id:2, login: "user2", groupId: 2}, {id:3, login: "user3", groupId: 1} ], infos: [ 'a','b','c' ] }
<table> { {#each users} } <tr data-id="{ {id} }"> <td>{ {login} }</td> <td data-id="{ {groupId} }">{ {lookup ../infos @index} }</td> </tr> { {/each} } </table>
user1 a user2 b user3 c
這裏在users數組中按照索引值引用infos數組中對應的值,若是想引用groups中的groupId呢?很簡單,用with。
<table> { {#each users} } <tr data-id="{ {id} }"> <td>{ {login} }</td> <td data-id="{ {groupId} }">{ {#with (lookup ../groups @index)} }{ {title} }{ {/with} }</td> </tr> { {/each} } </table>
內置的helper不夠強大,因此一般須要寫js代碼自定義helper,先看一個簡單的單行helper。
數值、字符串、布爾值這種常規數據能夠直接傳入,同時也能夠傳遞JSON對象(但只能傳一個),以key=value這種形式寫在後面,最後就能夠經過參數的hash屬性來訪問了。
模板
{ {agree_button "My Text" class="my-class" visible=true counter=4} }
代碼
hbs.registerHelper('agree_button', function() { console.log(arguments[0]);//==>"My Text" console.log(arguments[1].hash);//==>{class:"my-class",visible:true,conter:4} }
傳變量時能夠用this指針來指代它訪問屬性,經過邏輯判斷後能夠返回一段html代碼,不過太建議這樣作。考慮之後的維護性,這種html代碼和js代碼混合起來的維護性是比較差的,若是要抽象層組件仍是使用分頁比較好。
模板:
{ {agree_button person} }
註冊helper:
hbs.registerHelper('agree_button', function(p) { console.log(p===this);//==> true var blog = hbs.handlebars.escapeExpression(this.person.blog), name = hbs.handlebars.escapeExpression(this.person.name); return new hbs.handlebars.SafeString( "<a href='"+blog+"'>"+ name + "</button>" ); });
數據:
var context = { person:{name: "哈哈哈", blog: "https://yalishizhude.github.io"} }; };
html頁面:
<a href="https://yalishizhude.github.io">亞里士朱德</a>
當內容只想作字符串解析的時候能夠用 escapeExpression 和 SafetString 函數。
塊級helper獲取參數的方式跟以前差很少,只是最後多了一個參數,這個參數有兩個函數fn
和revers
能夠和else
搭配使用。後面將會講解。
模板:
{ {#list nav} } <a href="{ {url} }">{ {title} }</a> { {/list} }
註冊helper:
Handlebars.registerHelper('list', function(context, options) { var ret = "<ul>"; for(var i=0, j=context.length; i<j; i++) { ret = ret + "<li>" + options.fn(context[i]) + "</li>"; } return ret + "</ul>"; });
數據:
{ nav: [ { url: "https://yalishihzude.github.io", title: "blog" }, { url: "https://www.github.com/yalishizhude", title: "github" }, ] }
html頁面:
<ul> <li> <a href="https://yalishizhude.github.io">blog</a> </li> <li> <a href="https://www.github.com/yalishizhude">github</a> </li> </ul>
each的index變量比較經常使用,可是它是從0開始的,每每不符合業務中的需求,這裏寫個helper來擴展一下。
註冊helper:
hbs.registerHelper('eval', function(str, options){ var reg = /\{\{.*?\}\}/g; var result = false; var variables = str.match(reg); var context = this; //若是是each if(options.data){ context.first = context.first||options.data.first; context.last = context.last||options.data.last; context.index = context.index||options.data.index; context.key = context.key||options.data.key; } _.each(variables, function(v){ var key = v.replace(/{ {|} }/g,""); var value = typeof context[key]==="string"?('"'+context[key]+'"'):context[key]; str = str.replace(v, value); }); try{ result = eval(str); return new hbs.handlebars.SafeString(result); }catch(e){ return new hbs.handlebars.SafeString(''); console.log(str,'--Handlerbars Helper "eval" deal with wrong expression!'); } });
模板:
{ {#each list} } { {eval '{ {index} }+1'} } { {/each} }
上面說到if不支持複雜的表達式,若是是「&&」操做還能夠用子表達式來實現,更加複雜的就很差辦了,這裏我寫了一個helper來實現。
註冊helper:
hbs.registerHelper('ex', function(str, options) { var reg = /\{\{.*?\}\}/g; var result = false; var variables = str.match(reg); var context = this; _.each(variables, function(v){ var key = v.replace(/{ {|} }/g,""); var value = typeof context[key]==="string"?('"'+context[key]+'"'):context[key]; str = str.replace(v, value); }); try{ result = eval(str); if (result) { return options.fn(this); } else { return options.inverse(this); } }catch(e){ console.log(str,'--Handlerbars Helper "ex" deal with wrong expression!'); return options.inverse(this); } });
模板:
{ {#ex "{ {state} }==='submiting'"} } <i class="icon cross-danger">1</i> { {else} } <i class="icon cross-success">2</i> { {/ex} }
先將整個邏輯表達式做爲一個字符串傳入,而後替換其中的變量值,最後用eval函數來解析表達式,同時增長異常處理。
比較推崇使用分頁來實現組件化。分頁跟helper同樣須要先註冊。在hbs模塊中能夠批量註冊,比較簡單。
hbs.registerPartials(__dirname + '/views/partials');
用「>」來引用模板,這種狀況通常用來處理頁頭頁尾這種簡單的分頁。後面能夠傳入參數。 { {> myPartial param} }
當使用塊級表達式時,咱們一般添加「#」,而分頁是「>」,因此塊級分頁使用「#>」,這裏表示若是layout分頁不存在則顯示塊內的內容My Content。
{ {#> layout } } My Content { {/layout} }
固然也能夠用表達式來代替分頁名稱
{ {> (whichPartial) } }
當分頁中一部分代碼是固定的,另外一部分是變化的時候,能夠在分頁中添加「@partial-block」,這時當引用這個分頁時,在內部編寫代碼將會填充到這個位置。
partial.hbs:
{ {> [@partial-block](/user/partial-block) } }
模板:
{ {#>partial} } https:yalishizhude.github.io { {/partial} }
當有多段代碼須要填充到分頁時,能夠用以下方法。分頁中內嵌分頁變量,模板中經過內聯分頁的方式傳入。
模板:
{ {#> partial} } { {#*inline "nav"} } 哈哈哈 { {/inline} } { {#*inline "content"} } https://yalishizhude.github.io { {/inline} } { {/partial} }
partial.hbs:
<div class="nav"> { {> nav} } </div> <div class="content"> { {> content} } </div>
html頁面:
<div class="nav"> 哈哈哈哈 </div> <div class="content"> https://yalishizhude.github.io </div>
本文列舉的只是handlebars中最重要和經常使用的功能,更多細碎的功能能夠去查看 官方API。
我想將導航條寫成一個分頁(partial),導航條左邊的文字標題是能夠經過參數傳遞的,可是右邊的內容多是文字、圖片其它元素,須要具體業務自定義實現。我又不想把html代碼寫在js中,因此但願在模板中將這段未知的模板代碼填充到分頁中進行展示。我在官網文檔中找到了 {{>@partial-block}}來實現此功能,可是本機實驗一直解析報錯。 解決過程: 這個問題緣由可能有兩個,一是官方文檔有錯,二是本機環境的插件有問題(Express用hbs模塊,該模塊封裝了handlebars引擎模塊)。爲了驗證官方文檔的正確性,我找到了一個在線handlebars解析器,輸入文檔中的代碼時能夠正確解析,那麼只可能出如今hbs模塊了。這時在github上找到hbs模塊最新版本爲4,查看本地版本爲3,更新後果真能夠正常解析了。
handlebars讓咱們看到一個好的插件應該有的特徵: