HTML 模板语言纵览
前端开发的本质,是把结构化的数据映射到 HTML。HTML 本身是静态的,因此模板引擎应运而生,接下了动态生成 HTML 的任务,直到近年来在前后端分离的浪潮下被面面俱到的前端框架所兼并。本文试图梳理出模板语言的主流范式,不过注意本文并非按照时间线编排,如果要还原历史的话,应该是 PHP (1995) → Zope 2 (1998) → JSTL (2002) → Django (2005) → Haml (2006) → Mustache (2009) → AngularJS (2010)。
PHP 风格
虽然 PHP 早已是一门通用编程语言了,不过它最早是作为 HTML 模版引擎而出现的。从它现在的全称「超文本预处理器」也可以想象出,PHP 代码可以嵌入到 HTML 中,在用户请求该网页时,后端预先执行 PHP 代码并生成插入了运行结果的 HTML。下面便是一个最简单的例子:
<html>
<head>
<title>Personal Home Page</title>
</head>
<body>
<?php echo "Hello {$world}"; ?>
<?= "Hello {$world}" ?>
</body>
</html>
包裹在 <?php … ?>
标签里面的便是服务器要执行的 PHP 代码,直接 echo
一个表达式可以简写为 <?= … ?>
。像这种在 HTML 里用特殊标签插入后端脚本的做法,从互联网诞生开始就相当普遍,至今仍屡见不鲜,本文称其为「PHP 风格」。这些 PHP 风格的模板引擎大同小异,区别主要在于内嵌语言用什么、代码块用什么标签包裹。
几乎每一门后端编程语言都有自己的 PHP 风格的模板引擎,因为在 HTML 直接嵌入后端脚本对于后端开发者来说最容易上手,没有任何学习上的负担。这些模板引擎中较为知名的有:
- 微软公司推出的 ASP,默认脚本语言为 VBScript;
- 昇阳公司推出的 JSP,相当于 ASP 的 Java 版本;
- Ruby on Rails 框架默认使用的 eRuby;
- JavaScript 也有类似的 EJS,博客框架 Hexo 在用。
JSP / eRuby / EJS 都继承了 ASP 的习惯,使用 <% … %>
来包裹脚本,还有 <%= … %>
渲染表达式结果等其他便利的标签。
Mustache
PHP 风格的模板引擎虽然历史悠久,但在设计上有一个比较明显的问题:代码逻辑和 HTML 模板混杂在一起。因此,GitHub 的联合创始人 Chris Wanstrath 发明了广为人知的 Mustache。Mustache 的语法非常简洁,没有任何显式的控制流语句,完全由数据驱动,因而自称 logic-less。它不与任何编程语言耦合,几乎所有主流语言都有 Mustache 模板引擎的实现。
Mustache 有两种基本的标签形式:一种是像 {{variable}}
这样渲染变量的值,另一种则是像 {{#section}} … {{/section}}
这样的区块。根据键值的不同,区块隐含四种语义:
- 如果是假值或者空列表,就完全不渲染;
- 如果既不是假值也不是列表,就会渲染一次;
- 如果是非空列表,就渲染列表长度次;
- 如果是函数,则会以区块包裹的原始文本调用该函数。
另外还有 {{^inverted}} … {{/inverted}}
与正常的区块相反,如果是假值或空列表则渲染一次,否则不渲染。下面是 Mustache 模板的一个典型用例:
<h1>{{header}}</h1>
{{#items}}
{{#first}}
<li><strong>{{name}}</strong></li>
{{/first}}
{{#link}}
<li><a href="{{url}}">{{name}}</a></li>
{{/link}}
{{/items}}
{{^items}}
<p>The list is empty.</p>
{{/items}}
假设我们的输入数据是用下面这个 JSON 表示的:
{
"header": "Colors",
"items": [
{"name": "Red", "first": true, "url": "#red"},
{"name": "Green", "link": true, "url": "#green"},
{"name": "Blue", "link": true, "url": "#blue"}
]
}
那么 Mustache 便会渲染出如下 HTML:
<h1>Colors</h1>
<li><strong>Red</strong></li>
<li><a href="#green">Green</a></li>
<li><a href="#blue">Blue</a></li>
虽然 Mustache 的设计小而美,但实际使用起来难免捉襟见肘。Handlebars 是对 Mustache 语言的扩展,最大的区别在于它引入了辅助函数。值得一提的是,其内置的 #if
#unless
#each
#with
辅助函数明确了 Mustache 区块的隐式语义,譬如前面例子中的:
{{#items}} … {{/items}}
可以显式写成{{#each items}} … {{/each}}
;{{#first}} … {{/first}}
可以显式写成{{#if first}} … {{/if}}
;{{^items}} … {{/items}}
可以显式写成{{#unless items}} … {{/unless}}
。
Django 风格
Mustache 完全去除了代码逻辑,而 Handlebars 又稍稍加回了一些;不过更多的模板引擎出于实用性考量,不吝于引入更多逻辑,但也不愿复杂到直接内嵌后端脚本,换句话说就是试图在 Mustache 和 PHP 风格之间寻找平衡。如果要给这些中庸的模板引擎选个代表,最早为人所知的应该是 Django Template Language(以下简称 DTL),实际上它的出现要早于 Mustache。
与先前的话术稍有不同,DTL 将渲染表达式的 {{ variable }}
称为变量,将控制流程的 {% tag %}
称为标签,其内置了二十多个标签,包括常用的 for
if
elif
else
等等。DTL 最大的特色是过滤器,譬如 {{ list | length }}
能够获取列表的长度、{{ text | escape | linebreaks }}
能先将文本转义再把换行符替换成 HTML 标签等等,大约有六十个过滤器内置其中。下面是一段 Django 模板语言的简单示例:
<h1>{% block title %}{% endblock %}</h1>
<ul>
{% for user in users %}
<li><a href="{{ user.url }}">{{ user.username }}</a></li>
{% endfor %}
</ul>
后来,Flask 的作者 Armin Ronacher 参考 DTL 的设计实现了独立于后端框架的 Jinja 模板引擎;而 Mozilla 提供了一个 JavaScript 上的实现 Nunjucks;Shopify 在 Ruby 上也有十分相似的 Liquid 模板引擎,并被用于 GitHub Pages 默认的静态站点生成器 Jekyll。
Go 语言标准库的模板也可以算是 Django 风格,但它没有 {% … %}
只有 {{ … }}
。比如前面 DTL 的 {% for user in users %} … {% endfor %}
写作 {{ range $user := .Users }} … {{ end }}
,而渲染变量和字段写作 {{ $variable }}
和 {{ .Field }}
,函数链式调用亦可用管道表达。
模板属性语言
上述三种风格,其实都可以归类于往 HTML 里面插各种 HTML 语法以外的 <% … %>
{{ … }}
,那么还有没有别的方式嵌入动态内容呢?有一种有趣的设计叫做「模板属性语言」(TAL),也就是说我们把动态内容写在正常 HTML 标签的自定义属性里。TAL 最大的好处是简化了开发者和设计师的协作,因为 TAL 能直接加在设计原型上,加上之后仍然是照常显示的 HTML,不经后端渲染直接用浏览器打开也不会感知到动态代码的存在。最早提出 TAL 的是 Python 编写的 Zope 2,其模板引擎 Zope Page Templates 使用了一系列 tal:
属性来引入动态内容。
如今较为纯粹的例子是 Java 上的模板引擎 Thymeleaf,自称「自然模板」。下面是自然模板的一个示例,其中 th:text
会替换掉标签内的原有内容、th:each
会进行迭代:
<table>
<thead>
<tr>
<th th:text="#{msgs.headers.name}">Name</th>
<th th:text="#{msgs.headers.price}">Price</th>
</tr>
</thead>
<tbody>
<tr th:each="prod: ${allProducts}">
<td th:text="${prod.name}">Oranges</td>
<td th:text="${#numbers.formatDecimal(prod.price, 1, 2)}">0.99</td>
</tr>
</tbody>
</table>
标签库
既然能自定义 HTML 属性,那么可不可以自定义 HTML 标签呢?JSP 标准标签库(JSR-52: JSTL)便实践了这一想法,虽然自定义标签不再有自然模板的好处,但写起来会更方便不少。JSTL 定义了 <c:if test="${age >= 20}">
<fmt:message key="i18n">
<sql:query … >
<x:parse … >
等四类标签,在属性上还可以使用表达式语言(JSR-341: EL)来插入动态内容,就像前述 <c:if>
中的 ${age >= 20}
那样。
JSP 也允许用户定义 JSTL 以外的自定义标签,这不禁让我们联想起了如今的 Web Components:
class PopUpInfo extends HTMLElement {
constructor() {
super();
…… // write element functionality in here
}
}
customElements.define('popup-info', PopUpInfo);
以上代码便可以创建一个自定义标签 <popup-info>
,而该元素的行为和语义均可由用户自行决定。 React / Angular / Vue 等前端框架非常提倡这种可复用的组件,不过它们提供了更高层的抽象,让自定义组件更易写易用。
Haml
前面的模板语言说到底都还是 HTML 的超集,而 Haml 则完全抛弃了 HTML 原有的语法,走向了截然不同的方向。Haml 全称 HTML 抽象标记语言,由 Sass 之父 Hampton Catlin 发明。Haml 的写法有点像 CSS selector,譬如 %p.sample#welcome Hello, World!
会被渲染为 <p class="sample" id="welcome">Hello, World!</p>
。Haml 有 =
和 -
前缀分别用来渲染表达式结果和控制流程,另外它跟 Python / Haskell 一样采用了越位规则,也就是说以缩进来界定文档结构。
不过 Haml 需要在每个标签前面写 %
还是有点麻烦的,JavaScript 上的 Pug(原名 Jade)对其进行了一些语法上的改进,后来又出口转内销,Slim 把相似的语法带回了 Ruby:
doctype html
html
head
title Slim Examples
link rel="icon" type="image/png" href=file_path("favicon.png")
body
#content
p This example shows you what a basic Slim file looks like.
- if items.any?
table#items
- items.each do |item|
tr
td.name = item.name
td.price = item.price
- else
p No items found. Please add some inventory.
Thank you!
div id="footer"
= render "footer"
| Copyright © #{year} #{author}
说句题外话,我在用 Spring 写网站的时候曾经一度很困惑,过去不支持 Java 注解的时候,大家是如何忍受手写 XML 配置文件的呢?然而我开始写 Thymeleaf 模板的时候突然意识到,我自己对于手写 HTML 不也习以为常了吗?XML 配置文件正逐渐被 YAML / TOML 等新兴格式所取代,HTML 模板的未来又会如何呢?
结语
虽然上述六种分类特意将 HTML 模板语言的范式孤立开来,但如今流行的前端框架往往集成了多种范式,譬如 Angular 和 Vue 都支持 Django 风格的插值和管道 {{ interpolation | pipe }}
,而写在 HTML 属性上的指令 <p *ngIf="true">
<p v-if="true">
则类似于模板属性语言,众所周知它们也都支持自定义标签的组件化开发。这里我有意忽略了 React 的 JSX:在 JSX 中 JS 反客为主,HTML 组件变成了 JS 代码的一部分,恕我不算它是 HTML 模板语言了。
总而言之,本文力求归纳了主流的 HTML 模板范式,但 Web 开发毕竟不是我的主业,行文难免有所疏漏,但愿不会贻笑大方。