Nodejs实战 —— Web 程序的模板

读 《node.js实战2.0》,进行学习记录总结。

当当网购买链接

豆瓣网1.0链接

这章会涉及三个热门的模板引擎,以及如何用模板把显示层标记从逻辑代码中分离出来,保持 Web 程序代码的整洁性。

Web 程序的模板

用模板保持代码的整洁性

在 Node 中 可以像其他 Web 技术一样,用 MVC 模式开发传统的 Web 程序。主要的思想是将逻辑、数据和展示层分离。在遵循 MVC 模式的 Web 程序中,一般是用户向服务器请求资源,然后控制器向模型请求数据,得到数据后传到视图,再由视图以特定格式将数据呈现给用户。MVC 中的视图部分一般会用到某种模板语言。在使用模板时,视图会将模型返回的数据传递给模板引擎,并制定用那个模板文件展示这些数据。

MVC 程序的流程以及它跟模板层的交互

模板文件中通常包含数据的占位符、HTML、CSS,有时还会用一些客户端 JavaScript 来做第三方小部分显示,比如点赞按钮、或者触发界面行为,隐藏显示页面等等操作。因为模板文件的工作重点是展示而不是处理逻辑,所以前后端人员可以一起工作,有利于项目任务的分工。

模板实战

博客文章是从文本中读取的,格式如下。---表明一篇文章结束,另一篇文章开始。

1
2
3
4
5
6
7
title: It is someone birthday!
data: January 09,2098
I am getting old, but thankfully i am not in jail!
---
title: Movies are pretty good
data: January 2,2078
I have been watching a lot of movies lately.It is relaxing,except when they have clowns in them.

博客程序代码,读取博客文章

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// index.js
const fs = require('fs');
const http = require('http');
// 读取和解析博客文章的数据
function getEntries() {
const entries = [];
// 从文件中读取博客文章的数据
let entriesRaw = fs.readFileSync('./entries.txt', 'utf8');
entriesRaw = entriesRaw.split('---');
entriesRaw(entryRaw => {
const entry = {};
const lines = entryRaw.split('\n');
lines.map(line => {
if (line.indexOf('title: ') === 0) {
entry.title = line.replace('title: ', '');
} else if (line.indexOf('date: ') === 0) {
entry.date = line.replace('date: ', '');
} else {
entry.body = entry.body || '';
entry.body += line;
}
})
entries.push(entry);
});
return entries;
}
const entries = getEntries();
console.log(entries);

const server = http.createServer((req, res) => {
const output = blogPage(entries);
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(output);
});
server.listen(8000);

页面用函数 blogPage 函数渲染的,用它把文章渲染到 HTML 页面中,以便发送浏览器,有两种不同的尝试方式,用模板和不用模板。

不用模板

博客程序可以直接输出 HTML,但杂处理逻辑中引入 HTML 会让代码变得很乱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function blogPage(entries) {
let output = `
<html>
<head>
<style type="text/css">
.entry_title { font-weight: bold; }
.entry_date { font-style: italic; }
.entry_body { margin-bottom: 1em; }
</style>
</head>
<body>
</html>
`
entries.map(entry => {
output += `
<div class="entry_title">${entry.title}</div>
<div class="entry_date">${entry.date}</div>
<div class="entry_body">${entry.body}</div>
`
});
output += `</body></html>`;
return output;
}

可以看出来,逻辑中有很多 HTML 会显得很乱。

用模块渲染 HTML

模板可以把 HTML 从处理逻辑中挪走,大幅度提高代码的整洁性。

安装 Embedded JavaScript 模块演示:

1
npm install ejs

加载模板

1
2
3
4
5
6
7
const ejs = require('ejs');
const template = fs.readFileSync('./template/blog_page.ejs', 'utf8');

function blogPage() {
const values = { entries };
return ejs.render(template, values);
}

EJS 模板文件是由 HTML 标记和数据占位符构成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<html>

<head>
<style>
.entry_title {
font-weight: bold;
}

.entry_date {
font-style: italic;
}

.entry_body {
margin-bottom: 1em;
}
</style>
<body>
<% entries.map(entry =>{ %>
<div class="entry_title"><%= entry.title %></div>
<div class="entry_date"><%= entry.date %></div>
<div class="entry_body"><%= entry.body %></div>
<% }) %>
</body>
</head>

