鍍金池/ 教程/ HTML/ 番外篇之——使用 Handlebars
第9章 增加標(biāo)簽和標(biāo)簽頁面
番外篇之——使用 Mongoose
番外篇之——使用 Async
第4章 實(shí)現(xiàn)用戶頁面和文章頁面
第12章 增加友情鏈接
第14章 增加頭像
第7章 實(shí)現(xiàn)分頁功能
第5章 增加編輯與刪除功能
第11章 增加文章檢索功能
第3章 增加文件上傳功能
番外篇之——部署到 Heroku
第2章 使用 Markdown
第13章 增加404頁面
第16章 增加日志功能
第1章 一個(gè)簡單的博客
番外篇之——使用 Handlebars
第10章 增加pv統(tǒng)計(jì)和留言統(tǒng)計(jì)
番外篇之——使用 Passport
第15章 增加轉(zhuǎn)載功能和轉(zhuǎn)載統(tǒng)計(jì)
第8章 增加存檔頁面
番外篇之——使用 generic pool
番外篇之——使用 _id 查詢
番外篇之——使用 Disqus
番外篇之——使用 KindEditor
第6章 實(shí)現(xiàn)留言功能

番外篇之——使用 Handlebars

前面我們在 Express 中使用的 EJS 模板引擎進(jìn)行渲染視圖和頁面的展示。當(dāng)模版文件代碼比較多且邏輯復(fù)雜時(shí),代碼就變得非常難看了,滿眼的 <% 和 %>。下面我們嘗試使用 Handlebars 這個(gè)模版引擎替換 EJS ,代碼會(huì)變得整潔許多。

Handlebars 是 JavaScript 一個(gè)語義模板庫,通過對 view 和 data 的分離來快速構(gòu)建 Web 模板。它采用 "Logic-less template"(無邏輯模版)的思路,在加載時(shí)被預(yù)編譯,而不是到了客戶端執(zhí)行到代碼時(shí)再去編譯,這樣可以保證模板加載和運(yùn)行的速度。Handlebars 兼容 Mustache,你可以在 Handlebars 中導(dǎo)入 Mustache 模板。

