番外篇之——使用 Handlebars - GeoffreyQiao/N-blog GitHub Wiki
前面我们在 Express 中使用的 EJS 模板引擎进行渲染视图和页面的展示。当模版文件代码比较多且逻辑复杂时,代码就变得非常难看了,满眼的 <%
和 %>
。下面我们尝试使用 Handlebars 这个模版引擎替换 EJS ,代码会变得整洁许多。
Handlebars 是 JavaScript 一个语义模板库,通过对 view 和 data 的分离来快速构建 Web 模板。它采用 "Logic-less template"(无逻辑模版)的思路,在加载时被预编译,而不是到了客户端执行到代码时再去编译,这样可以保证模板加载和运行的速度。Handlebars 兼容 Mustache,你可以在 Handlebars 中导入 Mustache 模板。
Handlebars 的语法也非常简单易学。这里我们不会讲解 Handlebars 的语法,官网( http://handlebarsjs.com/ )的文档非常全面。
我们使用 express-handlebars 这个第三方包添加对 Handlebars 的支持。
注意:也许你会非常自觉的认为应该使用 npm install handlebars
安装 Handlebars 然后开始大刀阔斧地修改代码。但在这里我们不使用官方提供的 Handlebars 包,Express 默认支持的模板引擎中不包含 Handlebars ,虽然我们可以通过 consolidate.js + handlebars 实现,但仍然有一个缺点是不支持从一个模版文件加载另一个模版文件,而在 EJS 中可以使用 <%- include someTemplate %>
轻松实现。express-handlebars 包弥补了该缺点,所以我们使用 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');
设置模板引擎。以上参数的意思是:
-
layoutsDir: 'views'
: 设置布局模版文件的目录为 views 文件夹 -
defaultLayout: 'layout'
: 设置默认的页面布局模版为 layout.hbs 文件,跟 Express 2.x 中的 layout.ejs 作用类似。 -
extname: '.hbs'
: 模版文件使用的后缀名,这个.hbs
是我们自定的,我们当然可以使用.html
和.handlebars
等作为后缀,只需把以上的hbs
全部替换即可。
我们还可以设置其他几个参数,详见 https://github.com/ericf/express-handlebars。
我们以修改主页为例,学习如何使用 Handlebars 。为了测试修改后能否正常显示文章及其相关信息,在开始之前,我们先注册几个用户并发表几篇文章,然后进行一些互相转载、访问和留言等工作,而不是清空数据库。
然后打开 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="标签" 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="发表" 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>
这里我们定义了一个默认的页面布局模版(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}} |
标签:
{{#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}} |
转载:
{{#if reprint_info.reprint_to}}
{{reprint_info.reprint_to.length}}
{{else}}
0
{{/if}}
</p>
{{/each}}
这样就可以了,现在运行你的博客试试吧。
当我们渲染 index.hbs (res.render('index', { ... });
)时,index.hbs 会替换 layout.hbs 中的 {{{body}}}
部分,然后渲染视图。需要注意的是,我们在 {{#each}} ... {{/each}}
中使用了 this
,这里的 this
指向当前上下文,即代表遍历的每一项。
注意:Handlebars 中的 {{{htmlContext}}}
,相当于 EJS 中的 <%- htmlContext %>
,{{textContext}}
相当于 <%= textContext %>
。
在 ejs 中,我们可以随意使用 JavaScript 表达式,如 <% 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 %>
我们通过定义了一个 lastYear 变量实现了判断并只显示一次年份的功能。在 Handlebars 中,我们可以通过 registerHelper 实现以上功能,关于 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}}
内的 @index 表示当前遍历的索引。
最后,还需提醒的一点是:我们每次渲染一个视图文件时,都会结合 layout.hbs 然后渲染,有时候我们并不需要 layout.hbs ,比如 404 页面,需设置为:
res.render('404', {
layout: false
});
通过设置 layout: false
就取消了自动加载 layout.hbs 页面布局模版。
至此,我们通过采用 layout 的方式实现了视图文件的加载及渲染,express-handlebars 还提供了另一种类似于 EJS 中 include 的加载方式——使用 partial ,前面的修改中我们并没有添加分页模版(paging.hbs),要想引入分页模版使用 {{> paging}}
即可。详细使用见 https://github.com/ericf/express-handlebars。
读者可自行完成剩余的修改工作。