</html>

Node 社区创建了很多模板引擎,HTML 需要闭合标签,而 CSS 需要左右大括号,会有特殊的语言,例如 Pug 语言可以更简洁地表示 HTML 或者 CSS 。

本节会介绍三种模板引擎,以及在 Node 中如何取使用它们:

  • Embedded JavaScript (EJS)引擎
  • 极简的 Hogan 引擎
  • Pug 模板引擎

Embedded JavaScript 的模板

Embedded JavaScript 处理模板的方式简单直接,可以把 EJS 标签当做数据准备的占位符嵌入到 HTML 中,还可以在模板中执行纯 JavaScript 代码,就像 PHP 那样完成条件分支和循环之类的工作。

创建模板

在模板的世界中,发送给模板引擎做渲染的数据有时被称为上下文,而下面是 EJS 用一个简单的模板渲染上下文的例子:

1
2
3
4
const ejs = require('ejs');
const template = '<%= message %>'
const context = { message: 'Hello template!'}
console.log(ejs.render(template, context));

注意发送给 render 第二个参数中的 locals 的用法。第二个参数可以包含 EJS 选项以及上下文数据,而 locals 可以确保上下文数据不会被当做 EJS 选项。但大多数情况下你都可以把上下文本身当做第二个参数,例如下面render:

1
console.log(ejs.render(template,context));

如果把上下文直接当做 render 的第二个参数,一定不要给上下文中的值用这些名称:cache/client/close/compileDebug/debug/filename/open/scope 。它们是为修改模板引擎设定的保留字。

字符转义

在渲染时,EJS 会转义上下文值中的所有特殊字符,将它们替换为 HTML 实体码,这是为了防止跨站脚本(xss)攻击,恶意用户会将 JavaScript 作为数据提交给 Web 程序,希望其他用户访问包含这些数据 的页面时能在他们的浏览器中执行。下面的代码展示了 EJS 的转义处理。

1
2
3
4
const ejs = require('ejs');
const template = '<%= message %>'
const context = { message: "<script> alert('XSS attack);<script/>"}
console.log(ejs.render(template,context));

这段代码在显示时会输出下面这种代码:

1
&lt;script&gt;alert('XSS attack');&lt;/script&gt;

如果用在模板中的是可信数据,不想做转义处理,可以用 <%-代替 <%=

1
2
3
4
5
6
const ejs = require('ejs');
const template = '<%- message %>'
const context = {
message: "<script>alert('Trusted JavaScript!');</script>"
};
console.log(ejs.render(template, context));

注意!指明 EJS 标签的字符是可修改的,比如像这样:

