平时各种模板用的很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操作(用这个模式实现起来好像不怎么方便)
那么这样实现的模板怎么样?
在后端渲染页面来展示的方式有点像漫画:看完一页翻一页;每次改变数据刷一次页面有点像动画:一帧一帧地动。
参考资料: