天天看點

Handlebars 模闆引擎之前後端用法

前言

不知不覺間,居然已經這麼久沒有寫部落格了,堅持還真是世界上最難的事情啊。

不過我最近也沒閑着,辭工換工、戀愛失戀、深圳北京都經曆了一番,這有起有落的生活實在是太刺激了,就如拿着兩把菜刀剁洋蔥一樣,想想就淚流滿面。

棄我去者、昨日之日不可留,亂我心者、今日之日多煩憂,還是說說最近接觸到的模闆引擎 Handlebars 吧。

Handlebars 簡介

先引用下百科的說法:

Handlebars 是 JavaScript 一個語義模闆庫,通過對view和data的分離來快速建構Web模闆。它采用"Logic-less template"(無邏輯模版)的思路,在加載時被預編譯,而不是到了用戶端執行到代碼時再去編譯, 這樣可以保證模闆加載和運作的速度。

好吧,看了有點懵閉。這裡關鍵詞就是兩個:無邏輯、預加載。所有的模闆引擎都是view和data分離,這點不用說。無邏輯準确點來說應該是弱邏輯,畢竟裡面還是有一些if、each邏輯在的。你可能看過很多這樣寫的模闆語言:

1 <% if (names.length) { %>
2   <ul>
3     <% names.forEach(function(name){ %>
4       <li><%= name %></li>
5     <% }) %>
6   </ul>
7 <% } %>      

注:閉合的大括号一定不要忘了寫哦。

看這種 js 與 HTML 的雜交寫法我覺得很眼疼,我的眼裡代碼的可讀性是非常重要的,這種寫法真不是我的那杯茶!不過這種模闆技術的實作方式倒是值得一探,推薦看看這個20行代碼的模闆引擎實作:http://blog.jobbole.com/56689/,挺有意思的做法,當然用eval也可以做。

而 Handlebar 的文法就簡單精練了許多,比如上面的可以寫成:

