平时各种模板用的很HIGH,但是当有一天遇到一个特殊需求,貌似现在的各种模板都不是那么好用,那么就不得不系统地思考一下模板语言应该如何设计~
做过WEB开发的JAVAER可能都这么干过:
String data = "<body>hello world</body>";
OutputStream outputStream = response.getOutputStream();
response.setHeader("content-type", "text/html;charset=UTF-8");
outputStream.write(data.getByte("UTF-8"));
简单的输出没问题,稍微复杂一点就不行了:
在使用Velocity之后情况有了明显的好转,当你想输出一个列表的时候可以这么干:
<ul> #foreach($i in $list) <li>$i</li> #end </ul>
这种写法非常直观以至于我现在都用它来渲染SQL语句,如果你也想这么玩需要自己定义ResourceLoader来自定义资源加载。编写Velocity模板时只需要记住两个关键点:
#开头$开头(恰好你也在用jQuery的话会产生冲突)而其他的部分都会append到输出,使用时如果每次都对模板进行解析那速度估计就跟蜗牛一样,正确的姿势应该是编译并缓存(这些内容与主题关系不大就不说了)。
其中语法设计的核心思想是:
使用HTML(甚至普通文本)中很少用到的字符来区分语法结构
学习成本很低,但是单纯地使用它来编写一些复杂的逻辑还是很痛苦的事情。不过相比较来看FreeMarker的写法更让人难受,加个<有啥意义么(估计会被喷):
<ul> <#list list as i> <li>i</li> </#list> </ul>
类似的模板引擎还有CommonTemplate、HTTL等。虽然这些技术已经将性能提到到一个很高的水准,但在后端处理页面展示还是非常局限:
只能每次都获取全部的数据并把页面渲染一遍
浪费服务器资源不说,体验也很差,真是出力不讨好!
在JavaScript的世界里也有与Velocity相似的模板技术(更多可以看这里):
<h3>
<% if (typeof content === 'string'){ %>
<%= content %>
<% } %>
</h3>
从Java转JavaScript可能觉得这种方式很好用:在数据发生变化的时候执行一下render然后替换掉原先改位置的DOM结构就可以了~ 但是能不能更进一步:
DOM结构随着数据的变化而自动跟着变化
看起来是终极目标,貌似Angular完美地实现了:
Your name: <input type="text" ng-model="yourname" placeholder="World"> <hr> My name: <input type="text" ng-model="yourname" placeholder="World">
执行的效果为:
两个输入框的内容,你随便改变哪一个,另一个都会随之变化,这就是双向绑定(也许你已经注意到ng-model了),当你需要输出列表时要用到ng-repeat:
<ul>
<li ng-repeat="o in question.options">
<b>\.</b>
<input type="radio" name="optcheck" />
\
</li>
</ul>
这种思想非常先进,和之前出现的模板都是不一样的:
之前的模板都是静态的,像一锤子买卖,而Angular的模板是动态的!
刚开始接触前端的时候也想过这个问题,但是实在没有想出来应该如何定义这样的模板语言,而Angular则已经实现了,但是代价就是限制多、门槛高:
可能大家不明白为什么门槛高:当你在适合Angular的例子上操作的时候上手非常容易,但是实现复杂的功能需要熟悉很多不那么直观的概念。还有一点比较不喜欢的是:
将展示和数据完全分开,甚至模板与展示相关的判断逻辑也分开!
这样确实能保持模板的简洁,但是总体上是否简洁、直观就不好说了。甚至连双向绑定这么好的卖点都有时候会被吐槽:
在页面复杂的时候双向数据绑定的行为可能是预测不出来(这点保留意见,没有深入玩过)。
接着我们来看下最近红得发紫的React,网上已经有很多它与Angular的比较,有些还是有点道理的。使用React的第一关是JSX语法:
var root =(
<ul className="my-list">
<li>First Text Content</li>
<li>Second Text Content</li>
</ul>
);
看起来就是将HTML代码嵌入到JavaScript中,看起来很怪但是也比较容易理解,而真正得到的代码如下:
var root = React.createElement(
"ul",
{ className: "my-list" },
React.createElement("li", null, "First Text Content"),
React.createElement("li", null, "Second Text Content")
);
当然你也可以在babel在线工具来体验这种语法。
看起来很美好,但实际上也不能太任性:模板的作用仅在于映射!控制语句for等是不能使用(多用babel玩一下就能体会到从JSX到JS之间的转换有多简单):
思路有点像Angular那样去扩展HTML原有的东西(Angular扩展的是Attribute,而React进一步扩展了Element)!
在用React的时候需要过的第二关是生命周期,讲道理的话生命周期这种东西应该越简单越合理,然而并不是这样:
| 状态 | 含义 |
|---|---|
| Mounting | 已插入真实 DOM |
| Updating | 正在被重新渲染 |
| Unmounting | 已移出真实 DOM |
为每个状态配了两个处理函数:will函数在进入状态之前调用,did函数在进入状态之后调用:
另外提供两个特殊状态的处理函数:
| 函数 | 作用 |
|---|---|
| componentWillReceiveProps | 已加载组件收到新的参数时调用 |
| shouldComponentUpdate | 组件判断是否重新渲染时调用 |
大家都在讲React很快、非常快,这就是第三关的虚拟DOM:真实的DOM操作代价太大,在render会先操作内存中的DOM结构,然后最小化反映到真正的DOM上(DomDiff算法可以在这里感受下)。
在上面我们看到的各种办法把逻辑与HTML代码分开,如果是JS的话:
HTML的
<和JavaScript的{其实已经天然地起到了这个作用!
下面这段代码不用说也应该可以猜到输出应该是什么吧:
<ul>
for(var i = 0; i < 10; i++){
<li>${i}</li>
}
</ul>
当用组件来搭建一个页面的时候可以是这样(包含嵌套的逻辑):
@xxxxxx
@yyyyyy
...
@yyyyyy
...
@yyyyyy
...
用过MarkDown或者Jade或者Python的同学可能对这种方式已经比较熟悉了,都没用过的话可以对比一下几种层级表示方式:
使用组件时需要设置一些属性来控制其行为:
@xxxxx(name="TEST" style= list=ajax("/url.do"))
另外如果可以在模板中直接编写JavaScript代码就更灵活了:
@xxxxx
@on(init)
this.name = "TEST";
this.style = {color:"white"};
....
然后结合React的精华:
最小化DOM操作(用这个模式实现起来好像不怎么方便)
那么这样实现的模板怎么样?
在后端渲染页面来展示的方式有点像漫画:看完一页翻一页;每次改变数据刷一次页面有点像动画:一帧一帧地动。
参考资料: