上一讲说到了eleventy支持模板,而模板文件其实就是模板化的HTML文档。有点类似于JSP或者Razor page。eleventy作为一个框架,其实支持很多种模板引擎,只不过最常见的是一种叫Nunjucks的模板(引擎)。这个模板语言是由Mozilla搞出来的,被广泛应用在Firefox的应用市场和Mozilla的一些产品上,比如Mozilla Webmarker。
不过说实话,不是很流行,是一个比较偏门的模板语言,所幸呢,这个模板语言的语法还是比较简单的,只需要花几个小时就可以完全掌握。。这玩意毕竟不是编程语言,难度有限。今天这一讲主要过一下这个模板语言,或者说模板引擎支持的语法。
在具体说语法之前,提一下,作为一种,虽然很简单,但至少也算是个“独立”的模板语言,nunjucks是有自己的编辑器扩展插件的,visual studio的扩展插件叫vscode-nunjucks
,应用市场搜就能搜出来,装上之后能提升编码效率。prettier也有对应的插件可以使用。
总之怎么说呢,给人一种:你说它小众吧,该有的都有,你说他流行吧,确实之前没怎么听说过的感觉。
nunjucks是一门独立的模板语言,它可以脱离开eleventy框架来单独使用,有两种使用方式
项目添加依赖npm install nunjucks
,然后下面是一段示例代码:
njk = require("nunjucks");
njk.configure({autoescape: true});
renderResult = njk.renderString('Hello {{ username }}', {username: "James"});
console.log(renderResult);
运行结果是"Hello James"
。上面的代码是使用了renderString
这个API来使用nunjucks引擎的,不过鉴于我们的目的只是"了解nunjucks模板语言的语法,从而在eleventy框架中使用它",我们就不过多涉及这部分的内容了。有兴趣的可以去官方文档的API页面去查手册,手册也很简短,几个小时就能摸清楚。
不过,虽然直接使用nunjucks引擎不是我们的重点,但还是有一点点知识是需要了解的,看下面的代码例子:
njk = require("nunjucks");
njk.configure({autoescape: true});
let template = njk.compile('<h1>hello {{ username }}</h1>');
let context = {username: "Bond"};
renderResult = template.render(context);
console.log(renderResult);
这个例子将nunjuck引擎的工作流程展示了出来:首先,用户编写的njk
文件内容会被送到一个神秘的地方去处理,然后生一个所谓的“编译结果”:一个JS对象。也就是上面代码中的template
变量所引用的对象。
template
是结构化的模板数据,而要获得渲染结果,我们还需要提供一个“上下文”:去告诉引擎,模板中引用的变量的值都是什么,也就是上面代码中的context
变量所引用的对象。
最终,template
和context
通过方法render
结合在一起,给出我们一个渲染结果。
(说实话,我真的有点讨厌前端领域在滥用“编译”这个术语,这问题不光出现在中文前端领域,其实在英文前端领域,大家也在滥用"compile"这个词)
简单,HTML文档中引用https://mozilla.github.io/nunjucks/files/nunjucks.js
就可以,然后你就发挥想象力,为所欲为就行。不过,在浏览器环境中使用nunjucks的话,请:
nunjucks的引擎实现是比较简单的,没有做什么所谓的沙盒运行时,也没有做过多的安全检查,引擎的工作方式非常简单直接:你给我*.njk
文件,我给你吐出*.html
文件。所以,它天然是不能防注入攻击的。谨记这一点,不要瞎搞。
你可以在双大括号中输入几乎任何合法的JS表达式,然后它的值会被求作渲染结果。如果值是一个对象的话,那么tostring()
就会被作为渲染结果。如下所示
{{ user }}
{{ user.firstName }}
{{ user["secondName"]}}
{{ "Mr." + user["secondName"] }}
另外,如果context中定义了函数的话,函数调用也是符合直觉的表达式
{{ foo(1, 2) }}
这些都是JS中的内容,可以简单的总结为:几乎所有符合直觉的JS表达式都可以写在{{}}
中。而我这里说的是几乎所有,是因为有一种表达式nunjucks不支持:三元运算符写出来的表达式,比如:{{ foo == bar ? "foo equals to bar" : "foo not equals to bar"}}
。作为补充,nunjucks有自己的一种特殊的三元运算符表达式写法,这里先不提,我们后面会说到。
然而nunjucks有一种特殊的“函数”,它叫“过滤器”,filter。它的调用语法有两种:一种是简易写法,一种是tag写法。后一种写法我们会在后面介绍,这里先介绍使用最广泛的简易写法
|
将表达式的值传递给过滤器函数如下:
{{ foo | replace("foo", "bar") | capitalize }}
上面就是管道风格的过滤器函数调用,但有些filter比较特殊,比如上面的replace
:除了管道传递过来的值外,还接受额外的参数,额外的参数就是以括号形式正常传递的。
Tag是nunjucks模板的核心功能,它实现了程序设计语言中的分支判断、循环、跳转和一些其它功能。整体上,Tag的语法满足以下的形式:
{% tag xxx %}
...
...
...
{% endtag %}
一般来说,引擎是将“模板”和“context”这两个东西分开的,set
这个tag就可以在模板中就地“创建”一个变量,你更应当将它理解为就地给context
中的变量进行覆盖或者补充
{% set username = "joe" %}
{{ username }}
还有一点需要注意,在一些特殊的tag体内使用set
的话,这个set
的作用域一般也被限定在那个上级tag的体内。这些特殊的tag有两个:include
和marco
,先记得这个知识点,后面我们会介绍到这两个特殊的tag。
另外还可以将另外一个文件的内容整体读出来赋给一个变量
非常直观,没什么可多说的
{% if variable %}
It is true
{% endif %}
{% if hungry %}
I am hungry
{% elif tired %}
I am tired
{% else %}
I am good!
{% endif %}
{% if happy and hungry %}
I am happy *and* hungry; both are true.
{% endif %}
{% if happy or hungry %}
I am either happy *or* hungry; one or the other is true.
{% endif %}
{% for item in items %}
<li>{{ item.title }}</li>
{% endfor %}
这个语法糖比较有意思,它在for
循环后再加个else
语句,用来表达“当for循环所应用的集合本身为空时”应当执行的逻辑
{% for item in items %}
<li>{{ item.title }}</li>
{% else %}
<li>This would display if the 'item' collection were empty</li>
{% endfor %}
与JS的设计逻辑一样,循环也可以将一个对象视为Map来遍历它其中的键值对。假设我们有以下的context
var food = {
'ketchup': '5 tbsp',
'mustard': '1 tbsp',
'pickle': '0 tbsp'
};
我们就可以把对象中的字段名称为ingredient
,而将字段值称为amount
来遍历上面这个对象,如下
{% for ingredient, amount in food %}
Use {{ amount }} of {{ ingredient }}
{% endfor %}
渲染结果是:
Use 5 tbsp of ketchup
Use 1 tbsp of mustard
Use 0 tbsp of pickle
“受益”于JS混乱又迷惑的类型体系,nunjucks引擎还支持通过for
关键字来遍历Map
对象,以及unpack数组中的元素。不过我不建议你深究这些迷乱的用法,让for
专注于循环吧。
loop
对象来访问更多的循环信息引擎没有提供传统的for(int i = 0; i < 10; ++i)
这种循环,引擎仅提供了for .. in
这一种风格,不过,你可以通过一个神奇的loop
变量,在循环体内访问一些额外信息
属性 | 语义 |
---|---|
loop.index |
当前元素的脚标,但脚标是从1 起算的 |
loop.index0 |
同上,但脚标是从0 起算的 |
loop.revindex |
反向脚标,但脚标是从1 起算的 |
loop.revindex0 |
同上,但脚标是从0 起算的 |
loop.first |
一个布尔值,代表“当前循环是否是第一个循环,当前元素是否是第一个元素” |
loop.last |
一个布尔值,代表“当前循环是否是最后一个循环,当前元素是否是最后一个元素” |
loop.length |
循环集合的容量 |
我们上面讲了,几乎所有有效的JS表达式都能放在{{}}
中进行求值渲染。对于布尔表达式,JS沿用了C语言的复合操作符设计:即用&&
, ||
, !
来表示“且”, “或”, “非”。
不过在if
和elif
tag里,书写分支判断条件时,用的却是另外一套“操作符”:and
, or
和not
。这一点需要特别注意,如下所示:
{% if users and showUsers %}...{% endif %}
{% if i == 0 and not hideFirst %}...{% endif %}
{% if (x < 5 or y < 5) and foo %}...{% endif %}
我们上面说了,虽然几乎所有合法的JS表达式都能放在{{}}
中,但三元运算符写出来的表达式除外。nunjucks有自己独立的三元表达式语法,如下:
{{ "true" if foo else "false" }}
你可以将它看作是JS三元表达式的平替,所以它的求值结果可以传递给函数(当然前提是这个函数定义在context中),如下:
{{ baz(foo if foo else "default") }}
与JS中原生的三元表达式不同:else
分支是可以被省略的
{{ "true" if foo }}
宏的概念和C语言中的宏十分相似:长得像函数,但实际上引擎是用复制粘贴的方式处理它的
{% macro field(name, value='', type='text') %}
<div class="field">
<input type="{{ type }}" name="{{ name }}"
value="{{ value | escape }}" />
</div>
{% endmacro %}
有了以上定义后,你就可以如下复用
{{ field('user') }}
{{ field('pass', type='password') }}
从上面的例子你也能看出来,nunjucks中的宏还支持参数默认值
include
是一个类似于C语言中#include
的tag,但与C语言不同的是,在nunjucks引擎里,是把include
的文件先进行独立渲染,然后把渲染结果就地引用的。而不是先引用,再渲染。
{% for item in items %}
{% include "item.html" %}
{% endfor %}
不过还是要正确理解这个“先渲染,再引用”:虽然渲染是独立的,但外部可以给include
的文件进行context补充,比如上面的例子,外部调用现场先补充了item
变量的定义,这样即使item.html
是独立渲染的,也是可以访问到item
变量的。
默认情况下,被引用的文件如果缺失,引擎就会报错,如果想让引擎别报错,可以在后面添加ignore missing
,如下:
{% include "missing.html" ignore missing %}
上面我们说了set
和marco
这两个Tag:一个用来补充context,一个用来定义宏。默认情况下,这些补充的变量定义与宏定义,都是“可以被其它文件使用”的,即"exported"
而其它文件要使用,就先要使用import
这个tag来将目标文件进行“导入”。导入与引用的区别是:导入import
使用的是目标模板文件的宏定义与变量定义,而引用include
使用的是目标模板文件的渲染结果。
比如我们有一个form.njk
,如下定义:
{% macro field(name, value='', type='text') %}
<div class="field">
<input type="{{ type }}" name="{{ name }}"
value="{{ value | escape }}" />
</div>
{% endmacro %}
{% macro label(text) %}
<div>
<label>{{ text }}</label>
</div>
{% endmacro %}
我们在它里面定义了两个宏:field
和label
,那么我们就可以在另外 一个模板文件中如下引用这两个宏:
{% import "forms.html" as forms %}
{{ forms.label('Username') }}
{{ forms.field('user') }}
{{ forms.label('Password') }}
{{ forms.field('pass', type='password') }}
上面的用法中,是把整个form.njk
中exported的宏和变量,包装在as forms
里。还可以直接将exported的宏和变量直接导入成独立的变量,如下:
{% from "forms.html" import field, label as description %}
{{ description('Username') }}
{{ field('user') }}
{{ description('Password') }}
{{ field('pass', type='password') }}
raw
与verbatim
这两个tag是完全等价的,verbatim
是早期起的名字,后来因为太抽象不够直观被deprecate掉了,但依然可用。
这俩tag的效果就是“完全原样的输出内部内容”
过滤器的直接用法是|
,宏的直接用法是括号传参。但如果有时你想将一些复杂内容传递给过滤器或宏时,简写的直接调用就有点力不从心,你就可以用如下的方式将复杂内容传递给过滤器和宏:过滤器用filter
,宏用call
以下写法会把filter
内部的渲染内容传递给title
和replace
过滤器
{% filter title %}
may the force be with you
{% endfilter %}
{% filter replace("force", "forth") %}
may the force be with you
{% endfilter %}
call
则稍微有点不同:除了本身宏定义的参数列表,宏还可以通过call
将一大坨复杂内容传递到宏内部,而宏内部可以通过caller()
宏来访问这些内容,听起来有些绕,看一眼就明白了:
{% macro add(x, y) %}
{{ caller() }}: {{ x + y }}
{% endmacro%}
{% call add(1, 2) -%}
The result is
{%- endcall %}
上面先定义了宏add
,然后下面用call
来调用这个add
。在add
的定义内部,它使用了caller()
来访问传递进来的“复杂内容”。。怎么说呢,这个设计还是有些迷惑性的,谁想得到caller()
取的只是复杂内容,而不是调用者的元数据呢?
extend
和block
上面提到了include
和import
,这俩玩意某些程度上可以实现面向对象设计中的“包含”。除此外,nunjucks专门设计了一套“继承”。这个说起来太干巴,我们拿个例子来说明:
比如我们有一个parent.njk
,当“父类”
{% block header %}
This is the default content
{% endblock %}
<section class="left">
{% block left %}{% endblock %}
</section>
<section class="right">
{% block right %}
This is more content
{% endblock %}
</section>
再有个child.njk
,当“子类”
{% extends "parent.html" %}
{% block left %}
This is the left side!
{% endblock %}
{% block right %}
This is the right side!
{% endblock %}
最终child.njk
的渲染结果就是:
This is the default content
<section class="left">
This is the left side!
</section>
<section class="right">
This is the right side!
</section>
这看起来很奇怪,但铭记如下规则就可以豁然开朗:
block
这个关键字,在父类中,代表定义一个“抽象方法”:它可以有默认实现,就写在父类中。子类也可以override它block
这个关键字,在子类中,代表覆盖一个“抽象方法”extend
关键字就是继承关键字:在子类中使用,用来指明要继承的父类是谁另外,在子类覆盖抽象方法时,可以通过super()
来调用父类的默认实现,如下:
{% block right %}
{{ super() }}
Right side!
{% endblock %}
上面这个代码块的渲染结果如下:
This is more content
Right side!
到现在为止,nunjucks中的可调用物,总共有三种:
{{}}
里面求值|
管道式调用,或使用filter
tag进行调用。使用管道式调用时,可视为是一个表达式,可放在{{}}
里面求值渲染{{}}
里面求值渲染。也可以使用call
tag进行调用。关于JS函数,其实引擎也附带了三个JS函数可供开箱使用,它们分别是:range
, cycler
和joiner
。这里我们先介绍这三个特殊的函数,另外再介绍一下引擎携带的过滤器函数。
range([start], stop, [step])
用来生成一个序列集合,比如如下的代码渲染结果就是0,1,2,3,4
{% for i in range(0, 5) -%}
{{ i }},
{%- endfor %}
cycler(item1, item2, ...itemN)
生成一个特殊的对象,用迭代器的方式循环访问参数表里的值。比如下面这个例子:
{% set cls = cycler("odd", "even") %}
{% for row in rows %}
<div class="{{ cls.next() }}">{{ row.name }}</div>
{% endfor %}
用"odd", "even"
两个字符串通过cycler
函数生成了一个对象叫cls
,这个cls
可以看做是["odd", "even"]
的迭代器,初始时指向第一个元素,即是"odd"
,然后调用cls.current()
方法访问当前指向的对象,调用cls.next()
访问下一个对象,并将迭代器+1。
最重要的是,这个迭代器是循环式的,当访问到最后一个对象时再调用cls.next()
,迭代器又会返回指向第一个对象,即"odd"
joiner([sep])
joiner
的返回值是一个函数,这个函数特别有意思:第一次调用这个函数时,它返回一个空字符串(或者可以理解为什么都不返回),而后续每一次调用它,它都返回当初你调用joiner
时传递的那个sep
参数。sep
的默认值是一个逗号,即","
。这么说你可能有点迷惑,这有什么用?你看下面的例子就知道了:
{% set comma = joiner() %}
{% for tag in tags -%}
{{ comma() }} {{ tag }}
{%- endfor %}
如果tags
的值是["food", "beer", "dessert"]
的话,上面的渲染结果就是food, beer, dessert
。
它在渲染列表集合的时候特别有用,因为多数情况下我们都只希望列表项之前的分隔符只在第2个以及后续的列表项之前存在。
nunjucks语言本身要求引擎支持一些过滤器函数,这些过滤器函数是由“语言标准”规定的,所以一定能用。另外除过这些过滤器函数外,你所使用的框架,比如eleventy,可以还扩展实现了一些额外的过滤器函数,这些函数就需要你自己去查阅相关的框架文档了。内置的过滤器函数数目比较有限,列表如下:
函数名 | 功能 | 额外参数说明 | 示例 | 示例的渲染结果 |
---|---|---|---|---|
abs |
求绝对值 | 无 | {{ -3 | abs}} |
3 |
batch(num) |
将一个集合里的元素,每num 个以拼接的形式合并在一起,形成一个新的集合 |
数值 | {% for item in [1,2,3,4,5,6] | batch(2) %}{{"[" + item.toString() + "]"}}{% endfor %} |
[1,2][3,4][5,6] |
capitalize |
首字母大写 | 无 | {{ "this is a test" | capitalize}} |
"This is a test" |
center(length) |
返回一个长度为length 的新字符串,新字符串首尾由空格填充,将原字符串居中 |
数值 | {{"[" + "xxx" | center(20) + "]"}} |
[ xxx ] |
default(default, [falsyAsUdf]) |
对管道对象可能出现的空值做处理 | 第二个可选参数是boolean 类型,其默认值为false 。在 falsyAsUdf == false , 也就是默认情况下:如果管道传入的值严格的等于undefined ,则函数返回default ,否则返回管道值本身。在 falsyAsUdf == true 时,只要管道值在JS里算是“假”,函数就会返回default ,否则才会返回管道值本身 |
1. {{ undefined | default("it is undefined", false) }} 2. {{ null | default("it is undefined", false) }} 3. {{ false | default("it is undefined", false) }} 4. {{ undefined | default("it is falsy", true) }} 5. {{ null | default("it is falsy", true) }} 6. {{ false | default("it is falsy", true) }} |
1. it is undefined 2. 3. false 4. it is falsy 5. it is falsy 6. it is falsy |
dictsort |
将一个对象或Map里的键值对进行排序(按键进行字典排序)。返回的是原对象的一个排序后的副本 | 无 | {% for item in {"d":4, "a":1, "e":5, "b":2, "c":3} | dictsort %} {{item}} {% endfor %} |
a,1 b,2 c,3 d,4 e,5 |
dump([tabSize]) |
内部调用JSON.stringify ,并把结果中的引号等符号进行转义,以确保能原样显示在HTML文档中这玩意常用于开发过程中要debug的时候,可以在页面上临时显示对象的值 相当于调用C语言时使用的 printf |
可选,代表输出的JSON字符串的缩进宽度,或缩进字符。默认不换行也不缩进,当传入数值时,代表缩进的空格数,当传入字符串时,会把传入的字符串作为缩进字符去使用 | {{ ["a", 1, {b: true}] | dump }} |
["a",1,{"b":true}] |
escape |
将字符串进行转义,确保字符串能原样显示在HTML页面上 | 无 | {{ "<h1>xxx</h1>" | escape }} |
<h1>xxx</h1> |
first |
获取集合中的第一个元素,或者字符串中的第一个字符 | 无 | {{ [1,2,3] | first + "idiots" | first }} |
1i |
float([default=0.0]) |
将管道对象先toString ,再试图将结果转化成一个浮点数。如果转化失败,则返回0.0 ,或指定的值 |
可选,表示转化失败后要返回的默认值。默认情况下是0.0 |
{{ "35.67" | float + "one point seven" | float(1.7) }} |
37.370000000000005 |
forceescape |
行为与escape 类似,在某些情况下它可能会多添加双引号 |
无 | {{ "<h1>xxx</h1>" | forceescape }} |
<h1>xxx</h1> |
group(propertyName) |
将集合中的元素按指定的key聚合起来,形成一个新的集合 | 聚合时使用的key | {% set items = [ { name: 'james', type: 'green' }, { name: 'john', type: 'blue' }, { name: 'jim', type: 'blue' }, { name: 'jessie', type: 'green' }] %} {{ items | groupby("type") | dump(2) }} |
{ "green": \[ { "name": "james", "type": "green" }, { "name": "jessie", "type": "green" } \], "blue": \[ { "name": "john", "type": "blue" }, { "name": "jim", "type": "blue" } \] } |
indent([size=4], [firstLine=false]) |
将包含换行符的字符串进行缩进 | size 代表缩进宽度,默认为4firstLine 代表是否缩进首行,默认为false |
{{ "abc\nabc\nabc" | indent(2, true) }} |
abc abc abc |
int([default=0]) |
将管道对象先toString ,再试图将结果转化成一个整数。如果转化失败,则返回0 ,或指定的值 |
可选,表示转化失败后要返回的默认值。默认情况下是0 |
{{ "35.67" | int + "seventeen" | int(17) }} |
52 |
join([sep=""], [property]) |
将集合中的元素都concat起来变成一个大字符串 | sep 表示元素与元素之间的连接字符,默认为空字符串property 指,如果元素是对象的话,要取它哪个属性进行字符串化,默认是在元素本身上执行toString() |
{{ [1,2,3] | join }} {{ [1,2,3] | join("-") }} {{ [{v:1}, {v:2}, {v:3}] | join(",", "v") }} |
123 1-2-3 1,2,3 |
last |
获取集合中的最后一个元素,或者字符串中的最后一个字符 | 无 | {{ [1,2,3] | last + "idiots" | last }} |
3s |