2011年7月23日

tumblr自定义风格系统的实现

要实现类似tumblr的自定义风格系统,首先需要一个模板引擎。 该模板引擎需要满足以下需求:

  1. 语法足够简单,最好只支持variable、block元素,不要支持循环。由于要对所有用户开放,支持循环的模板存在安全问题,可能遭到攻击,因为用户可以构造一个死循环的模板。

  2. 数据不通过${对象.属性}的方式来访问,这可能会造成普通用户理解困难,数据是单层的。比如日志标题就是${Title},而不是${post.title}

这样的话,freemarker、velocity虽然功能强大,却不符合上面两点,就不适合在这里使用了。

不过还是有很多轻量级的模板引擎可供选择的,比如MiniTemplator、Mustache,这两个引擎都有多种语言的实现。

其中MiniTemplator的语法更接近tumblr,稍加修改就能改成和tumblr一样的语法,正好符合我们的要求。MiniTemplatorde的解析部分基本可以直接使用,无需做太大修改。

由于tumblr模板的数据是单层的,因此同一个变量在不同的块级元素下可能需要填充不同的数据,比如${Title}在post的block中就是日志的标题,而在顶级的block中就是博客的标题。

MiniTemplator只提供了最基本的数据填充方法,因此需要自己实现一套处理上下文相关的数据填充框架。对这种数据的填充逻辑必然是和具体的数据定义耦合在一起的:

/**  
  * 数据填充接口  
  *  
  */  
 public interface Data {  
     /**  
     * 该block是否需要处理  
     * @param blockName  
     * @return  
     */  
     public boolean needProcessBlock(String blockName);  
     /**  
     * 填充该block数据  
     * @param xpath  
     * @param blockName  
     * @param blockNo  
     * @param miniTemplator  
     */  
     public void process(String xpath, String blockName, int blockNo, MiniTemplator miniTemplator);  
     /**  
     * 获取该变量的值  
     * @param key  
     * @return  
     */  
     public Object getValue(String key);  
 }  

process是个递归的过程,调用process("","",0,templator)就完成对模板的数据填充:

public void process(String xpath, String curBlockName, int curBlockNo, MiniTemplator miniTemplator) {  
     List blockNos = miniTemplator.getSubBlocks(curBlockNo);  
     for (int blockNo : blockNos) {  
         String blockName = miniTemplator.getBlockName(blockNo);  
         if (needProcessBlock(blockName)) {  
             process(xpath, blockName, blockNo, miniTemplator);  
         }  
     }  
     List variables = miniTemplator.getSubVariables(curBlockNo);  
     for (String variable : variables) {  
         miniTemplator.setVariable(variable, String.valueOf(getValue(variable)));  
     }  
     miniTemplator.addBlockByNo(curBlockNo);  
 }  

getValue是个类似回溯的过程,根据数据填充逻辑的复杂度,可以实现多个Data接口,比如GlobalData表示全局数据,PostData表示日志数据,PostData会保存对GlobalData的引用,调用PostData的getValue时,如果postData无法处理该值,就调用GlobalData的getValue。

Appearance Options是tumblr模板的一个特色,可以给模板的使用者提供一些选项,模板根据用户的设置会显示成不同的效果,比如就是一个条件值。

虽然minitemplator本身提供条件判断,但是如果把这些条件值在初始化minitemplator的时候传给TemplateSpecification,就会导致模板的结构和用户的数据交织在一起,这样每个用户都会有一个minitemplator的实例,给缓存模板带来麻烦。

因此还是使用和tumblt一样的语法,把这些参数都作为数据,和templator的解析区分开:
如果if:Show Likes的选项会真,就设置数据IfShowLikes为true,IfNotShowLikes为false,{block:IfShowLikes}{/block:IfShowLikes}中内容会被渲染,{block:IfNotShowLikes}{/block:IfNotShowLikes}中内容不会被渲染。

Appearance Options的定义也单独解析:

(?i)<meta\s+name=\"(color|font|if|image|text|customcss):([^"]+)"\s+content="([^"]*)"

只要再对MiniTemplator做一些扩展(主要是添加一些接口),这样就基本上实现tumblr的自定义风格模板系统,只要针对自己的需求添加一些处理各种块数据的逻辑就可以了。

使用这样的自定义风格系统,所有的数据都是在服务器端填充的,因此数据的加载速度是很重要的。
需要用一些方法来加速页面的加载:

  1. 服务器端对于相互间没有依赖的数据的获取,应该使用并行的方式来查询数据,这样可以成倍的减少加载时间。

  2. 对某些特殊的数据块,服务器不直接填充数据,而是填充js代码,当页面加载完成后,再异步加载这些数据并显示。

  3. 对某些用户数据,可是适当的使用本地缓存。

由于tumblr允许用户上传自己的html模板,因此防范恶意代码也是相当重要的。tumblr的cookie是放在www.tumblr.com下的,因此可以防止个人主页窃取cookie。

但是在个人主页加入xss代码似乎还是比较容易的。安全方面不熟悉,就不多说了。