怎样写一个模板引擎 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
irainy
V2EX    分享创造

怎样写一个模板引擎

  •  
  •   irainy 2015-08-02 01:30:44 +08:00 3072 次点击
    这是一个创建于 3726 天前的主题,其中的信息可能已经有所发展或是发生改变。

    原文链接

    模板引擎是Web开发中通常用于动态生成网页的工具,例如PHP常用的Smarty、Python的Jinja、Node的Jade等。本文通过Python(Approach: Building a toy template engine in Python)和Js(Javascript Micro-Templating)的两个简单模板引擎项目学习怎样写一个模板引擎。

    一般模板由下面三部分组成:

    • 文本
    • 变量
    • 组块

    通常变量和代码组块由特定的分隔符标识,如:

    Hello, {{name}}! {% if role == "admin" %} <a href="/dashboard">Dashboard</a> {% end %} 

    对文本的渲染就是返回文本本身;变量和组块的渲染依赖于我们赋予变量名的值和约定的组块语法规则(如条件、循环等)。要将字符串当做变量进行求值,首先想到的是eval方法:

    name = "rainy" print("Hello, " + eval("name") + "!") # Hello, rainy! 

    许多编程语言中的eval方法用于将字符串转化成表达式进行求值,完成类似编译器本身的工作,而实质上模板引擎更像是一个针对于模板的编译器。我们知道编译器一般采用抽象语法树(AST)这种树形结构来对程序源码进行表征,如果我们将模板看作是源码,同样可以将其表征为抽象语法树,例如上面的模板文件可以表示为:

    template engine AST

    要将模板文件变成上图所示的AST结构,首先需要按照分隔符划分,例如在Python中:

    import re VAR_TOKEN_START = '{{' VAR_TOKEN_END = '}}' BLOCK_TOKEN_START = '{%' BLOCK_TOKEN_END = '%}' TOK_REGEX = re.compile(r"(%s.*?%s|%s.*?%s)" % ( VAR_TOKEN_START, VAR_TOKEN_END, BLOCK_TOKEN_START, BLOCK_TOKEN_END )) content = """Hello, {{name}}! {% if role == "admin" %} <a href="/dashboard">Dashboard</a> {% end %}""" TOK_REGEX.split(content) # OUTPUT => ['Hello, ', '{{name}}', '\n', '{% if role == "admin" %}', '\n<a href="/dashboard">Dashboard</a>\n', '{% end %}', ''] 

    构建成AST之后对每一节点逐一进行渲染(render),例如对变量的渲染可以用下面的方法:

    def resolve(name, context): for tok in name.split('.'): context = context[tok] return context class VarTmpl(): def __init__(self, var): self.var = var def render(self, **kwargs): return resolve(self.var, kwargs) tmpl = VarTmpl("name") tmpl.render(name = "rainy") #=> rainy tmpl.render(name = "python") #=> python 

    对组块的渲染稍微复杂一些但原理上类似于eval

    role = 'user' eval('role == "admin"') # OUTPUT False 

    只不过所有组块的语法和求值规则需要重新定义,有兴趣可以查看源码。下面再来看基于Js的一种解决方案。

    从上文可以看出,模板引擎的核心在于区分字符串和表达式,而表达式本身又是以字符串的形式呈现。为了实现字符串与表达式之间的切换,上面Python的版本采用eval(或者更专业点的:ast.literal_eval)。当然Js中也有与之类似的eval方法,但Js还有另外一个非常灵活的特性,在定义一个函数时,可以用下面两种方式:

    var Tmpl = function(context){ with(context){ console.log(name); } } Tmpl({name: "rainy"}); //=> rainy var raw = "name"; var Tmpl = new Function("context", "with(context){console.log("+ raw+ ");}"); Tmpl({name: "rainy"}); //=> rainy Tmpl({name: "js"}); //=> js 

    也就是说我们可以通过new Function()的方法实现字符串向表达式的转化,结合上文提到的分割-求值-重组的步骤,我们再来看John Resig的简化版本:

    (function(){ this.tmpl = function tmpl(str, data){ var fn = new Function("obj", "var p=[];"+ "with(obj){p.push('" + str .replace(/[\r\t\n]/g, " ") // 去掉了单引号处理部分,简化版本中模板文件中暂时不能出现单引号; .split("<%").join("\t") .replace(/\t=(.*?)%>/g, "',$1,'") .split("\t").join("');") .split("%>").join("p.push('") + "');}return p.join('');"); return data ? fn( data ) : fn; }; })(); console.log(tmpl("Hello, <%=name%>!", {name: "rainy"})); // OUTPUT "Hello, rainy!" 

    在这段15行不到的(微型)模板引擎中,首先还是根据约定的分隔符将模板分割:

    var str = "Hello, <%=name%>!"; str = str.split("<%").join("\t"); //=> 'Hello, \t=name%>!' str = str.replace(/\t=(.*?)%>/g, "',$1,'"); //=> 'Hello, \',name,\'!' 

    注意这一行是在new Function()的定义中,相当于:

    function fn(str, data){ var p = []; with(data){ p.push('Hello, ',name,'!'); // p === ['Hello, ', name, '!']; }; } 

    而在with(data){}作用范围内,name === data.name,因此得到:

    p === ['Hello, ', 'rainy', '!']; p.join('') === "Hello, rainy!"; 

    以上就是这一微型模板引擎的核心部分,如果需要处理单引号的问题,可以在str处理过程中加上:

    str .replace(/[\r\t\n]/g, " ") .replace(/'/g, "\r") // 全部单引号替换为\r .split("<%").join("\t") .replace(/\t=(.*?)%>/g, "',$1,'") .split("\t").join("');") .split("%>").join("p.push('") .replace(/\r/g, "\\'") // 置换回单引号 

    总结

    表面上看来模板引擎复杂的地方是抽象语法树的构建和操作,但实际上其核心问题在于变量名和值的区分,也就是程序和数据的区分。而有趣的是,在Lisp语言中,“数据即程序、程序即数据”,它们之间并无本质差异,有兴趣可以展开阅读一下这篇文章:The Nature of Lisp。模板引擎非常实用,从实用性出发深入探索,一不小心拓展到其它领域,这才是programming最大的乐趣所在:D

    参考

    2 条回复    2015-08-02 15:12:38 +08:00
    Septembers
        1
    Septembers  
       2015-08-02 02:14:17 +08:00 via Android
    用正则做模板引擎不适合生产用 做做原型还可以
    一本正经做模板引擎还是需要自己写NFA
    tpwow
        2
    tpwow  
       2015-08-02 15:12:38 +08:00 via Android
    不错。学习了
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     5463 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 30ms UTC 07:29 PVG 15:29 LAX 00:29 JFK 03:29
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86