首先不考虑实现,从用户的角度想想,这个模板应该有什么样的语法。
我们可能已经用过或见过很多模板引擎,大多数都是提供了一些特别的标记,可以让我们在HTML或其它文本中,嵌入我们自己的标签,以实现如数据展示、逻辑控制等功能。每一种都有自己独特的标记和语法定义,但实际使用起来,多数都差不多的。每种模板都提供了一些常用的标签,比如for, if, 变量显示等,都是居家必备的。
SharkDart也不例外。
SharkDart语法上的主要特点有几下几点:
@
作为特殊字符!
表示取用原始文本@
SharkDart采用了字母@
作为特殊的标识符,用于标示可由SharkDart处理的标签。这种做法参考了Rythm Template Engine和Play2的模板,而它们又参考了.net的Razor.
不过也仅此而已,其它的部分,SharkDart都有自己的特色,很快就能看到。
很多模板都为某些内置标签提供了量身定做的语法。比如for
,在Play2中是:
@for(order <- orders) {
}
这模仿了scala中的for语法。
而if-else则是:
@if(items.isEmpty) {
<h1>Nothing to display</h1>
} else {
<h1>@items.size items!</h1>
}
它跟前面的for完全不同,无法使用同一种解析规则。所以在模板的解析阶段,必须为每一个标签提供相应的解析规则。
这样做的好处是,每个标签的语法都可定制,相当灵活,缺点则是,一旦想提供自定义的标签,可能需要修改解析规则的源代码,工作量挺大的,对于扩展也不利。
在SharkDart之前我还做过一个叫RythmDart的模板引擎,里面就采用了上面的方式,为每一个标签都提供了相应的解析规则。这一次我想换种方式。
通用规则的难点在于,要想出一个结构,可以满足大部分内置标签的需要,比如如何把上面的for和if-else统一成一种格式。
经过几天的思考与推敲,我最后定义了两种标签结构(一种实在搞不定):
可接受多个参数的标签
@tagname(paramType paramVar1: paramDesc, paramType paramVar2: paramDesc) {
…
}
其中paramType和paramDesc都是可选项。即也可以为:
@tagname(paramVar1, paramVar2) {}
可接受一段代码作为参数的标签
@tagname(some code here) {
...
}
使用这两种结构,基本上可满足我想到的各内置标签的功能。比如:
@params(String name: 'Shark') {
<h1>It's all about @name</h1>
}
@extends(anotherTemplate, name: 'Freewind') {
<div>Custom content</div>
}
这些都使用了第一种结构。(注意上面的@name,表示显示变量name的值)
再比如:
@if(name=='Shark') {
Enter the ocean!
} @elseif(name=='Bird') {
Fly!
} @else {
Sleep ~~
}
使用了第二种结构。
实际上,我设计出的各内置标签(数量不多),都可以用它们实现。
另外,为了跟后面的“表达式”结构作为区分,标签必须至少有参数部分()
或主体部分{}
之一,哪怕是空的也行。
表达式是指在页面中显示一个变量或者一些调用代码的值,也是必不可少的功能。为了一致,它也使用了@
。
简单表达式,直接在前面加@
,如:
@name
@age
复杂表达式,可用@{}
包起来:
@{name.toLowerCase()}
@{users.map((u)=>“[$u]“).join(', ')}
当然单个变量也可以包:
@{name}
@
字符有了特殊的意义,那我想显示一个单纯的@
怎么办?为了解决这个问题,规定@@
会被解析为单个@
。比如:
@@name
会被解析为纯文本:
@name
有的时候,我希望标签的主体就是一段纯文本,里面的所有内容都原封不动的取出。为了实现这个功能,我定义了一个@!
,即在@
字符后加一个!
,表示主体将会被当作纯文本看待。
它不跟任何一个具体的标签相关,即可以添加到任何标签前。而标签的其它部分都跟以前一样。比如,我想定义一段Dart代码:
@!dart {
print("Hello, Shark!");
}
或者一段普通文本:
@!plainText {
I'm plainText, @@ is @@.
}
里面的@@
还是@@
,而不会被解析为@
。但如果去掉那个!
,就不一样了。
仔细看前一段,很快会发现一个问题:如果纯文本中有一个}
,会怎样?
@!dart {
var s = "}";
}
回答:dart标签的主体会提前结束,我们为dart标签取得的内容,实际上为:
var s = "
为了解决这个问题,我对标签主体两边的花括号进行了调整,允许使用多个花括号,以保证内容不会被意外截断:
@!dart {{{
var s = "}";
}}}
多长都行:
@!dart {{{{{{{{{{
// oh my God, I can't breath
}}}}}}}}}}
有些标签主体外面的花括号有点烦人,比如说@params
和@extends
。它们两个一般放在页面的最顶端,用来指定整个页面接受的参数,以及继承了哪个页面。如果有花括号,会写成:
@params(String name) {
<html>
<head>...</head>
<body>...</body>
</html>
}
及:
@extends(anotherTemp) {
<div>...</div>
<div>...</div>
<div>...</div>
}
如果去掉花括号,则可以写成:
@params(String name)
及:
@extends(anotherTemp)
这样看起来标签就像是页面顶上的一个注释,不会对主要内容产生干扰,还能省下一个不必要的缩进。
这种需求实际上对语法的解析不产生影响,因为它并不跟前面列出的规则矛盾。只是在后面将语法树翻译成dart代码时,需要考虑如何让标签决定它后面跟着的内容是标签的一部分(主体),还是排在它后面的普通内容。
经过以上几个简单的规则,SharkDart的语法就定义好了!
看起来很简单,但让我想了好几天,多次推翻才定下这种方案。中间过程不提,只能说,这真是一项特别费脑子的事情。
今天到此为止,下一篇将会尝试使用这里定义的标签通用结构设计一些常用标签,看看是否真的可行。