读 《node.js实战2.0》,进行学习记录总结。
这章会涉及三个热门的模板引擎,以及如何用模板把显示层标记从逻辑代码中分离出来,保持 Web 程序代码的整洁性。
Web 程序的模板
用模板保持代码的整洁性
在 Node 中 可以像其他 Web 技术一样,用 MVC 模式开发传统的 Web 程序。主要的思想是将逻辑、数据和展示层分离。在遵循 MVC 模式的 Web 程序中,一般是用户向服务器请求资源,然后控制器向模型请求数据,得到数据后传到视图,再由视图以特定格式将数据呈现给用户。MVC 中的视图部分一般会用到某种模板语言。在使用模板时,视图会将模型返回的数据传递给模板引擎,并制定用那个模板文件展示这些数据。
模板文件中通常包含数据的占位符、HTML、CSS,有时还会用一些客户端 JavaScript 来做第三方小部分显示,比如点赞按钮、或者触发界面行为,隐藏显示页面等等操作。因为模板文件的工作重点是展示而不是处理逻辑,所以前后端人员可以一起工作,有利于项目任务的分工。
模板实战
博客文章是从文本中读取的,格式如下。---
表明一篇文章结束,另一篇文章开始。
1 | title: It is someone birthday! |
博客程序代码,读取博客文章
1 | // index.js |
页面用函数 blogPage 函数渲染的,用它把文章渲染到 HTML 页面中,以便发送浏览器,有两种不同的尝试方式,用模板和不用模板。
不用模板
博客程序可以直接输出 HTML,但杂处理逻辑中引入 HTML 会让代码变得很乱
1 | function blogPage(entries) { |
可以看出来,逻辑中有很多 HTML 会显得很乱。
用模块渲染 HTML
模板可以把 HTML 从处理逻辑中挪走,大幅度提高代码的整洁性。
安装 Embedded JavaScript 模块演示:
1 | npm install ejs |
加载模板
1 | const ejs = require('ejs'); |
EJS 模板文件是由 HTML 标记和数据占位符构成的。
1 | <html> |
Node 社区创建了很多模板引擎,HTML 需要闭合标签,而 CSS 需要左右大括号,会有特殊的语言,例如 Pug 语言可以更简洁地表示 HTML 或者 CSS 。
本节会介绍三种模板引擎,以及在 Node 中如何取使用它们:
- Embedded JavaScript (EJS)引擎
- 极简的 Hogan 引擎
- Pug 模板引擎
Embedded JavaScript 的模板
Embedded JavaScript 处理模板的方式简单直接,可以把 EJS 标签当做数据准备的占位符嵌入到 HTML 中,还可以在模板中执行纯 JavaScript 代码,就像 PHP 那样完成条件分支和循环之类的工作。
创建模板
在模板的世界中,发送给模板引擎做渲染的数据有时被称为上下文,而下面是 EJS 用一个简单的模板渲染上下文的例子:
1 | const ejs = require('ejs'); |
注意发送给 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 | const ejs = require('ejs'); |
这段代码在显示时会输出下面这种代码:
1 | <script>alert('XSS attack');</script> |
如果用在模板中的是可信数据,不想做转义处理,可以用 <%-
代替 <%=
:
1 | const ejs = require('ejs'); |
注意!指明 EJS 标签的字符是可修改的,比如像这样:
1 | const ejs = require('ejs'); |
将 EJS 集成到你的程序中
把模板和代码放到同一个文件里很别扭,并且会显得代码很乱。接下来使用 Node API 从单独的文件中读取模板。
进入工作目录,创建 app.js 文件,把下面的代码放在里面。
1 | // app.js |
接下来创建存放模板文件的 students.ejs 文件。
1 | <% if (students.length) { %> |
缓存 EJS 模板
如果有必要,可以让 EJS 把模板函数缓存在内存中。也就是说在解析完模板文件后,EJS 可以把解析得到的函数存下来。这样以后需要渲染这个模板时不用再次解析,所以渲染速度会快。
如果正在开发过程当中,想即时看到模板文件修改后的效果,则不要启用缓存。但在把程序
部署到生产环境中时,启用缓存是一种简单快捷的性能优化办法。可以通过环境变量 NODE_ENV判定是否启用缓存
1 | const cache = process.env.NODE_ENV === 'production' |
第二个参数的 filename 选项不仅限于文件,可以用要渲染的模板的唯一标识。接下来看看怎么在客户端使用 EJS。
在客户端程序中使用 EJS
要在客户端使用 EJS ,首先要先把 EJS 搜索引擎下载到工作目录下面
1 | curl -O https://raw.githubusercontent.com/tj/ejs/master/lib/ejs.js |
下载完就可以在客户端使用 EJS ,下面代码存为 index.html 并试着在浏览器打开运行
1 |
|
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 | const hogan = require('hogan.js'); |
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 | const context = { |
如果要创建一个模板,让每个学生都显示在单独的 HTML 段落中,可以像下面这样轻松实现:
1 | <p>Name: J,Age: 23 years old </p> |
下面这个模板能生成上面的 HTML :
1 | {{# students}} |
3.反向区块:值不存在时的默认 HTML
如果上下文数据中的 students 不是数组会怎么样?比如说,如果只是单个对象,那么模板互显示这个对象,但是如果是 undefined 或 false ,空数组,则什么都不显示。
如果想输出消息指明该区块的值不存在,那么可以用 Mustache 的反向区块。把下面的快模板代码加到前面那个显示学生消息的模板中,上下文中没有数据时就会显示这条消息:
1 | {{^ students}} |
4.区块 lambda: 区块内的定制功能
如果 Mustache 现有功能无法满足,可以按照它的标准定制区块标签。让它调用函数处理模块内容,不用循环遍历数组。这被称为 区块 lambda 。
下面的例子用到了 github-flavored markdown 模块:
1 | npm install github-flavored-markdown --dev |
在下面这段代码中,模板中的Name传给由区块 lambda 调用的 Markdown 解析器,生成
了Name。
1 | const hogan = require('hogan.js'); |
使用区块 lambda 可以在模板中轻松实现缓存和转换机制等功能。
5.子模板:在其他模板中重用模板
为了避免多个模板中复制粘贴相同的代码,可以将这些通用的代码做成子模板(partial)。
子模板是放在其他模板内的构件,可以把复杂的模板分解成简单模板。
比如下面这个例子,将显示学生数据的代码从主模板中分离出来做成了子模板。
1 | const hogan = require('hogan.js'); |
微调 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 | <html> |
这段 HTML 对应的 Pug 模板如下:
1 | html |
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 | a(href='http://laibh.top', |
也可以指定不需要值的属性,下面的这段 Pug 实例是一个 HTML 表单,其中包含一个 select 元素,也有预定的 option :
1 | strong Select your favorite food: |
在前面那段代码中还有标签内容的示例:strong 标签后面的 Select your favorite food:;第一个 option 后面的 Cheese;第二个 option 后面的 Tofu。
这是 Pug 中指定标签内容的常用方法,但不是唯一的。尽管这种风格在指定的比较短的内容时很好用,但如果标签的内容很长,则会导致 Pug 模板中出现超长的代码行。不过。就想下面这个例子,可以用 | 指定标签的内容
1 | textarea |
如果 HTML 标签只接受文本(即不能嵌入 HTML 元素),比如 style 会让 script 。则可以去掉 | 字符,像下面这样:
1 | style. |
用这两种方法别分可以表示长短两种内容,可以让 Pug 模板保持有呀,Pug 还有一种表示嵌套关系的办法,叫做扩展。
2.用块扩展把他组织好
Pug 一般用 缩进表示嵌套,但有时缩进形成的空格太多了。比如下面的这个用缩进定义链接列表的 Pug 模板:
1 | ul |
如果用 Pug 块扩展,这个例子会更加紧凑。块扩展可以在标签后面用冒号表示嵌套。例如下面这段代码:
1 | ul |
3.将数据纳入到 Pug 模板中
数据传入到 Pug 引擎的方式跟 EJS 一样,模板先被编译成函数,然后带着上下文调用它,以便于渲染输出 HTML 。如下例:
1 | const pug = require('pug'); |
这个模板中的 #{message}是要被上下文值替换掉的占位符。
上下文值也可以作为属性的值。这个例子会渲染出来
1 | <a href="http://laibh.top"> </a> |
1 | const pug = require('pug'); |
Pug 模板中的逻辑
把数据交给模板后,还需要定义处理数据的逻辑,可以直接在模板中嵌入 JavaScript 代码来定义数据处理逻辑。例如 if 、for 、var 声明等等。下面有个例子
1 | h3.contact-header My Contacts |
下面来看一下 Pug 模板中嵌入 javascript 代码如何处理输出
1.在Pug模板中使用 JavaScript
带有 -
前缀的 JavaScript 代码的返回结果不会出现在渲染结果中。带有 =
前缀的 JavaScript 代码的执行结果会出现。但为了防止 XSS 攻击做了转义处理。如果 JavaScript 代码生成的内容不应该转义。那么可以使用前缀 !=
.
在 Pug 中,有些常用的 条件判断和循环语句可以不带前缀:if/else/case/when/default/until/each和unless
Pug 中还可以定义变量,下面两种赋值方式效果是一样的:
1 | -count = 0 |
没有前缀的语句没有输出。
2.循环遍历对象和数组
Pug 中的 JavaScript 可以访问上下文中的值。在下面的例子中,我们会读取一个 Pug 模板,并让它显示一个包含两个消息的上下文的数组:
1 | const pug = require('pug'); |
Pug 模板的内容
1 | - messages.forEach( message =>{ |
运行代码:
1 | node index.js |
输出这样的结果:
1 | <p>You have logged in successfully.</p><p>Welcome back!</p> |
Pug 中还有一种非 JavaScript 形式的循环:each 语句。用 each 语句很容易实现数组和对象属性的循环遍历.
1 | each message in messages |
对象属性的循环遍历可以稍有不同,例如这样:
1 | each value, key in post |
3.条件化渲染的模板代码
有时候要根据数据的取值来决定显示那些模板,下面是一个条件判断的例子。几乎有一半的可能会输出 script 标签:
1 | -n = Math.round(Math.random() * 1) + 1; |
条件判断在 Pug 中还有中更简洁的写法:
1 | -n = Math.round(Math.random()* 1) + 1 ; |
如果条件判断是取反的,比如 if (n!=1),可以用 Pug 的 unless 关键字:
1 | -n Math.round(Math.random() *1) + 1; |
4.在 Pug 中使用 case 语句
Pug 中还有类似 switch 的 非 JavaScript 条件判断:case 语句。case 语句可以根据模板的场景指定输出。
在下面的例子中,我们用 case 语句以三种不同的方式显示博客的搜索结果。如果没有结果,则显示一条提示消息。如果找到一篇文章,则显示它的详细内容。如果很多篇则用 each 语句循环遍历所有文章,显示它们的标题:
1 | case results.length |
组织 Pug 模板
模板定义好了之后,要知道该如何组织。跟程序逻辑一样,你肯定也不想让模板文件过大,一个模板应该对应一个构件:一个页面,一个边栏或者是一篇博客文章的内容;
下面会介绍几种机制,让几个不同模板文件一起渲染内容:
- 用模板继承组织多个模板文件
- 用块前缀/追加实现布局
- 模板包含
- 借助 mixins 重用模板逻辑
1.用模板继承组织多个模板文件
模板继承是多个模板文件的结构化处理方法之一。从概念上讲,模板就像面向对象编程中的类。一个模板可以有扩展另一个,然后这个再扩展另一个,只要合理,使用多少层都可以。
下面有个例子,用模板继承提供一个简单的 HTML 包装器,可以用来包装 页面内容。创建存放 Pug文件的 templates目录。在其中创建模板文件 layout.pug
1 | html |
layout.pug 中有 HTML 页面的基本定义和两个模板块。模板块可以由后裔模板提供内容的占位符。layout.pug 的后裔模板可以在 title 模板块的位置设置页面标题,在 content 模板块的位置设定页面的内容要显示什么。
接下来创建 page.pug。这个模板会提供 title 和 content 块的具体内容:
1 | html |
继承的实际用法:
1 | const pug = require('pug'); |
2.用块前缀/块追加实现布局
在前面那个例子中,layout.pug 中的模板没有内容,因此在 page.pug 模板中设定内容简答直接。如果被继承的模板中没有内容,也可以用块前缀和块追加,在原有内容基础上构建新内容而不是替换它。
下面的 layout.pug 模板中加了一个模板块 scripts ,其中有加载 jq 的 script 标签
1 | html |
1 | extends layout.pug |
但模板继承不是唯一一种集成多个模板的办法,还可以使用 include Pug 命令。
3.模板包含
Pug 中的 include 命令式另一个组织模板的工具。这个命令会引入另一个模板中内容。
如果在前面那个 layout.pug 中加入一个 include footer 就会引入这个模板
1 | html |
可以 用 include 往 layout.pug 中添加关于网站的消息或设计元素。也可以指定文件的扩展名,包含 非 Pug 文件(比如 include xxx.html)
4.借助 mixin 重用模板逻辑
尽管 Pug 的 include 命令能帮我们引入之前创建的 代码块,但要构建在程序和不同页面之间共享的可重用功能库时,它就帮不上忙了。Pug 为此专门提供了 mixin 命令,可以用来定制可重用的 Pug 代码块。
mixin 模拟的是 JavaScript 函数。它跟函数一样,可以带参数,并且这些参数可以用来生成 Pug 代码。
比如说要处理下面这种数据结构:
1 | const students = [ |
如果要把对象中提取出来的属性输出到 HTML 列表中,那么可以像下面这样定义一个 mixin:
1 | mixin list_object_property(objects, property) |
然后就可以像下面这样借助 mixin显示这些数据了
1 | mixin list_object_property(students,'name'); |
借助模板继承、include 语句和 mixin,你可以轻松地重用展示标记,防止模板文件变得过于冗长
总结
- 模板引擎可以把程序逻辑和展示组织好
- EJS、Hogan.js 和 Pug 都是 Node 中比较流行的模板引擎。
- EJS 支持简单的流程控制,以及转义或非转义插值。
- Hogan.js 是简单的模板引擎,不支持流程控制,但支持 Mustache 标准
- Pug 是比较复杂的模板语言,可以输出 HTML,但不用尖括号。
- Pug 用空格表示标签的嵌套关系