130行代码写一个模板引擎

130行代码写一个模板引擎简单来说,模板引擎就是先定义好一个模板,然后喂它数据,就会生成对应的html结构。 这样就体现了数据与视图分离的思想,以后修改任何一方都是很方便的。 参数分别对应模板和数据,而返回值对应最终的html。函数原型如下: 由于我在工作中经常用到Django开发,所以我对Django…

我理解的模板引擎

简单来说,模板引擎就是先定义好一个模板,然后喂它数据,就会生成对应的html结构。 模板是一段预先定义好的字符串,是一个类html的结构,里面穿插着一些控制语句(if、for等), 比如如下:

<p>Welcome, {{ user_name }}!</p>

{% if is_show %}
    Your name: {{ user_name }}
{% endif %}

<p>Fruits:</p>
<ul>
{% for product in product_list %}
    <li>{{ product.name }}:{{ product.price }}</li>
{% endfor %}
</ul>

数据是json数据,喂的数据不同,生成的html也不同,如

{
    'user_name': 'Jack',
    'is_show': True,
    'product_list': [
        {
            'show': True,
            'name': 'Apple',
            'price': 20
        },
        {
            'show': False,
            'name': 'Pear',
            'price': 21
        },
        {
            'show': True,
            'name': 'Banana',
            'price': 22
        }
    ]
}

就会生成如下所示的html:

<p>Welcome, Jack!</p>
    Your name: Jack
<p>Fruits:</p>
<ul>
    <li>Apple:20</li>
    <li>Banana:22</li>
</ul>

这样就体现了数据与视图分离的思想,以后修改任何一方都是很方便的。

要做的事

我们要做的事情就是根据已知的模板和数据来生成对应的html,于是我们可以定义这样一个函数,该函数有两个参数和一个返回值, 参数分别对应模板数据,而返回值对应最终的html。函数原型如下:

def TemplateEngine(template, context):
    ...
    return html_data

template是str类型,context是dict类型,html_data也是str类型

支持的语法

由于我在工作中经常用到Django开发,所以我对Django的模板引擎比较熟悉,这里就采用Django支持的语法来讲解。其实说白了,大体上就 两种语法,{{ }}{% %}{{ }}里面包含的是一个(变)量,数据来自于context,整个会被context里面对应的数据替换掉, 如前面的例子{{ user_name }}最终会被替换成Jack。{% %}是控制结构,有四种:{% if %}{% for %}{% endif %}{% endfor %}{% if %}{% endif %}必须成对出现,同理,{% for %}、{% endfor %}也必须成对出现。

实现思路

大体上实现一个模板引擎有三种方法,替换型、解释型和编译型。替换型就是简单的字符串替换,如{{ user_name }}被替换成Jack,对应 如下代码:

'{user_name}'.format(user_name = 'Jack')

这种最简单,一般来说运行效率也最低。解释型和编译型都是生成对应的(python)代码,然后直接运行这个代码来生成最终的html,这个实现难度 相对替换型来说复杂了一点。本篇先只讲替换型。

总的思路是这样:我们从最外层按照普通字符串、{{ }}、{% %}将模板切块,然后递归处理每一个块,最后将每一个子块的结果拼接起来。 关键词依次是:切块、递归处理、拼接。我们依次来讲解每个步骤。

切块

还是举前面那个例子,

<p>Welcome, {{ user_name }}!</p>

{% if is_show %}
    Your name: {{ user_name }}
{% endif %}

<p>Fruits:</p>
<ul>
{% for product in product_list %}
    <li>{{ product.name }}:{{ product.price }}</li>
{% endfor %}
</ul>

为了处理方便,我们把模板切得尽可能的碎,使得每一个小块都是普通字符串块、{{ }}块、{% if %}块、{% endif %}块、{% for %}块、{% endfor %}块中的 一种,如上述模板切成:

['<p>Welcome, ', '{{ user_name }}', '!</p>', '{% if is_show %}', 'Your name: ', '{{ user_name }}', '{% endif %}', '<p>Fruits:</p><ul>', '{% for product in product_list %}', '<li>', '{{ product.name }}', ':', '{{ product.price }}', '</li>', '{% endfor %}', '</ul>']

要把模板(str类型)切成如上图所示(list类型),读者立马会想到使用split函数,没错。但是这里最好使用正则表达式的split函数re.split,代码如下:

tokens = re.split(r"(?s)({{.*?}}|{%.*?%})", template)

递归处理

在上一节(切块)我们已经得到了一个list,本节我们只需遍历它即可。我们遍历那个list,如果是普通块并且没有被{% if %}块和{% for %}块包围,则我们直接将值pusth到最终 的结果中;同理,如果是{{ }}块并且没有被{% if %}块和{% for %}块包围,则我们调用VarEngine来解析这个{{ }}块,并且把解析结果pusth到最终的结果中;如果是{% if %}块, 则我们先不急求值,而是将它push到一个栈中,并且将之后的块也push到这个栈中,直到遇到对应的{% endif %}块。遇到了{% endif %}块之后,我们就调用IfBlock来解析这个 栈,并且把解析结果pusth到最终的结果中;跟{% if %}块类似,如果遍历到了{% for %}块,则我们将{% for %}块push到一个栈中,然后将之后的块也push到这个栈中,直到遇到对应的 {% endfor %}块,遇到了{% endfor %}块之后,我们就调用ForBlock来解析这个栈,并且把解析结果pusth到最终的结果中。代码(剪辑后)如下:

def recursive_traverse(lst, context):
    stack, result = [], []
    is_if, is_for, times, match_times = False, False, 0, 0
    for item in lst:
        if item[:2] != '{{' and item[:2] != '{%':
            # 普通块的处理
            result.append(item) if not is_if and not is_for else stack.append(item)
        elif item[:2] == '{{':
            # {{ }}块的处理
            result.append(VarEngine(item[2:-2].strip(), context).result) if not is_if and not is_for else stack.append(item)
        elif item[:2] == '{%':
            expression = item[2:-2]
            expression_lst = expression.split(' ')
            expression_lst = [it for it in expression_lst if it]
            if expression_lst[0] == 'if':
                # {% if %}块的处理
                stack.append(item)
                if not is_for:
                    is_if = True
                    times += 1
            elif expression_lst[0] == 'for':
                # {% for %}块的处理
                stack.append(item)
                if not is_if:
                    is_for = True
                    times += 1
            if expression_lst[0] == 'endif':
                # {% endif %}块的处理
                stack.append(item)
                if not is_for:
                    match_times += 1
                if match_times == times:
                    result.append(IfBlock(context, stack).result)
                    del stack[:]
                    is_if, is_for, times, match_times = False, False, 0, 0
            elif expression_lst[0] == 'endfor':
                # {% endfor %}块的处理
                stack.append(item)
                if not is_if:
                    match_times += 1

                if match_times == times:
                    result.append(ForBlock(context, stack).result)
                    del stack[:]
                    is_if, is_for, times, match_times = False, False, 0, 0

result是一个list,是最终的结果

拼接

通过递归处理那一节,我们已经把各个块的执行结果都存放到了列表result中,最后使用join函数将列表转换成字符串就得到了最终的结果。

return ''.join(result)

各个引擎的实现

递归处理那一节,我们用到了几个类VarEngine、IfBlock和ForBlock,分别用来处理{{ }}块、{% if %}块组成的栈、{% for %}块组成的栈。 下面来说明一下这几个引擎的实现。

VarEngine的实现

先直接上代码

class VarEngine(Engine):
    def _do_vertical_seq(self, key_words, context):
        k_lst = key_words.split('|')
        k_lst = [item.strip() for item in k_lst]
        result = self._do_dot_seq( k_lst[0], context)
        for filter in k_lst[1:]:
            func = self._do_dot_seq(filter, context, True)
            result = func(result)
        return result
    def __init__(self, k, context):
        self.result = self._do_vertical_seq(k, context) if '|' in k else self._do_dot_seq(k, context)

