Node.js模板引擎的深入探讨

第一轮排除

在上次node模板引擎简单比較的文章里。事实上已经有个简单的筛选了。总结成规则应该是这种:

最后在 Template Chooser 上依据条件选择下来,就剩这些了:

基本上就剩下 mustache 系了

尽管之前也接触了一些的模板引擎,传统的比方 PHP 的 Smarty。Java 的 Velocity,甚至曾经公司里跨平台的火麒麟等,但我还是承认看过一次 mustache 就对这个系列产生了偏好,那么接下来就具体分析一下他的一些特性。

mustache类引擎特性分析

长处是简洁, if / for 都能够通吃。但缺点是依赖于必选变量,非常难将推断条件其扩展成表达式。另外 for 的索引变量名也无法设置。

#xxx 是一个 Object 类型值时。是应该依照 if 存在推断还是依照 for-in 遍历?眼下是推断存在并创建下级作用域,这就导致无法使用 Object 类型作为 Map 进行 for-in 遍历。

另一点,假设是在进行一个 list 的循环时,无法定义循环项和索引的变量名。非常难利用到索引这个特殊变量。只是在 GRMustache 的实现中有增强,能够使用 @index特殊变量,但这不是一个比較好的解决方式。

# 模块生成了子级作用域,使模块内的子级变量得到简写。缺点是设计渲染引擎的时候可能对于scope的情况考虑起来比較复杂,比方不同层次的同名变量,有可能会导致性能问题。

除了变量输出,控制语句的双括号有点多余了,能够考虑降低一层,由于内部还是一个符号,造成代码冲突的几率事实上非常低。另外模板语句的结束符名称事实上也能够省略掉,正如语法分析过程中的关闭括号。演示样例:

{
* {{variable}}
{/}

可能原始设计以检測到双括号作为打开解析引擎的标志,假设改为单括号,后面碰到非模板的字符须要回朔一位。

因为条件推断都合并到了 # 符号的语句中。而推断因为要简化多种形式。导致不能使用表达式。而 mustache 仅仅设计了 ^ 用来取反的一种表达方式,实际上和 # 都没有不论什么关联,在表达能力上就比較不足,比方想用 else
if
就非常麻烦。

类似 switch/case 的多条件分支就更难实现,尽管用的也不多,可是还是会有一定机会。

在 mustache 里仅仅有一种,就是引入模板片段,类似于其它引擎的 include。符号是 {{>partial}}

并且这指定的是模板名,在后端程序中一般是直接寻找文件名称,但还须要自己映射。

另外除了Handlebars其它也不支持继承形式的模板复用。所以我之前写了MustLayout这个npm包来在express中预处理这两个缺陷。

变量在 mustache 中很easy,差点儿仅仅有模板替换的功能。

而在其它引擎中,能够对暂时变量赋值,输出能够使用表达式,或者管道过滤器等便捷的方法。

对照其它模板引擎的突出特性

如 liquid/Smarty/Swig 等中,能够在渲染模板时创建暂时变量,在某些情况下有一定的便利性。

比方在不同模板里引用一个模板片段,该片段中的某个变量名是固定的。但在不同地方引用的时候变量名不同,此时能够在引用之前声明一个统一的变量,帮助统一引用。

这个特性一定程度上也能够由函数模板来完毕。

在 liquid/Smarty/Swig/Etpl 等中,能够通过类似 *nix shell 的管道模式,对要输出的变量进行很多其它处理,比方日期格式化。编码转义等功能。

{{ aDate | date(‘Y-m-d') }}

在继承 block 块时能够使用父模板中已定义的部分。方便的追加很多其它内容,比方 CSS 和 JS 的引用部分:

{% block head %}
  {% parent %}
  <span class="tag"><<span class="title">link</span> <span class="attribute">rel</span>=<span class="value">"stylesheet"</span> <span class="attribute">href</span>=<span class="value">"custom.css"</span>></span>
{% endblock %}

Dust.js 的继承方式看起来比較诡异,是使用一个正常理解应该是 include 的方式来实现的。并且符号也是从 mustache 系继承过来的 {>parent} ,而只在之后定义 block 区块,对父模板进行覆盖来实现。从实现的角度看这是一个比較取巧的方式,由于假设不过声明 layout 。那么声明语句究竟放在模板的哪里比較合适?假设声明两次是否会造成问题?而通过引入的话就比較直白了,无论如何这是必须写的且只会写一次。我是要用父模板。先拿进来。之后的 block 部分实际上是重名再次定义的赋值过程。

issue里甚至有人提到这样的写法应该使用开闭标签。让 {>parent}&#x2026;{/parent} 之间包括其block的内容,也有道理,可是写起来是略有复杂,不够直白。

在 include 一个模板片段时代入一个自己定义的块。以覆盖片段中的部分内容。这给 block 除了向上继承以外很多其它的一种灵活性。


        <span class="tag"><<span class="title">div</span> <span class="attribute">class</span>=<span class="value">"list"</span>></span>list<span class="tag">div</span>>
        <span class="tag"><<span class="title">div</span> <span class="attribute">class</span>=<span class="value">"pager"</span>></span>pager<span class="tag">div</span>>

在 Etpl 中称为过滤器,眼下用例是将 Markdown 格式的模板内容转换成HTML,有一定价值。但不一定是必须功能,能够考虑作为扩展实现。


## markdown document

This is the content, also I can use ${variables}

期待 mustache 增强的特性

对照了那么多,事实上说对 mustache 最基本的偏好还是来自于模板语言表达的间接性,而对于他最核心的轻逻辑来说。有点太轻。尽管我不须要完整的原生语言控制,但轻的难以表达了就还是须要权衡。

终于我把我期待的模板引擎的样子描绘出来。看看是不是有人和我一样。

最基本的变量还是使用双括号,而控制语句使用但括号+特殊字符,同一时候关闭能够为自结束。而且不须要写相应的关闭标签名。

使用双括号在模板中输出变量:

{{ variable }}
{{ nested.element }}
{{ array[index] }}
{{ <span class="keyword">object</span>[key] }}

输出能够使用带运算符的简单表达式:

{{ ok ? <span class="number">1</span> : <span class="number">0</span> }}
{{ ok || <span class="string">'none'</span> }}
{{ <span class="keyword">index</span> * (<span class="keyword">x</span> + <span class="number">3</span>) }}

能够使用过滤器管道:

{{ variable | escapeHTML }}
{{ today | date:<span class="string">'Y-m-d'</span> }}
{{ <span class="keyword">group</span> | max }}

默认不进行 HTML 转义。这样能够支持很多其它情景,而不是 HTML 专属。相反使用三括号才进行默认转义:

{{{ content }}}

能够使用等号 = 进行暂时变量赋值,但赋值使用专门的 $ 符号语句且须要自关闭符号:

{<span class="variable">$ </span>x = y * <span class="number">5</span> /}
{<span class="variable">$ </span>obj = {<span class="symbol">a:</span> <span class="number">1</span>, <span class="symbol">b:</span> []} /}

变量作用域没有发现太大的必要性。并且可能造成性能问题,临时取消。

尽管 mustache 的 # 功能非常强大。但表达能力略有欠缺且easy造成歧义,所以我还是把条件分支单独拿出来。

if 语法用问号开头表达,和条件表达式一样有疑问的意思:

{? expression }
<span class="literal">true</span>
{/}

{? !condition }
<span class="literal">false</span>
{/}

else 语法借用原来的 ^ 符号,且不再能够单独使用这个取反符号:

{? expression}
<span class="literal">true</span>
{^}
<span class="literal">false</span>
{/}

else if 类型的多条件继续使用 ^ 符号进行额外推断:

{? case1 }
1
{^ case2 }
2
{^}
-1
{/}

临时没想到怎样简洁的表达对同一条件的 switch/case 表达,先用 else
if
结构取代。

普通的 for 循环继续使用 # ,但添加迭代条目和索引暂时变量声明:

{# list:item@index }
<span class="tag"><<span class="title">li</span>></span>{{ index }}: {{ item }}<span class="tag">li</span>>
{/}

循环能够针对普通数组。也能够针对 Object 类型的对象:

{# map:value@key }
<span class="tag"><<span class="title">li</span>></span>{{ key }}: {{ value }}<span class="tag">li</span>>
{/}

能够联合取反符号 ^ 使用。输出没有元素项时的内容:

{
{{ item }}
{^}
none items :(
{/}

内嵌模板片段:

{> partialName /}

模板名称能够在 API 中分情况实现。比方在后端 node.js 环境中,模板名直接相应相对路径进行文件读取;而在前端假设是使用 <script type="text/template"></code>方式加载的,能够在相应标签属性,通过 DOM 选择器读取。</p><p>能够考虑引入 etpl 的片段替换扩展:</p><pre class="prettyprint undefined"><code>{> partialName } <span class="indent"> </span>{+ blockName }My Title{/} {/} </code></pre><p>继承上级模板能够考虑引入 Swig 的父级块引用:</p><pre class="prettyprint xml"><code> {+ scripts} <span class="indent"> </span><span class="tag"><<span class="title">script</span> <span class="attribute">type</span>=<span class="value">"text/javascript"</span> <span class="attribute">src</span>=<span class="value">"lib.js"</span>></span><span class="javascript"></span><span class="tag"><!--<span class="title"-->script</span>></span> {/}</p> <p>{<span class="tag">< <span class="attribute">parent</span> /} {+ <span class="attribute">scripts</span> } <span class="indent"> </span>{+/}<!<span class="attribute">--</span> 内嵌上级的块 <span class="attribute">--</span>></span> <span class="indent"> </span><span class="tag"><<span class="title">script</span> <span class="attribute">type</span>=<span class="value">"text/javascript"</span> <span class="attribute">src</span>=<span class="value">"main.js"</span>></span><span class="javascript"></span><span class="tag"><!--<span class="title"-->script</span>></span> {/} </code></pre><p>这样的声明式写法比較easy理解。而假设要实现简单,能够学习 dust.js 直接利用片段插入扩展的方法。</p><p>这样设计的符号体系,让引擎能够从最简单的规则出发,并减少冲突的可能性。</p><h2>前后端共用模板的一些问题</h2><p>问题一:后端能够用文件名称取代模板名,但前端没有。</p><p>所以要在前端生成模板名。</p><p>假设生成后,前后端模板名不一致,也会导致无法复用。比方须要include的时候,后端默认是文件路径。但前端一般的模板名称不会带有斜杠 <code class="prettyprint">/</code> 字符。</p><p>问题二:在后端 render 完某个 path 的页面后,前端接管并使用 ajax 方式,此时使用 History API 在 route 到下一个 path 的时候,调用 view 的相应模板怎样推断使用那一层 layout?</p><p>问题三:后端和前端在模板中使用的变量名或者层次往往不一致,假设仅作为片段问题到不大。但假设是整页渲染,可能须要额外的针对性处理。</p><h2>API设计</h2><pre class="prettyprint php"><code><span class="keyword">var</span> Mustplus = Class({ <span class="indent"> </span>config: { <span class="indent"> </span><span class="indent"> </span>ifStart: [<span class="string">'{?'</span>, <span class="string">'}'</span>], <span class="indent"> </span><span class="indent"> </span>ifEnd: <span class="string">'{/}'</span>, <span class="indent"> </span><span class="indent"> </span>elseWord: [<span class="string">'{^'</span>, <span class="string">'}'</span>], <span class="indent"> </span><span class="indent"> </span>forExp: /{</code><p><code>:@(\w+))}?/, <span class="indent"> </span><span class="indent"> </span>forEnd: <span class="string">'{/}'</span> <span class="indent"> </span><span class="indent"> </span> <span class="indent"> </span>}, <span class="indent"> </span>templates: {}, <span class="indent"> </span>compiled: {},</p> <p><span class="indent"> </span>constructor: <span class="function"><span class="keyword">function</span> <span class="params">(options)</span> {</span> <span class="indent"> </span><span class="indent"> </span><span class="keyword">this</span>.read = browser ? <span class="keyword">this</span>.readDOM : <span class="keyword">this</span>.readFile; <span class="indent"> </span>}, <span class="indent"> </span> <span class="indent"> </span>readDOM: <span class="function"><span class="keyword">function</span> <span class="params">(name)</span> {</span> <span class="indent"> </span><span class="indent"> </span><span class="keyword">return</span> $(<span class="string">'script[type=text/template][name='</span> + name + <span class="string">']'</span>).html(); <span class="indent"> </span>},</p> <p><span class="indent"> </span>readFile: <span class="function"><span class="keyword">function</span> <span class="params">(file)</span> {</span> <span class="indent"> </span><span class="indent"> </span><span class="keyword">return</span> fs.readFileSync(path.join(<span class="keyword">this</span>.base, file)); <span class="indent"> </span>}, <span class="indent"> </span> <span class="indent"> </span> <span class="indent"> </span>extend: <span class="function"><span class="keyword">function</span> <span class="params">(name)</span> {</span> <span class="indent"> </span> <span class="indent"> </span>},</p> <p><span class="indent"> </span><span class="keyword">include</span>: <span class="function"><span class="keyword">function</span> <span class="params">(template)</span> {</span> <span class="indent"> </span><span class="indent"> </span><span class="keyword">return</span> template.match(includeRE) ? template.replace(includeRE, <span class="function"><span class="keyword">function</span> <span class="params">(name)</span> {</span> <span class="indent"> </span><span class="indent"> </span><span class="indent"> </span><span class="keyword">return</span> <span class="keyword">this</span>.<span class="keyword">include</span>(<span class="keyword">this</span>.read(name)); <span class="indent"> </span><span class="indent"> </span>}) : template; <span class="indent"> </span>},</p> <p><span class="indent"> </span>resolve: <span class="function"><span class="keyword">function</span> <span class="params">(name)</span> {</span> <span class="indent"> </span><span class="indent"> </span><span class="keyword">return</span> <span class="keyword">this</span>.<span class="keyword">include</span>(<span class="keyword">this</span>.extend(name)); <span class="indent"> </span>}, <span class="indent"> </span> <span class="indent"> </span>compile: <span class="function"><span class="keyword">function</span> <span class="params">(name)</span> {</span> <span class="indent"> </span><span class="indent"> </span><span class="keyword">var</span> content = <span class="keyword">this</span>.resolve(name); <span class="indent"> </span><span class="indent"> </span><span class="keyword">var</span> stream = <span class="keyword">this</span>.parse(content); <span class="indent"> </span><span class="indent"> </span><span class="keyword">var</span> fn = <span class="string">''</span>; <span class="indent"> </span><span class="indent"> </span><span class="keyword">while</span>(token = stream.next()) { <span class="indent"> </span><span class="indent"> </span><span class="indent"> </span><span class="keyword">switch</span>(token.type) { <span class="indent"> </span><span class="indent"> </span><span class="indent"> </span><span class="indent"> </span><span class="keyword">case</span> <span class="string">'text'</span>: <span class="indent"> </span><span class="indent"> </span><span class="indent"> </span><span class="indent"> </span><span class="keyword">default</span>: <span class="indent"> </span><span class="indent"> </span><span class="indent"> </span><span class="indent"> </span><span class="indent"> </span>fn += text; <span class="indent"> </span><span class="indent"> </span><span class="indent"> </span>} <span class="indent"> </span><span class="indent"> </span>} <span class="indent"> </span><span class="indent"> </span><span class="keyword">return</span> <span class="keyword">new</span> Function(<span class="string">'data'</span>, fn); <span class="indent"> </span>},</p> <p><span class="indent"> </span>cache: <span class="function"><span class="keyword">function</span> <span class="params">(name)</span> {</span> <span class="indent"> </span><span class="indent"> </span><span class="keyword">return</span> <span class="keyword">this</span>.compiled[name] || (<span class="keyword">this</span>.compiled[name] = <span class="keyword">this</span>.compile(name)); <span class="indent"> </span>}, <span class="indent"> </span> <span class="indent"> </span>render: <span class="function"><span class="keyword">function</span> <span class="params">(name, data)</span> {</span> <span class="indent"> </span><span class="indent"> </span><span class="keyword">return</span> <span class="keyword">this</span>.cache(name)(data); <span class="indent"> </span>} });</p> <p><span class="keyword">var</span> engine = <span class="keyword">new</span> Mustplus({});</p> <p><span class="keyword">var</span> template = engine.compile(<span class="string">'xxx'</span>); template(data);</p> <p>engine.render(<span class="string">'xxx'</span>, data); </code></p></pre><p>眼下市面上最接近我想法的应该就是 <a href="http://linkedin.github.io/dustjs/" rel="noopener">dust.js</a> 了。难怪 LinkedIn 的project团队 <a href="http://engineering.linkedin.com/frontend/client-side-templating-throwdown-mustache-handlebars-dustjs-and-more" rel="noopener">通过对照最后选择的也是 dust.js</a> 。当然如我期待的话还有能够改进的地方,YY总是要有的。万一哪天顺手就实现了呢?</p></script>

Original: https://www.cnblogs.com/yfceshi/p/7403948.html
Author: yfceshi
Title: Node.js模板引擎的深入探讨

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/554340/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球