1 {{#if names.length}}
2   <ul>
3     {{#each names}}
4       <li>{{this}}</li>
5     {{/each}}
6   </ul>
7 {{/if}}      

就喜歡這種一目了然的感覺,當然還有其他的swig、tx出的art-template之類的模闆引擎,蘿蔔青菜各有所愛,就不多說了。

文法基礎

文法很簡單,就是用大括号将 data 包裹起來。其中兩個 {{}} 會将内容做HTML編碼轉換,這裡你輸入的HTML标簽代碼什麼的都會按你輸入的字元輸出;而三個 {{{}}} 的時候則不做轉換,你在裡面輸入<h1>最後是真的能得到一個h1标簽的。其他一些規則要素分别有:

1)塊級

在 Handlebars裡面,每個#就代表了一個局部塊,每個塊都有自身的作用域範圍。舉例來說:

1 // 資料
2 hehe: { words: 'hehehehe' }
3 yoyo: { words: 'yoyoyoyo'}      

對應的模闆:

1 {{#hehe}}
2   <p>{{words}}</p>
3 {{/hehe}}
4 {{#yoyo}}
5   <p>{{words}}</p>
6 {{/yoyo}}      

這個例子很好了解,words屬性都是根據自身的對象來輸出的。這裡還是按照塊級作用域去了解會比較簡單(雖然js并沒有塊級作用域。。。),也可以用this來指代目前對象。注意,即使是#if、#each也是有作用域的,不要跟js中的作用範圍混為一談。

2)路徑

對于對象來說,你可以按照上文的例子一樣直接使用 name 的 length 屬性,還可以使用使用路徑的表達方式去通路對象的其他層級。舉個栗子:

1 var post = {
 2   title: "Blog Post!",
 3   author: [{
 4     id: 47,
 5     name: "Jack"
 6   },{
 7     id: 20,
 8     name: "Mark"
 9   }]
10 };      

模闆要這麼寫:

1 {{#post}}
 2   {{#if author.length}}
 3     <h3>{{title}}</h3>
 4     <ul>
 5       {{#each author}}
 6         <li>{{../title}}'s author is {{name}}</li>
 7       {{/each}}
 8     </ul>
 9   {{/if}}
10 {{/post}}      

li标簽裡面已經是在 author 字段之内了,是以要使用 '../' 來轉到上層的 title。

3)helper

上面其實已經用過helper了,内置的helper有if、each、unless、with等,當然你也可以自己去寫helper。由于Handlebar的弱邏輯屬性,如果要實作複雜一點的邏輯就需要去自定義helper。舉個栗子:

1 //判斷是否是偶數
 2 Handlebars.registerHelper('if_even', function(value, options) {
 3   console.log('value:', value); // value: 2
 4   console.log('this:', this); // this: Object {num: 2}
 5   console.log('fn(this):', options.fn(this)); // fn(this): 2是偶數
 6   if((value % 2) == 0) {
 7     return options.fn(this);
 8   } else {
 9     return options.inverse(this);
10   }
11 });      

helper是這樣用的:

1 {{#if_even num}}
2       {{this.num}}是偶數
3 {{else}}
4       {{this.num}}是奇數
5 {{/if_even}}      

當然輸出你也能想到,就是根據奇數偶數輸出相應資訊。我們看看定義的一個function(value, options){},這個items就是我們使用模闆時候的num,options是一些配置項,這裡我們用到的是fn函數,這個函數執行的結果就是編譯的結果(這裡結果是“2是偶數”這一句話)。另外一個options.inverse就是取反,對應的就是模闆裡面的else語句了。

but:在模闆中過度使用邏輯,實際上就是模糊了模闆的專注點,這有違原本資料和表現分離的出發點。我還是認為模闆應該專注資料綁定,邏輯應該在資料層做預處理,然後将結果傳回給模闆,而不是讓模闆去做各種資料的運算。

4)partial

使用模闆引擎最重要的一點就是使用其partial功能,Handlebars裡面是按照注冊再使用的方式來管理partial的。舉個栗子:

1 Handlebars.registerPartial('userMessage',
2   '<{{tagName}}>By {{author.firstName}} {{author.lastName}}</{{tagName}}>'
3   + '<div class="body">{{body}}</div>'
4 );      

使用的時候就可以直接使用{{> userMessage}}将這個小塊引入到頁面中了。這裡就是簡單的局部替換,是以partial裡面的data跟目前頁面的data是在同一級的作用域内,也就是說你隻要定義好author、body傳進去就行了。tagName這個屬于表現層的變量,應該在hbs檔案裡面進行聲明,也即是{{> userMessage tagName="h1" }}這樣使用。

在前端使用hbs

直接引入js的方式就不多說了,這裡我是使用webpack來統一管理各種資源的。Handlebars對應的webpack插件為handlebars-loader,loader的配置非常簡單:

1 { 
2   test: /\.hbs$/,
3   loader: "handlebars"
4 }      

Handlebars的字尾有兩種,全稱的handlebars以及簡稱的hbs,也可以直接用html,但還是跟普通html檔案區分開來好一點。

使用模闆的好處當然就是可以元件化開發了。我這裡采用的目錄是這樣的:

Handlebars 模闆引擎之前後端用法

其中頁面元件指的是應用中的頁面單元,頁面是由各種控件元件組成的,這些都已經是共識了,就不再贅述了。引用的方法有幾種:

(1)因為Handlebar編譯出來的隻是一個字元串,是以我們可以用js作為入口去管理元件,每個元件的js檔案引入相應的css和模闆,輸出為dom字元串。頁面引用元件的時候就直接引用js子產品得到dom字元串,然後将dom字元串渲染到相應的{{{}}}中去。這種js大一統的方式跟現在主流架構的做法是一樣的,可以将邏輯、樣式、内容和資源統一起來管理,元件也得内聚性比較強。

1 // header.js
 2 require('./header.scss');
 3 var headerTpl = require('./header.hbs');
 4 var data = {words: "This is header!"}; //data可以用參數傳入
 5 var header = headerTpl(data);
 6 module.exports = header;
 7 
 8 // home.hbs
 9 <div class="home">
10   {{{ header }}}
11 <h2>This is {{name}} page.</h2>
12 {{{ footer }}}
13 </div>
14 
15 // home.js
16 require('./home.scss');
17 var header = require('../../component/header/header.js');
18 var footer = require('../../component/footer/footer.js');
19 var homeTpl = require('./home.hbs');
20 var data = {
21   header: header,
22   footer: footer,
23   name: 'home'
24 };
25 var home = homeTpl(data);
26 module.exports = home;      

(2) 另外的方案就是使用局部模闆的方式了,這種方式對一些不帶js邏輯的元件非常合适,比如頁頭頁尾這些純内容的元件。在hbs裡面可以直接按照路徑去引用particle,然後把引入元件的時候提供partial所需的資料,例如home頁面就是這樣的:

<div class="home">
  {{> ../../component/header/header}}
  <h2>this is {{}} page</h2>
  {{> ../../component/footer/footer}}
</div>      

既然我們已經用了webpack來管理,當然也可以讓webpack來處理引用路徑了,這裡隻需要在配置裡面聲明partial的路徑即可直接引用,loader配置:

1 {
 2   test: /\.hbs$/,
 3   loader: "handlebars",
 4   query: {
 5     partialDirs: [
 6       path.join(SRC_PATH, 'component', 'header'),
 7       path.join(SRC_PATH, 'component', 'footer'),
 8       path.join(SRC_PATH, 'page', 'home')
 9     ]
10   }
11 }      

模闆檔案:

<div class="home">
  {{> header }}
    <h2>This is {{name}} page.</h2>
  {{> footer }}
</div>      

上面列出的幾種方式各有優劣,使用partial的方式可以将相應的模闆檔案集中放到一個view檔案夾裡面,partialDirs就不用寫一大堆路徑了。個人還是更偏向于使用第一種方式,每個元件的css、html、js檔案做成一個整體的方式,遵循就近管理原則。

Nodejs後端使用hbs

Node後端使用hbs也非常友善,這裡我用的是express架構,直接後端渲染。當然更精細的做法就是首屏渲染、僅移動端後端渲染了,在這種混搭的場合模闆是可以通用的,這樣就減少了一定的開發工作量。目前express中自帶4種模闆引擎,jade、esj、hogan與hbs,我是使用express-generator來生成項目腳手架的,輸入指令為: express --hbs 項目名。

express-generator中的hbs用的是hbs庫(https://github.com/donpark/hbs),而并非很多資料介紹的express-handlebars。hbs預設使用layout模闆,實際上就是将你的模闆檔案替換掉{{{body}}}。layout是可配置的,可以在渲染選項中通過layout項來配置。

1 res.render('index', {
2   title: 'Express',
3   head: '<h1>head part</h1>',
4   layout: true  //預設為true,設為false則不啟用layout模闆
5 });      

目前我所接觸到的hbs項目都是express+hbs+zepto/jq這一套,如果有用其他前端架構的話,一般也不會用到hbs了,是以隻說說這種情況。後端渲染跟前端渲染的開發模式略有差異,但思路還是一樣的要做元件化開發。上文說過前端使用hbs的時候是以js為入口,而在後端使用hbs的話個人認為更适合使用局部模闆的方式。

我的目錄是這樣的:

Handlebars 模闆引擎之前後端用法

頁面統一放入views中,局部模闆放入views/partial裡面。js和css還是按照官方預設的方式集中管理。使用局部模闆要先注冊,需要在app.js這個伺服器腳本裡面加入以下代碼:

1 var hbs = require('hbs');
2 hbs.registerPartials(__dirname + '/views/partials');      

模闆檔案中引入小模闆:

1 <!DOCTYPE html>
 2 <html>
 3 <head>
 4   <title>{{title}}</title>
 5   <meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=0">
 6   <link rel="stylesheet" href="/css/main.css"/>
 7   {{> resource}}
 8 </head>
 9 <body>
10 {{> header}}
11 <div class="container">
12   {{{body}}}
13 </div>
14 {{> footer}}
15 </body>
16 </html>      

resource模闆主要是控制不同頁面引入的不同資源:

1 {{#each css}}
2   <link rel='stylesheet' href={{this}} />
3 {{/each}}
4 {{#each js}}
5   <script src={{this}}></script>
6 {{/each}}      

頁面渲染的時候就是這樣的:

1 res.render('index', {
2   title: 'Express',
3   css: ['/css/home.css', '/css/home_add.css'],
4   js: ['/js/home.js'],
5   name: "茄果" //這個是頁面中用到的資料,與title同一性質
6 });      

這種方式的一個問題就是css、js這些資源的寫法跟我們平常直接在html引用的方式不一樣。比如我想資源引用寫在頁面中,比如home.hbs裡面,如果直接寫入home.hbs裡面的話,内容是直接插入到{{{body}}}的位置,但我們想要的是在head的位置啊。這個如何實作呢?之前我們定義partial隻是為了簡單的替換,這一次除了替換還要做一個插入dom的操作,這個就要用到helper來幫我們完成了。很多時候我們要把css放入頭部,而把js放在頁面尾部,是以layout檔案要改造成:

1 <!DOCTYPE html>
 2 <html>
 3 <head>
 4   <title>{{title}}</title>
 5   <meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=0">
 6   <link rel="stylesheet" href="/css/main.css"/>
 7   {{#each cssList}}
 8     <link rel='stylesheet' href={{this}} />
 9   {{/each}}
10 </head>
11 <body>
12 {{> header}}
13 <div class="container">
14   {{{body}}}
15 </div>
16 {{> footer}}
17 {{#each jsList}}
18   <script src={{this}}></script>
19 {{/each}}
20 </body>
21 </html>      

現在隻要注冊helper把hbs檔案中定義的值傳入cssList、jsList中即可。另外還要考慮到頁面元件引用的時候可能會出現重複依賴的情況,是以要做一個去重的工作。注冊helper與注冊partial一樣,都要寫在app.js檔案中,下面給出css的寫法,js也類似:

1 hbs.registerHelper('css', function(str, option) {
 2   var cssList = this.cssList || [];
 3   str = str.split(/[,,;;]/);
 4   console.log('css: ',str);
 5   str.forEach(function (item) {
 6     if(cssList.indexOf(item)<0) {
 7       cssList.push(item);
 8     }
 9   });
10   this.cssList = cssList.concat();
11 });      

頁面中引入css、js就應該是這樣:

{{css "/css/home_add.css"}}
{{js "/js/home.js"}}
{{> p}}
<p>This is home page!</p>      

上面例子中的局部模闆p作為一個元件,也引用了相應的css和js,寫法跟頁面的寫法是一樣的。

總結

handlebars作為一個非常輕量級的模闆引擎,單純從模闆這個功能上看,他的前後端通用性強,指令簡單明了,代碼可讀性強。但他是一個單純的模闆引擎,在前端架構滿天飛的年代感覺是有點弱了。無論是前端還是後端,各種大架構都有渲染子產品。當然不喜歡大架構的全家桶倒是可以考慮使用handlebars,是以主要還是要看項目吧。青菜蘿蔔,各有所好。

文字較多,慣例上圖吧。

Handlebars 模闆引擎之前後端用法

繼續閱讀