这里主要是要注意处理.和|,|表示过滤器,.最常用的表示一个对象的属性,如

{{ person.username | format_name }}

person可能表示一个对象,也可能表示一个类实例,username是它的属性,format_name是一个过滤器(函数),表示将处理左边给的值(这里是username)并返回 处理后的值。更复杂一点的,可能有如下的{{ }}块,如

{{ info1.info2.person.username | format_name1 | format_name2 | format_name3 }}

VarEngine类继承自Engine类,_do_dot_seq在Engine类中定义:

class Engine(object):
    def _do_dot(self, key_words, context, stay_func = False):
        if isinstance(context, dict):
            if key_words in context:
                return context[key_words]
            raise KeyNotFound('{key} is not found'.format(key=key_words))
        value = getattr(context, key_words)
        if callable(value) and not stay_func:
            value = value()
        return value
    def _do_dot_seq(self, key_words, context, stay_func = False):
        if not '.' in key_words:
            return self._do_dot(key_words, context, stay_func)
        k_lst = key_words.split('.')
        k_lst = [item.strip() for item in k_lst]
        result = context
        for item in k_lst:
            result = self._do_dot(item, result, stay_func)
        return repr(result)

_do_dot函数主要是用来处理.(点)情形的,如{{ person.name }},返回结果。有三个参数:key_words、context和stay_func,key_words 是属性名,如name;context对应上下文(或对象、类实例等),如person;stay_func是如果属性是一个函数的话,是否要运行这个函数。 代码很简单,就讲到这里。

IfBlock的实现

class IfEngine(Engine):
    def __init__(self, key_words, context):
        k_lst = key_words.split(' ')
        k_lst = [item.strip() for item in k_lst]
        if len(k_lst) % 2 == 1:
            raise IfNotValid
        for item in k_lst[2::2]:
            if item not in ['and', 'or']:
                raise IfNotValid
        cond_lst = k_lst[1:]
        index  = 0
        while index < len(cond_lst):
            cond_lst[index] = str(self._do_dot_seq(cond_lst[index], context))
            index += 2
        self.cond = eval(' '.join(cond_lst))

class IfBlock(object):
    def __init__(self, context, key_words):
        self.result = '' if not IfEngine(key_words[0][2:-2].strip(), context).cond else recursive_traverse(key_words[1:-1], context)

IfBlock的逻辑也很简单,就是先判断if条件是否为真(通过IfEngine判断),如果为真,则递归下去(调用recursive_traverse),如果为假,则直接返回空 字符串。这里稍微讲下IfEngine的实现,主要是对and、or的处理,用到了eval函数,这个函数会执行里面的字符串,如eval(‘True and True and True’)会返回True。

ForBlock的实现

class ForBlock(Engine):
    def __init__(self, context, key_words):
        for_engine = key_words[0][2:-2].strip()
        for_engine_lst = for_engine.split(' ')
        for_engine_lst = [item.strip() for item in for_engine_lst]
        if len(for_engine_lst) != 4:
            raise ForNotValid
        if for_engine_lst[0] != 'for' or for_engine_lst[2] != 'in':
            raise ForNotValid
        iter_obj = self._do_dot_seq(for_engine_lst[3], context)
        self.result = ''
        for item in iter_obj:
            self.result += recursive_traverse(key_words[1:-1], {for_engine_lst[1]:item})

这里采用了Python的for语法for…in…,如{% for person in persons %},同IfBlock类似,这里也采用了递归(调用recursive_traverse)实现。

总结

本文用130行代码实现了一个模板引擎,总的思路还是很简单,无非就是依次处理各个block,最后将各个block的处理结果拼接(join)起来。关键是基本功要 扎实,如递归、正则表达式等,此外,Python的常用(内置)函数也要搞清楚,如repr、eval(虽然不推荐使用,但是要了解)、str.join、getattr、callable等等,这些 函数可以帮助你达到事半功倍的效果。 本节的源码都在github上,欢迎给个star。

今天的文章130行代码写一个模板引擎分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/23149.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注