whisker

0.3.9 • Public • Published

Whisker.js v0.3.9 - 轻逻辑&可扩展的js模板引擎

为什么选择 Whisker.js?

简单,简洁

像这样的模板代码

{$property}
{#group}{/group}
{@method}
{%exp}

是不是相对清爽一些?( 你是不是已经受够了 {{xxx}} ?)

轻逻辑,但并不是没有一点逻辑

把js逻辑从模板语言中剥离出来,有利于视图和逻辑解耦,使得模板更清晰。 但是很多时候,如果过于简单,则面对复杂的需求时总显得有些力不从心。 所以,一定的逻辑性还是很有必要的。比如if语句支持表达式

可扩展

whisker.js是遵循开闭原则的精神进行设计和编码。所谓开闭原则即要对扩展开放,对修改关闭。 whisker.js提供了多种层次的扩展性。最高层次可供用户进行扩展。比较底层的供开发者扩展。

易于调试错误&适配amd cmd模块

whisker解析式会发现错误并报告模板中出现错误的位置,方便调试。并且自己已经适配了amd cmd模块(包括node)可以直接script引用,也可以由遵循这些规范的 模块加载器加载。

自定义界定符

whisker默认使用{}为界定符 并且支持自定义开始界定符和结束界定符, 最多支持两个字符如'<%','%>' 建议在和后台模板语言冲突(如php的smarty)时才自定义界定符。 因为单字符界定符其他符号容易在js中出现被误解析,而两个字符的界定符多多少少会影响性能~

不依赖任何框架,压缩后仅10k。

快速上手

一个简单地示例如下

var data={
    name:'张三',
    birth:'1989-02-13',
    getAge:function(birth,虚岁){
            return new Date().getFullYear()-new Date(birth).getFullYear()+虚岁;
        }
    }
var html=Whisker.render("<h1>{$name}</h1><div>年龄:{@getAge($birth,1)}岁</div>",data);
//result:"<h1>张三</h1><div>年龄:26岁</div>"

上面的代码描述了whisker的基本用法。render函数接受两个必选参数第一个为需要渲染的模板代码,第二个则是渲染数据。 whisker模板中,所有的whisker代码都包括在{}中,由大括号里的第一个字符作为这块whisker代码的语义。 分别包括

  • $【属性】
  • @【方法】
  • #【块操作或者叫组操作】
  • /【结束块操作】
  • %【表达式求值】
  • !【注释】

下面逐一介绍 whisker的功能

属性

{$xxx} 代表取属性xxx的值 属性名的允许值和js中标示符的规则大致一致,但略有不同,比如允许数字开头甚至{$1} {$2}

1、允许使用 “.” 多级访问属性的属性。

例如

var data={
    a:{
        b:2
    }
}
Whisker.render("{$a.b}",data);

2、使用"~"来对将html标签编码成html实体

仅编码<和> 其他实体也没必要编码 。注意:~必须出新在变量名最前面,变量名中不允许出现~ 实例:

var data={
    tag:'<div>abc</div>'
}
Whisker.render('<span>{$~tag}</span>',data);
//result :"<span>&lt;div&gt;abc</div></span>"

使用“^”从根block的scope对象开始取属性

这里需要对block和scope做个解释。block是一块模板代码的抽象。整个模板代码其实就是一个block(称作根block)。一个block会有一个scope object即作用域对象。属性代码({$xxx})所取的属性其实就是当前block的作用域对象的属性。在上述的例子中,传递给render函数的那个data就是根block的作用域对象。{$name}能取到data.name的值就是这个原因。

那为什么要引入块呢?是为了处理子作用域和嵌套的问题。通过嵌套子block来描述嵌套。每个block都有他的scope对象(下面简化为scope)。最实际的例子就是each迭代即{#each}{/each} 直接例子说明吧 (下面会具体介绍{#}的用法,这里的{#each}只表明问题不做过多解释)

var data={
   name:'大舌头',
   abc:'abc',
   list:[
    {name:'张山',items:[1,2,3]},
    {name:'李式',items:[4,5]}
   ]
}
var tmpl='{#each $list}'              +
         '<div>{$name}</div>'         +
         '<div>{$abc}{$^name}</div>'  +
            '{#each $items}'          +
            '<span>{$}</span>'        +
            '{/each}'                 +
         '{/each}';
Whisker.render(tmpl,data);

为了避免说的过于深入而费解。在这就直接定义:每个each都会创建一个新的block,该block的scope都会变成each的参数,在本例中即list。而且在循环迭代输出时,当前的scope会动态的变成list的每个迭代项。 这里的{$name}引用的正是当前scope的name属性,即{name:'张山'}或者{name:'李式'}的name(循环迭代时决定)。而{$abc}这个怎么办呢?whikser如果在当前的scope没有发现指定的属性,那它会向上层的block的scope上查找,直到查到根block的scope。但是如果你想要取外层的name时就不行了,因为它被当前scope里的name屏蔽了。所以这时就需要^了{$^xxx}就代表从最外层的(即根block)的scope(即上述的data)上开始查找属性。这样就可以找到最外面那个name了。 *同时,建议用户在需要取外层scope的属性时,即使没被当前的属性覆盖。也使用^方式查找,因为自动搜索scope链还是比较影响效率的

$可代表当前的scope对象本身。

在上面那个例子中 迭代循环[1,2,3]时,每次迭代时的scope分别是1 ,2,3 所以用{$}即能取到他们。但是注意,如果当前的scope不是个普通类型(数字,字符串) 则会简单粗暴的使用toString() 额 所以小心输出成[object Object] =。=

$$属性名替换

属性名中以$开头的属性名 会替换成它的值作为新的属性名,这个可能有点儿拗口和费解。其实就是类似PHP中的$$abc这种, 比如$abc的值是"xxx"那么这个表达式($$abc)的值就是$xxx这个变量的值。 坦白讲这个功能的实用价值有待考虑~而且这种做法会限制数据中的属性名不能出现$ 此功能考虑去除~或者换标示符 比如# 实例:

var data={
    obj:{
        name:'exolution'
    }
    abc:'name'
}
Whisker.render('{$obj.$abc}',data);

方法

{@func} 代表调用func的这个方法。

取方法的规则和属性一样都是在当前scope上查找 当然,也支持^. 调用方法支持传递参数 如 {@func($name,1,'abc')},参数必须为下面选项之一

  • 数字
  • 字符串,须以' '包裹
  • 属性名 如$name

注意:参数不允许出现表达式 如 1+2, $index+1 皆为非法

另外,调用方法时,方法的this为当前的scope 示例见第一个例子

表达式

{%$index+1*(2+3)} 以%开头即表示表达式 其实引入表达式有悖于轻逻辑的初衷,而有时候有不得不需要一些表达式。额,反正少用吧,确实影响效率

  • 表达式支持属性({$xxx}) 暂不支持方法{@xxx}
  • 支持常用的表达式运算操作如加减乘除 逻辑运算,还有三元操作符。
  • 支持括号

块操作/组操作

{#xxx}{/xxx} 由#开头即为快操作或者,组操作 为啥不全叫块操作呢,因为容易误导,让人觉得每个{#xxx}都会创建新的block。实际上 if就不会。 组操作必须有以/开头的闭合代码,当然,也有例外 else 和elseif 和if共享闭合代码。下面着重内部已经实现的块操作/组操作

each

这个是基本需求了吧 ,前面也提到了,它会创建新的block,scope也变成了它的参数的值。下面主要讲一下他的参数 each的参数有两种模式 1、单一属性 即$xxx。 这里所有的单一属性都是指上面对于属性的描述,允许. ~ ^操作。 2、as 模式 。这种模式类似php的foreach 形式为:$xxx($aa=>$bb)。 $aa代表迭代索引 可以随意指定,$bb是迭代项名字也可以随意指定。不过引用属性和方法是必须得加上他的名字了即 {$bb.xxx} {@bb.func}。 实例:

var data={
    a:{name:'张山'},
    b:{name:'李师'}
};
Whisker.render('{#each $($key=>$val)} {$key},{$val.name} {/each}',data);//上面也说了 $代表当前scope本身 即data

if else elseif

额这个不用过多解释了吧 说明下参数支持表达式 和上面的{%}一样

P.S.对if做了优化,建议多在最外层使用if 因为能立即判断if的条件 因此如果false可以直接忽略if 里的代码,效率很高。 示例见习面的repeat

repeat

这个跟each相似,不过他只是简单的重复,而且它不会创建新的block。但是每次循环会创建新的scope scope包含两个值 一个是 $INDEX 代表循环索引从0递增的数字,另一个是$SEQ 代表循环序号,从1递增的数字 参数:数字常量,或者单一属性 {$xxx} 不支持表达式 示例

var data={
    num:6,
    list:['1-3','4','5-6']
}
var tmpl='{#repeat $num}'                                       +
             '{#if $INDEX<3}{$SEQ} in range:{$list.0}\n'        +
             '{#elseif $INDEX==3}{$SEQ} in range:{$list.1}\n'   +
             '{#else}{$SEQ} in range:{$list.2}\n'               +
             '{/if}'                                            +
         '{/repeat}';
Whisker.render(tmpl,data);
/*result
"1 in range:1-3
2 in range:1-3
3 in range:1-3
4 in range:4
5 in range:5-6
6 in range:5-6
"
*/

导入模板

{<partials} 类似于include。导入一个外部的模板代码,有利于模板的组织和分离。和直接将外部的模板代码直接替换到该位置一模一样。 不过需要给render传入第三个参数及外部模板对象,以键值对形式,partials对应该对象的键名 实例

var data=[1,2,3];
Whisker.render('{#each $}{<out}{/each}',data,{
    out:'<div>{$}</div>'
});
 

自定义界定符

通过 Whisker.setDelimeter(start,end)函数,定义界定符,start为左界定符,end为右界定符 如

   Whisker.setDelimeter('#','#');//单字符中,只有#$xxx#不容易被误解析 其他的都容易出现在js表达式中
   Whisker.setDelimete('<%','%>');//定义双字符界定符时,注意别和后端模板语言冲突
   Whisker.setDelimeter('<?php','?>')//错误!不支持超过两个字符的界定符 

renderSimple

有时候,模板并不需要非常复杂的逻辑 只需要简单的替换某些变量,这样再用解析式render就有点儿大材小用了,所以本着量体裁衣的原则,whisker提供了简单的替换渲染 renderSimple函数接受参数和render一样(不包含partials) 但是只支持{$property}属性 且属性不支持任何操作 (如. ~ ^) 另外,界定符({})可以自定义 使用setDelimeter即可

Whisker.setDelimeter('<?php','?>');
Whisker.renderSimple('<div><?php$abc?></div>',{abc:'exolution'});
//result <div>exolution</div>

P.S. renderSimple的界定符设置没有字符数限制 所以setDelimeter设置多于两个字符的界定符时 会对render无效(会发出警告 console.warn) 而对renderSimple有效 (这点设定虽然看起来有些不合理 其实是妥协兼顾于 界定符的一致性和多样性 有什么好的建议 请联系我)

配置项

目前配置项主要由两个 第一个是界定符 第二个是格式化输出

Whisker.setFormat(true);//启用格式化输出。 目前格式化的结果是 去掉模板块左右的回车和多余缩进 
Whisker.config('delimeter','<%','%>');//同时可以以这种形式进行配置
Whisker.config('format',true);

扩展性

以内部each是实现为例进行扩展性的介绍。目前只支持块操作的扩展,像if else 这种分支控制的扩展涉及很多内部的东西,没有想到好的方式扩展,不过分支控制,除了 if else 也没啥其他的了吧。 不过目前的扩展简单性还需要琢磨 (其实这已经是抽取出来的了,掩盖了很多底层细节了)

BlockManager.register('each', function (context, block) {
        //context 解析的上下文对象 用户主要使用他的三个方法 eval resolveResult和throwError 下面会逐一介绍
        //block就是当前这个each的block对象。包含一些block相关的信息。
        //block.blockArgs 这个block的参数即each 后面的参数
        //block.blockScope 这个block的scope block创建时继承于上层block的scope
        //由用户决定是否创建新的blockscope,如果是允许嵌套的结构一定要设置新的blockscope哦
        var result = '';//结果
        //参数处理 主要将$aaa=>$bbb解析出来
        var params = /^\$([a-zA-Z0-9_.]*)(?:\(\$([a-zA-Z0-9_]+)=>\$([a-zA-Z0-9_]+)\))?$/.exec(block.blockArgs);
        
        if (params) {//参数符合规范
            //设置当前block的scope context.eval对
            block.blockScope = context.eval(block.blockScope, params[1]);
            
            if (params[2]) {
                var key = params[2];
                var val = params[3];
            }
            
            var list = block.blockScope;
            //开始循环当前的scope
            if (list) {
                if (list.length > 0) {//判断是数组还是键值对 //这一点有一定缺陷 
                    for (var i = 0; i < list.length; i++) {
                        if (key) {
                            var scope = {};//创建每次迭代的scope
                            scope[key] = i;
                            scope[val] = list[i];
                        }
                        else {
                            scope = list[i];
                        }
                        //链接每次迭代的输出 resolveBlock函数会以scope作为当前scope输出block的结果。
                        result += context.resolveBlock(block, scope);
                    }
                }
                else {
                    for (var k in list) {
                        if (key) {
                            scope = {};
                            scope[key] = k;
                            scope[val] = list[k];
                        }
                        else {
                            scope = list[k];
                        }
                        result += context.resolveBlock(block, scope);
                    }
                }
            }
        }//参数不合法 跑出错误
        else context.throwError('can\'t resolve arguments of {each}:"' + block.blockArgs + '"');
        return result;//返回最终结果
    });

写到这突然发现,这所谓的扩展好复杂啊。还得隐藏底层细节。回去继续重构~ 不过不耽误使用(将在v0.4中重构)

下一步计划

  • 1、重构,优化用户扩展性
  • 2、专注性能~ 之前的简单测试中性能在mustache之下在handlebar之上
  • 3、预编译,其实我的模式现在编译和计算结构本来就分离的。原理就是把整个模板编译成一个block链组成的结果集。可以考虑下把这个结果集持久化,然后直接求值,提高效率。
  • 4、配置化 定制化。 目前想一些格式化功能和一些无关痛痒的功能可以选配。(已完成一部分)

自述

最早是想做一个类似SSH (Struts2 Spring Hibernate) Nodejs server框架。为了支持Action,所以就想写个模板引擎。 使用{$xxx}其实是模仿EL表达式的,跟mushtache和handlebar没啥关系,反倒觉得他们{{}}好奇怪,好麻烦啊。可能是更好解析,但是{}只要规范控制也会好的解析和分离啊,虽然说{$xx}这种js是不会报错的,但没人会这么写吧,如果你转牛角尖我也没什么办法,╮(╯_╰)╭(由于属性名有严格限制所以{$abc:1} {$abc=1}这一类的都不会被错误的识别成属性)。其实最容易出问题的反而是正则表达式/[{$abc}]/。不过只要稍微改变下写法就能避免。 当然了,我还是参考了mustache和handlebar的优点的,但是懒得研究他们的代码,不过还是取了whisker(络腮胡子)这个名字,和他们保持队形 ^_^。 目前我已经在我的各种项目中应用whisker。如果你有什么好的建议或者意见,或者发现了一些BUG,请开一个issue或者mail我

exolution#163.com 谢谢!

change log

  • v0.3.9 修复表达式计算的bug(之前没有延迟求值) renderSimple加入界定符
  • v0.3.8 增加一个新的api renderSimple 用于快速替换模板中的属性({$xxx}) 并修复一些BUG
  • v0.3.7 bug fix
  • v0.3.6 增加界定符自定义和配置项(目前可配置是否美化格式化输出结果)
  • v0.3.5 增加 导入外部模板功能(partials)
  • v0.3.4 优化 if语句。如果if中不含表达式 则快速判断结果 不再使用eval
  • v0.3.3 establish. first add to git

额 下面蹩脚的英文可以无视之~

Whisker.js--Logic-less and extensible javascript template engine

Why Whisker.js?

Simple &Concise

Template code like this

{$property}
{#block}{/block}
{@method}
{%exp}

Have you had enough of {{}} ?

Logic-less but not non-logic

Logic-less help decoupling the Logic and View , making code clearly. But excessive logic-less can't be satisfied the demand. so Whisker.js provides a certain logicality such as expression of "if"

extansible

this code write in a extansible way.Several layers for extend are supported;

Getting started

Below is quick example how to use whisker.js:

var abc

Package Sidebar

Install

npm i whisker

Weekly Downloads

1

Version

0.3.9

License

MIT

Last publish

Collaborators

  • exolution