前面我們在 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ù)的意思是:
我們以修改主頁為例,學(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。
讀者可自行完成剩余的修改工作。