Handlebars 的語法也非常簡單易學(xué)。這里我們不會(huì)講解 Handlebars 的語法,官網(wǎng)( http://handlebarsjs.com/ )的文檔非常全面。

我們使用 express-handlebars 這個(gè)第三方包添加對 Handlebars 的支持。

注意:也許你會(huì)非常自覺的認(rèn)為應(yīng)該使用 npm install handlebars 安裝 Handlebars 然后開始大刀闊斧地修改代碼。但在這里我們不使用官方提供的 Handlebars 包,Express 默認(rèn)支持的模板引擎中不包含 Handlebars ,雖然我們可以通過 consolidate.js + handlebars 實(shí)現(xiàn),但仍然有一個(gè)缺點(diǎn)是不支持從一個(gè)模版文件加載另一個(gè)模版文件,而在 EJS 中可以使用 <%- include someTemplate %> 輕松實(shí)現(xiàn)。express-handlebars 包彌補(bǔ)了該缺點(diǎn),所以我們使用 express-handlebars 來完成代碼的修改。

首先,打開 package.json ,刪除 ejs 并添加對 express-handlebars 的依賴:

"express-handlebars": "*"

并 npm install 安裝 express-handlebars 包。

打開 app.js ,添加一行:

var exphbs  = require('express-handlebars');

然后將:

app.set('view engine', 'ejs');

修改為:

app.engine('hbs', exphbs({
  layoutsDir: 'views',
  defaultLayout: 'layout',
  extname: '.hbs'
}));
app.set('view engine', 'hbs');

這里我們注冊模板引擎處理后綴名為 hbs 的文件,然后通過 app.set('view engine', 'hbs'); 設(shè)置模板引擎。以上參數(shù)的意思是:

  • layoutsDir: 'views': 設(shè)置布局模版文件的目錄為 views 文件夾
  • defaultLayout: 'layout': 設(shè)置默認(rèn)的頁面布局模版為 layout.hbs 文件,跟 Express 2.x 中的 layout.ejs 作用類似。
  • extname: '.hbs': 模版文件使用的后綴名,這個(gè) .hbs 是我們自定的,我們當(dāng)然可以使用 .html 和 .handlebars 等作為后綴,只需把以上的 hbs 全部替換即可。

我們以修改主頁為例,學(xué)習(xí)如何使用 Handlebars 。為了測試修改后能否正常顯示文章及其相關(guān)信息,在開始之前,我們先注冊幾個(gè)用戶并發(fā)表幾篇文章,然后進(jìn)行一些互相轉(zhuǎn)載、訪問和留言等工作,而不是清空數(shù)據(jù)庫。

然后打開 views 文件夾,刪除 header.ejs 和 footer.ejs ,新建 layout.hbs ,添加如下代碼:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Blog</title>
<link rel="stylesheet" href="/stylesheets/style.css">
</head>
<body>

<header>
<h1>{{title}}</h1>
</header>

<nav>
<span><a title="主頁" href="/">home</a></span>
<span><a title="存檔" href="/archive">archive</a></span>
<span><a title="標(biāo)簽" href="/tags">tags</a></span>
<span><a title="友情鏈接" href="/links">links</a></span>
{{#if user}}
  <span><a title="上傳" href="/upload">upload</a></span>
  <span><a title="發(fā)表" href="/post">post</a></span>
  <span><a title="登出" href="/logout">logout</a></span>
{{else}}
  <span><a title="登錄" href="/login">login</a></span>
  <span><a title="注冊" href="/reg">register</a></span>
{{/if}}
<span><form action="/search" method="GET"><input type="text" name="keyword" placeholder="SEARCH" class="search" /></form></span>
</nav>

<article>

{{#if success}}
  <div>{{success}}</div>
{{/if}}
{{#if error}}
  <div>{{error}}</div>
{{/if}}

{{{body}}}

</article>
</body>
</html>

這里我們定義了一個(gè)默認(rèn)的頁面布局模版(layout.hbs)。其余所有的模版都將 "繼承" 該模版,即替換掉 {{{body}}} 部分。

刪除 index.ejs ,新建 index.hbs ,添加如下代碼:

{{#each posts}}
  <p><h2><a href="/u/{{name}}/{{time.day}}/{{title}}">{{title}}</a></h2>
  <a href="/u/{{name}}"><img src="{{head}}" class="r_head" /></a></p>
  <p class="info">
    作者:<a href="/u/{{name}}">{{name}}</a> | 
    日期:{{time.minute}} | 
    標(biāo)簽:
    {{#each tags}}
      {{#if this}}
        <a class="tag" href="/tags/{{this}}">{{this}}</a>
      {{/if}}
    {{/each}}
    {{#if reprint_info.reprint_from}}
      <br><a href="/u/{{reprint_info.reprint_from.name}}/{{reprint_info.reprint_from.day}}/{{reprint_info.reprint_from.title}}">原文鏈接</a>
    {{/if}}
  </p>
  <p>{{{post}}}</p>
  <p class="info">
    閱讀:{{pv}} | 
    評論:{{comments.length}} | 
    轉(zhuǎn)載:
    {{#if reprint_info.reprint_to}}
      {{reprint_info.reprint_to.length}}
    {{else}}
      0
    {{/if}}
  </p>
{{/each}}

這樣就可以了,現(xiàn)在運(yùn)行你的博客試試吧。

當(dāng)我們渲染 index.hbs (res.render('index', { ... });)時(shí),index.hbs 會(huì)替換 layout.hbs 中的 {{{body}}} 部分,然后渲染視圖。需要注意的是,我們在 {{#each}} ... {{/each}} 中使用了 this ,這里的 this 指向當(dāng)前上下文,即代表遍歷的每一項(xiàng)。

注意:Handlebars 中的 {{{htmlContext}}},相當(dāng)于 EJS 中的 <%- htmlContext %> ,{{textContext}} 相當(dāng)于 <%= textContext %> 。

在 ejs 中,我們可以隨意使用 JavaScript 表達(dá)式,如 <% if (1 + 1 === 2) { %> ... <% } %> ,但在 Handlebars 中我們卻不能這樣寫 {{#if (1 + 1 === 2)}} ... {{/if}} ,那么該如何修改 archive.ejs 呢?archive.ejs 代碼如下:

<%- include header %>
<ul class="archive">
<% var lastYear = 0 %>
<% posts.forEach(function (post, index) { %>
  <% if (lastYear != post.time.year) { %>
    <li><h3><%= post.time.year %></h3></li>
  <% lastYear = post.time.year } %>
    <li><time><%= post.time.day %></time></li>
    <li><a href="/u/<%= post.name %>/<%= post.time.day %>/<%= post.title %>"><%= post.title %></a></li>
<% }) %>
</ul>
<%- include footer %>

我們通過定義了一個(gè) lastYear 變量實(shí)現(xiàn)了判斷并只顯示一次年份的功能。在 Handlebars 中,我們可以通過 registerHelper 實(shí)現(xiàn)以上功能,關(guān)于 registerHelper 的使用詳見 http://handlebarsjs.com/block_helpers.html。在 express-handlebars 中使用 registerHelper 也很簡單,具體如下。

打開 index.js ,將 app.get('/archive') 修改如下:

app.get('/archive', function (req, res) {
  Post.getArchive(function (err, posts) {
    if (err) {
      req.flash('error', err); 
      return res.redirect('/');
    }
    res.render('archive', {
      title: '存檔',
      posts: posts,
      user: req.session.user,
      success: req.flash('success').toString(),
      error: req.flash('error').toString(),
      helpers: {
        showYear: function(index, options) {
          if ((index == 0) || (posts[index].time.year != posts[index - 1].time.year)) {
            return options.fn(this);
          }
        }
      }
    });
  });
});

刪除 archive.ejs ,新建 archive.hbs ,添加如下代碼:

<ul class="archive">
{{#each posts}}
  {{#showYear @index}}
    <li><h3>{{this.time.year}}</h3></li>
  {{/showYear}}
  <li><time>{{this.time.day}}</time></li>
  <li><a href="/u/{{this.post.name}}/{{this.time.day}}/{{this.title}}">{{this.title}}</a></li>
{{/each}}
</ul>

假如你了解如何使用 Handlebars 中的 registerHelper ,那么上面的代碼就很容易理解了。其中,{{#each}} ... {{/each}} 內(nèi)的 @index 表示當(dāng)前遍歷的索引。

最后,還需提醒的一點(diǎn)是:我們每次渲染一個(gè)視圖文件時(shí),都會(huì)結(jié)合 layout.hbs 然后渲染,有時(shí)候我們并不需要 layout.hbs ,比如 404 頁面,需設(shè)置為:

res.render('404', {
  layout: false
});

通過設(shè)置 layout: false 就取消了自動(dòng)加載 layout.hbs 頁面布局模版。

至此,我們通過采用 layout 的方式實(shí)現(xiàn)了視圖文件的加載及渲染,express-handlebars 還提供了另一種類似于 EJS 中 include 的加載方式——使用 partial ,前面的修改中我們并沒有添加分頁模版(paging.hbs),要想引入分頁模版使用 {{> paging}} 即可。詳細(xì)使用見 https://github.com/ericf/express-handlebars。

讀者可自行完成剩余的修改工作。