1
2
3
4
5
const ejs = require('ejs');
ejs.delimiter = '$';
const template = '<$= message $>'
const context = { message: 'Hello template!'
console.log(ejs.render(template,context));

将 EJS 集成到你的程序中

把模板和代码放到同一个文件里很别扭,并且会显得代码很乱。接下来使用 Node API 从单独的文件中读取模板。

进入工作目录,创建 app.js 文件,把下面的代码放在里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// app.js
const ejs = require('ejs');
const fs = require('fs');
const http = require('http');
const filename = './template/students.ejs';
const students = [
{ name: 'Rick LaRue', age: 23 },
{ name: 'Sarah Cathands', age: 25 },
{ name: 'Bob Dobbs', age: 37 }
];

// 创建 HTTP 服务器
const server = http.createServer((req, res) => {
if (req.url === '/') {
// 从文件中读取模板
fs.readFile(filename, (err, data) => {
const template = data.toString();
const context = { students: students };
// 渲染模板
const output = ejs.render(template, context);
res.setHeader('Content-Type', 'text/html');
// 发送响应
res.end(output);
});
}else{
res.statusCode = 404;
res.end('Not Found');
}
});
server.listen(8000);

接下来创建存放模板文件的 students.ejs 文件。

1
2
3
4
5
6
7
<% if (students.length) { %>
<ul>
<% students.forEach(student =>{ %>
<li><%= student.name %> (<%= student.age %>)</li>
<% }) %>
</ul>
<% } %>
缓存 EJS 模板

如果有必要,可以让 EJS 把模板函数缓存在内存中。也就是说在解析完模板文件后,EJS 可以把解析得到的函数存下来。这样以后需要渲染这个模板时不用再次解析,所以渲染速度会快。

如果正在开发过程当中,想即时看到模板文件修改后的效果,则不要启用缓存。但在把程序
部署到生产环境中时,启用缓存是一种简单快捷的性能优化办法。可以通过环境变量 NODE_ENV判定是否启用缓存

1
2
3
4
5
const cache = process.env.NODE_ENV === 'production'
const output = ejs.render(
template,
{ students, cache, filename }
);

第二个参数的 filename 选项不仅限于文件,可以用要渲染的模板的唯一标识。接下来看看怎么在客户端使用 EJS。

在客户端程序中使用 EJS

要在客户端使用 EJS ,首先要先把 EJS 搜索引擎下载到工作目录下面

1
curl -O https://raw.githubusercontent.com/tj/ejs/master/lib/ejs.js

下载完就可以在客户端使用 EJS ,下面代码存为 index.html 并试着在浏览器打开运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<script src="./ejs.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8/jquery.js">
</script>
<title>EJS example</title>
</head>

<body>
<div id="output"></div>
</body>
<script>
const template = "<%= message %>";
const context = { message: 'Hello template!' };
$(document).ready(() => {
$('#output').html(
ejs.render(template, context)
)
});
</script>

</html>

Hogan 模板引擎,特意限制了模板代码中可用的功能。

使用 Mustache 模板语言与 Hogan

Hogan.js 是 Twitter 为满足自己的需求而创建的引擎模板。Hogan 是 Mustache 模板语言标准的具体实现。

跟 EJS 不用的是,Mustache 遵循极简主义,特意去掉了条件逻辑。在内容过滤上,Mustache 只为防止XSS 的攻击而保留了转义处理功能,主张模板代码应该尽可能地简单。

本节将介绍:

  • 如何在程序中创建和实现 Mustache 模板
  • Mustache 标准中的各种模板标签
  • 如何用子模板组织模板
  • 如何用定制的分隔符和其他选项对 Hogan 进行微调

创建模板

要使用 Hogan ,同样先安装

1
npm i -save hogan.js

下面是用 Hogan 使用简单模板渲染上下文的例子。运行后输出 ’Hello template‘

1
2
3
4
5
const hogan = require('hogan.js');
const templateSource = '{{message}}';
const context = { message: 'Hello Template' }
const template = hogan.compile(templateSource);
console.log(template.render(context));
1
node index.js

了解如何用 Hogan 处理 Mustache 模板后,下面看看它支持哪些标签

Mustache 标签

Mustache 标签在概念上跟 EJS 的标签类似,也有变量值的占位符,指明哪里需要循环。可以增强 Mustache 的功能,在模板里添加注释。

1.显示简单的值

在 Mustache 模板中,要把想要显示的上下文的名称放在大括号中。大括号在 Mustache 社区里被称为胡须。比如说,要显示上下文项 name 的值,应该使用 Hogan 标签

1
{{name}}

跟大多数模板一样,Hogan 默认也会对内容进行转义以防止XSS 攻击,如果想要在 Hogan 中显示未转义的值,既可以把上下文的名称放在第三条胡须中,也可以在前面添加一个 & 符号。

例如

1
{{&name}}

如果想在模板中加注释,可以这样

1
{{! This is a comment}}
2.区块:多个值的循环遍历

尽管 Hogan 不允许在模板中使用逻辑,但是它确实引入了一种更加优雅的方法,用 Mustache 区块对上下文项中多个值做循环遍历:

1
2
3
4
5
6
const context = {
students:[
{ name: 'J', age:'23'},
{ name: 'L', age:'53'}
]
}

如果要创建一个模板,让每个学生都显示在单独的 HTML 段落中,可以像下面这样轻松实现:

1
2
<p>Name: J,Age: 23 years old </p>
<p>Name: L,Age: 53 years old </p>

下面这个模板能生成上面的 HTML :

1
2
3
{{# students}}
<p>Name: {{name}}, Age: {{age}} years old </p>
{{/students}}
3.反向区块:值不存在时的默认 HTML

如果上下文数据中的 students 不是数组会怎么样?比如说,如果只是单个对象,那么模板互显示这个对象,但是如果是 undefined 或 false ,空数组,则什么都不显示。

如果想输出消息指明该区块的值不存在,那么可以用 Mustache 的反向区块。把下面的快模板代码加到前面那个显示学生消息的模板中,上下文中没有数据时就会显示这条消息:

1
2
3
{{^ students}}
<p>No students found</p>
{{/ students}}
4.区块 lambda: 区块内的定制功能

如果 Mustache 现有功能无法满足,可以按照它的标准定制区块标签。让它调用函数处理模块内容,不用循环遍历数组。这被称为 区块 lambda 。

下面的例子用到了 github-flavored markdown 模块:

1
npm install github-flavored-markdown --dev

在下面这段代码中,模板中的Name传给由区块 lambda 调用的 Markdown 解析器,生成
Name

1
2
3
4
5
6
7
8
9
10
11
12
const hogan = require('hogan.js');
// 引入 Markdown 解析器
const md = require('github-flavored-markdown');
// Mustache 模板也包含 Markdown 格式的内容
const templateSource = `{{#markdown}}**Name**:{{name}}{{/markdown}}`;
// 模板的上下文包含了一个解析 Markdown 的区块 lambda
const context = {
name: 'Rick LaRue',
markdown: () => text => md.parse(text)
}
const template = hogan.compile(templateSource);
console.log(template.render(context));

使用区块 lambda 可以在模板中轻松实现缓存和转换机制等功能。

5.子模板:在其他模板中重用模板

为了避免多个模板中复制粘贴相同的代码,可以将这些通用的代码做成子模板(partial)。
子模板是放在其他模板内的构件,可以把复杂的模板分解成简单模板。

比如下面这个例子,将显示学生数据的代码从主模板中分离出来做成了子模板。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const hogan = require('hogan.js');
// 用于子模板的代码
const studentTemplate = `
<p>
Name: {{ name }},
Age: {{ age }} years old
</p>
`
// 主模板
const mainTemplate = `
{{# students}}
{{>student}}
{{/ students}}
`
const context = {
students: [{
name: 'Jane Narwhal',
age: 21
}, {
name: 'Rick LaRue',
age: 26
}]
}
// 编译主模板和子模板
const template = hogan.compile(mainTemplate);
const partial = hogan.compile(studentTemplate);
// 渲染主模板和子模板
const html = template.render(context, { student: partial });
console.log(html);

微调 Hogan

Hogan 用起来相当简单,掌握它的标签汇总表就够了。在使用时可能只有一两个地方需要调整一下.

如果你不喜欢 Mustache 风格的大括号,可以给 compile 方法传入一个参数覆盖 Hogan 所用的分隔符。下面的例子把 EJS 风格的分隔符编译在 Hogan 中:

1
hogan.compile(text, { delimiters: '<% %>' });

除了 Mustache,还有其他模板语言。比如想把 HTML 的噪声都去掉的 Pug。

用 Pug 做模板

Pug,以前叫做 Jade,它用另一种方式来表示 HTML,是 Express 的默认模板引擎。Pug 和其他主流模板系统的差别主要在于它对空格的使用。Pug 模板用缩进表示 HTML 标签的嵌入关系。HTML 标签也不必明确给出关闭标签,从而避免了因为过早关闭,或根本就不关闭标签所产生的问题。由于有严格的缩进规则,因此 Pug 模板看起来很简洁,更易于维护。

1
2
3
4
5
6
7
8
9
10
<html> 
<head>
<title>Welcome</title>
</head>
<body>
<div id="main" class="content">
<strong>"Hello world!"</strong>
</div>
</body>
</html>

这段 HTML 对应的 Pug 模板如下:

1
2
3
4
5
6
html
head
title Welcome
body
div.content#main
strong "Hello world!"

Pug 像 EJS 一样,可以嵌入 JavaScript ,可以用在服务端和客户端。但 Pug 还有其他特性,比如模板继承和 mixins。用 mixins 可以定义易于重用的小型模板,用来表示常用的视觉元素的 HTML ,比如条目列表和盒子。Mixins 很像上一节介绍的 Hogan 的自末班。有了模板继承,那些把一个 HTML 页面渲染到多个文件中 Pug 模板组织起来就更容易了。先安装:

1
npm install pug -S

本节介绍:

  • Pug 的基础知识,比如说类名、属性和块扩展
  • 如何用内置的关键字往 Pug 模板里添加逻辑
  • 如何用继承、块和mixins 组织模板

Pug 基础知识

Pug 的标签和 HTML 一样,但是抛弃了前后的字符,并用缩进表示标签的嵌套关系。标签可以用 .<classname>关联一个或者是多个 CSS 类。比如应用了 content 和 sidebar 类的 div 元素表示为:

1
div.content.sidebar

在标签上添加 #<ID>可以赋予它 CSS 的 ID 。比如上面那个再加上一个 ID

1
div.content.sidebar#featured_content
1.指定标签的属性

将标签的属性放在括号中,每个属性之间用括号分开,下面的Pug 表示一个 会在新的浏览器打开的标签:

1
a(href='http://laibh.top',target='_blank')

因为带属性的标签可能会很长,所以 Pug 在处理这样的标签的时候会比较灵活。比如下面的这种表示跟那个效果是一样的:

1
2
a(href='http://laibh.top',
target='_blank')

也可以指定不需要值的属性,下面的这段 Pug 实例是一个 HTML 表单,其中包含一个 select 元素,也有预定的 option :

1
2
3
4
5
6
strong Select your favorite food:
form
select
option(value='Cheese') Cheese
option(value='Tofu') Tofu
Specifying tag content

在前面那段代码中还有标签内容的示例:strong 标签后面的 Select your favorite food:;第一个 option 后面的 Cheese;第二个 option 后面的 Tofu。

这是 Pug 中指定标签内容的常用方法,但不是唯一的。尽管这种风格在指定的比较短的内容时很好用,但如果标签的内容很长,则会导致 Pug 模板中出现超长的代码行。不过。就想下面这个例子,可以用 | 指定标签的内容

1
2
3
4
textarea 
| This is a some default text
| that the user should be
| provided with.

如果 HTML 标签只接受文本(即不能嵌入 HTML 元素),比如 style 会让 script 。则可以去掉 | 字符,像下面这样:

1
2
3
4
5
style.
h1{
font-size: 6em;
color: #9d9;
}

用这两种方法别分可以表示长短两种内容,可以让 Pug 模板保持有呀,Pug 还有一种表示嵌套关系的办法,叫做扩展。

2.用块扩展把他组织好

Pug 一般用 缩进表示嵌套,但有时缩进形成的空格太多了。比如下面的这个用缩进定义链接列表的 Pug 模板:

1
2
3
4
5
6
7
ul 
li
a(href='http://nodejs.org/') Node.js homepage
li
a(href='http://npmjs.org/') NPM homepage
li
a(href='http://nodebits.org/') Nodebits blog

如果用 Pug 块扩展,这个例子会更加紧凑。块扩展可以在标签后面用冒号表示嵌套。例如下面这段代码:

1
2
3
4
ul 
li: a a(href='http://nodejs.org/') Node.js homepage
li: a(href='http://npmjs.org/') NPM homepage
li: a(href='http://nodebits.org/') Nodebits blog
3.将数据纳入到 Pug 模板中

数据传入到 Pug 引擎的方式跟 EJS 一样,模板先被编译成函数,然后带着上下文调用它,以便于渲染输出 HTML 。如下例:

1
2
3
4
5
const pug = require('pug');
const template = 'strong #{message}'
const context = { message: 'Hello template' };
const fn = pug.compile(template);
console.log(fn(context));

这个模板中的 #{message}是要被上下文值替换掉的占位符。

上下文值也可以作为属性的值。这个例子会渲染出来

1
<a href="http://laibh.top"> </a>
1
2
3
4
5
const pug = require('pug');
const template = 'a(herf=url)'
const context = { url: 'http://laibh.top' };
const fn = pug.compile(template);
console.log(fn(context));

Pug 模板中的逻辑

把数据交给模板后,还需要定义处理数据的逻辑,可以直接在模板中嵌入 JavaScript 代码来定义数据处理逻辑。例如 if 、for 、var 声明等等。下面有个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
h3.contact-header My Contacts
if contacts.length
each contact in contacts
- var fullName = contact.firstName + ' ' + contact.lastName
.contact-box
p fullName
if contact.isEditable
p: a(href='/edit/+contact.id) Edit Record
p
case contact.status
when 'Active'
strong User is active in the system
when 'Inactive'
em User is inactive
when 'Pending'
| User has a pending invitation
else
p You currently do not have any contacts

下面来看一下 Pug 模板中嵌入 javascript 代码如何处理输出

1.在Pug模板中使用 JavaScript

带有 -前缀的 JavaScript 代码的返回结果不会出现在渲染结果中。带有 =前缀的 JavaScript 代码的执行结果会出现。但为了防止 XSS 攻击做了转义处理。如果 JavaScript 代码生成的内容不应该转义。那么可以使用前缀 !=.

在 Pug 中,有些常用的 条件判断和循环语句可以不带前缀:if/else/case/when/default/until/each和unless

Pug 中还可以定义变量,下面两种赋值方式效果是一样的:

1
2
-count = 0
count = 0

没有前缀的语句没有输出。

2.循环遍历对象和数组

Pug 中的 JavaScript 可以访问上下文中的值。在下面的例子中,我们会读取一个 Pug 模板,并让它显示一个包含两个消息的上下文的数组:

1
2
3
4
5
6
7
8
9
10
11
const pug = require('pug');
const fs = require('fs');
const template = fs.readFileSync('./template.pug');
const context = {
messages: [
'You have logged in successfully.',
'Welcome back!'
]
};
const fn = pug.compile(template);
console.log(fn(context));

Pug 模板的内容

1
2
3
- messages.forEach( message =>{
p= message
- })

运行代码:

1
node index.js

输出这样的结果:

1
<p>You have logged in successfully.</p><p>Welcome back!</p>

Pug 中还有一种非 JavaScript 形式的循环:each 语句。用 each 语句很容易实现数组和对象属性的循环遍历.

1
2
each message in messages
p= message

对象属性的循环遍历可以稍有不同,例如这样:

1
2
3
4
each value, key in post
div
strong #{key}
p value
3.条件化渲染的模板代码

有时候要根据数据的取值来决定显示那些模板,下面是一个条件判断的例子。几乎有一半的可能会输出 script 标签:

1
2
3
4
5
-n = Math.round(Math.random() * 1) + 1;
-if (n === 1){
script
alert('You win');
- }

条件判断在 Pug 中还有中更简洁的写法:

1
2
3
4
-n = Math.round(Math.random()* 1) + 1 ;
if n === 1
script
alert('You win!');

如果条件判断是取反的,比如 if (n!=1),可以用 Pug 的 unless 关键字:

1
2
3
4
-n Math.round(Math.random() *1) + 1;
unless n === 1
script
alert('You win!');
4.在 Pug 中使用 case 语句

Pug 中还有类似 switch 的 非 JavaScript 条件判断:case 语句。case 语句可以根据模板的场景指定输出。

在下面的例子中,我们用 case 语句以三种不同的方式显示博客的搜索结果。如果没有结果,则显示一条提示消息。如果找到一篇文章,则显示它的详细内容。如果很多篇则用 each 语句循环遍历所有文章,显示它们的标题:

1
2
3
4
5
6
7
8
case results.length
when 0
p No results found
when 1
p= results[0].content
default
each result in results
p= result.title

组织 Pug 模板

模板定义好了之后,要知道该如何组织。跟程序逻辑一样,你肯定也不想让模板文件过大,一个模板应该对应一个构件:一个页面,一个边栏或者是一篇博客文章的内容;

下面会介绍几种机制,让几个不同模板文件一起渲染内容:

  • 用模板继承组织多个模板文件
  • 用块前缀/追加实现布局
  • 模板包含
  • 借助 mixins 重用模板逻辑
1.用模板继承组织多个模板文件

模板继承是多个模板文件的结构化处理方法之一。从概念上讲,模板就像面向对象编程中的类。一个模板可以有扩展另一个,然后这个再扩展另一个,只要合理,使用多少层都可以。

下面有个例子,用模板继承提供一个简单的 HTML 包装器,可以用来包装 页面内容。创建存放 Pug文件的 templates目录。在其中创建模板文件 layout.pug

1
2
3
4
5
html
head
block title
body
block content

layout.pug 中有 HTML 页面的基本定义和两个模板块。模板块可以由后裔模板提供内容的占位符。layout.pug 的后裔模板可以在 title 模板块的位置设置页面标题,在 content 模板块的位置设定页面的内容要显示什么。

接下来创建 page.pug。这个模板会提供 title 和 content 块的具体内容:

1
2
3
4
5
html
head
block title
body
block content

继承的实际用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const pug = require('pug');
const fs = require('fs');
const templateFile = './templates/page.pug'
const iterTemplate = fs.readFileSync(templateFile);
const context = {
messages: [
'You have logged in successfully.',
'Welcome back!'
]
}
const iterFn = pug.compile(
iterTemplate,
{ filename: templateFile }
)
console.log(iterFn(context));
2.用块前缀/块追加实现布局

在前面那个例子中,layout.pug 中的模板没有内容,因此在 page.pug 模板中设定内容简答直接。如果被继承的模板中没有内容,也可以用块前缀和块追加,在原有内容基础上构建新内容而不是替换它。

下面的 layout.pug 模板中加了一个模板块 scripts ,其中有加载 jq 的 script 标签

1
2
3
4
5
6
7
8
html
head
- const baseUrl = "http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/"
block title
block style
block scripts
body
block content
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
extends layout.pug

block title
title Messages

block style
link(rel="stylesheet", href=baseUrl+"themes/flick/jquery-ui.css")

block scripts
script(src=baseUrl+"jquery-ui.js")

block content
- count = 0
each message in messages
- count = count + 1
script.
$(() => {
$("#message_#{count}").dialog({
height: 140,
modal: true
});
});
!= '<div id="message_' + count + '">' + message + '</div>'

但模板继承不是唯一一种集成多个模板的办法,还可以使用 include Pug 命令。

3.模板包含

Pug 中的 include 命令式另一个组织模板的工具。这个命令会引入另一个模板中内容。

如果在前面那个 layout.pug 中加入一个 include footer 就会引入这个模板

1
2
3
4
5
6
7
8
9
html
head
- const baseUrl = "http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/"
block title
block style
block scripts
body
block content
include footer

可以 用 include 往 layout.pug 中添加关于网站的消息或设计元素。也可以指定文件的扩展名,包含 非 Pug 文件(比如 include xxx.html)

4.借助 mixin 重用模板逻辑

尽管 Pug 的 include 命令能帮我们引入之前创建的 代码块,但要构建在程序和不同页面之间共享的可重用功能库时,它就帮不上忙了。Pug 为此专门提供了 mixin 命令,可以用来定制可重用的 Pug 代码块。

mixin 模拟的是 JavaScript 函数。它跟函数一样,可以带参数,并且这些参数可以用来生成 Pug 代码。

比如说要处理下面这种数据结构:

1
2
3
4
5
const students = [
{ name: 'Rick LaRue', age: 23 },
{ name: 'Sarah Cathands', age: 25 },
{ name: 'Bob Dobbs', age: 37 }
]

如果要把对象中提取出来的属性输出到 HTML 列表中,那么可以像下面这样定义一个 mixin:

1
2
3
4
mixin list_object_property(objects, property)
ul
each object in objects
li = object[property]

然后就可以像下面这样借助 mixin显示这些数据了

1
mixin list_object_property(students,'name');

借助模板继承、include 语句和 mixin,你可以轻松地重用展示标记,防止模板文件变得过于冗长

总结

  • 模板引擎可以把程序逻辑和展示组织好
  • EJS、Hogan.js 和 Pug 都是 Node 中比较流行的模板引擎。
  • EJS 支持简单的流程控制,以及转义或非转义插值。
  • Hogan.js 是简单的模板引擎,不支持流程控制,但支持 Mustache 标准
  • Pug 是比较复杂的模板语言,可以输出 HTML,但不用尖括号。
  • Pug 用空格表示标签的嵌套关系
0%