资讯专栏INFORMATION COLUMN

让我们一起来构建一个模板引擎(二)

Anshiii / 2947人阅读

摘要:首先我们来实现对语句的支持。下面我们就一起来让我们的模板引擎的语法支持和可以从上下载可以看到,其实也是只增加了两行代码。效果就这样我们的模板引擎对的支持算是比较完善了。

在 上篇文章中我们的模板引擎实现了变量和注释功能,同时在文章的最后我给大家留了一个 问题:如何实现支持 iffor 的标签功能:

{% if user.is_admin %}
    admin, {{ user.name }}
{% elif user.is_staff %}
    staff
{% else %}
    others
{% endif %}

{% for name in names %}
    {{ name }}
{% endfor %}

在本篇文章中我们将一起来实现这个功能。

if ... elif ... else ... endif

首先我们来实现对 if 语句的支持。 if 语句的语法如下:

{% if True %}
...
{% elif True %}
...
{% else %}
...
{% endif %}

我们首先要做的跟之前一样,那就是确定匹配标签语法的正则表达式。这里我们用的是下面 的正则来匹配标签语法:

re_tag = re.compile(r"{% .*? %}")

>>> re_tag.findall("{% if True %}...{% elif True %}...{% else %}...{% endif %}")
["{% if True %}", "{% elif True %}", "{% else %}", "{% endif %}"]

然后就是生成代码了, if 语句跟之前的变量不一样那就是:需要进行缩进切换,这一点需要注意一下。

下面我们来看一下为了支持 if 标签增加了哪些代码吧(完整代码可以从 Github 上下载 template2a.py ):

class Template:

    def __init__(self, ...):
        # ...
        # 注释
        self.re_comment = re.compile(r"{# .*? #}")
        # 标签
        self.re_tag = re.compile(r"{% .*? %}")
        # 用于按变量,注释,标签分割模板字符串
        self.re_tokens = re.compile(r"""(
            (?:{{ .*? }})
            |(?:{# .*? #})
            |(?:{% .*? %})
        )""", re.X)
    
        # 生成 def __func_name():
        # ...

    def _parse_text(self):
        # ...
        for token in tokens:
            # ...
            if self.re_variable.match(token):
                # ...
            elif self.re_comment.match(token):
                continue

            # {% tag %}
            elif self.re_tag.match(token):
                # 将前面解析的字符串,变量写入到 code_builder 中
                # 因为标签生成的代码需要新起一行
                self.flush_buffer()

                tag = token.strip("{%} ")
                tag_name = tag.split()[0]
                if tag_name in ("if", "elif", "else"):
                    # elif 和 else 之前需要向后缩进一步
                    if tag_name in ("elif", "else"):
                        self.code_builder.backward()
                    self.code_builder.add_line("{}:".format(tag))
                    # if 语句条件部分结束,向前缩进一步,为下一行做准备
                    self.code_builder.forward()
                elif tag_name in ("endif",):
                    # if 语句结束,向后缩进一步
                    self.code_builder.backward()

            else:
                # ...

上面代码的关键点是生成代码时的缩进控制:

在遇到 if 的时候, 需要在 if 这一行之后将缩进往前移一步

在遇到 elifelse 的时候, 需要将缩进先往后移一步,待 elif/ else 那一行完成后还需要把缩进再移回来

在遇到 endif 的时候, 我们知道此时 if 语句已经结束了,需要把缩进往后移一步, 离开 if 语句的主体部分

我们来看一下生成的代码:

>>> from template2a import Template
>>> t = Template("""
   ... {% if score >= 80 %}
   ... A
   ... {% elif score >= 60 %}
   ... B
   ... {% else %}
   ... C
   ... {% endif %}
   ... """)
>>> t.code_builder
def __func_name():
    __result = []
    __result.extend(["
"])
    if score >= 80:
        __result.extend(["
A
"])
    elif score >= 60:
        __result.extend(["
B
"])
    else:
        __result.extend(["
C
"])
    __result.extend(["
"])
    return "".join(__result)

代码中的 if 语句和缩进没有问题。下面再看一下 render 的结果:

>>> t.render({"score": 90})
 "

A

"
>>> t.render({"score": 70})
 "

B

"
>>> t.render({"score": 50})
 "

C

"

if 语句的支持就这样实现了。有了这次经验下面让我们一起来实现对 for 循环的支持吧。

for ... endfor

模板中的 for 循环的语法如下:

{% for name in names %}
    ...
{% endfor %}

从语法上可以看出来跟 if 语句是很相似了,甚至比 if 语句还要简单。只需在原有 if 语句代码 的基础上稍作修改就可以(完整版可以从 Github 上下载 template2b.py ):

class Template:

    # ...

    def _parse_text(self):
        # ...
            elif self.re_tag.match(token):
                # ...
                if tag_name in ("if", "elif", "else", "for"):
                    # ...
                elif tag_name in ("endif", "endfor"):
                    # ...

可以看到其实就是修改了两行代码。按照惯例我们先来看一下生成的代码:

>>> from template2b import Template
>>> t = Template("""
   ... {% for number in numbers %}
   ... {{ number }}
   ... {% endfor %}
   ... """)
>>> t.code_builder
def __func_name():
    __result = []
    __result.extend(["
"])
    for number in numbers:
        __result.extend(["
",str(number),"
"])
    __result.extend(["
"])
    return "".join(__result)

render 效果:

>>> t.render({"numbers": range(3)})
"

0

1

2

"

for ... endfor 语法就这样实现了。是不是很简单??但是还没完?

相信大家都知道在 python 中 for 循环其实还支持 breakelse 。 下面我们就一起来让我们的模板引擎的 for 语法支持 breakelse (可以从 Github 上下载: template2c.py )

class Template:

    # ...

    def _parse_text(self):
        # ...
            elif self.re_tag.match(token):
                # ...
                if tag_name in ("if", "elif", "else", "for"):
                    # ...
                elif tag_name in ("break",):
                    self.code_builder.add_line(tag)
                elif tag_name in ("endif", "endfor"):
                    # ...

可以看到,其实也是只增加了两行代码。效果:

from template2c import Template

>>> t = Template("""
... {% for number in numbers %}
...    {% if number > 2 %}
...       {% break %}
...    {% else %}
...       {{ number }}
...    {% endif %}
... {% else %}
...    no break
... {% endfor %}
... """)
>>> t.code_builder
def __func_name():
    __result = []
    __result.extend(["
"])
    for number in numbers:
        __result.extend(["
   "])
        if number > 2:
            __result.extend(["
      "])
            break
            __result.extend(["
   "])
        else:
            __result.extend(["
      ",str(number),"
   "])
        __result.extend(["
"])
    else:
        __result.extend(["
   no break
"])
    __result.extend(["
"])
    return "".join(__result)

>>> t.render({"numbers": range(3)}).replace("
", "")
"         0            1            2      no break"
>>> t.render({"numbers": range(4)}).replace("
", "")
"         0            1            2            "

就这样我们的模板引擎对 for 的支持算是比较完善了。 至于生成的代码里的换行和空格暂时先不管,留待之后优化代码的时候再处理。

重构

我们的 Template._parse_text 方法代码随着功能的增加已经变成下面这样了:

def _parse_text(self):
    """解析模板"""
    tokens = self.re_tokens.split(self.raw_text)

    for token in tokens:
        if self.re_variable.match(token):
            variable = token.strip("{} ")
            self.buffered.append("str({})".format(variable))
        elif self.re_comment.match(token):
            continue
        elif self.re_tag.match(token):
            self.flush_buffer()

            tag = token.strip("{%} ")
            tag_name = tag.split()[0]
            if tag_name in ("if", "elif", "else", "for"):
                if tag_name in ("elif", "else"):
                    self.code_builder.backward()
                self.code_builder.add_line("{}:".format(tag))
                self.code_builder.forward()
            elif tag_name in ("break",):
                self.code_builder.add_line(tag)
            elif tag_name in ("endif", "endfor"):
                self.code_builder.backward()
        else:
            self.buffered.append("{}".format(repr(token)))

有什么问题呢?问题就是 for 循环里的代码太长了,我们需要分割 for 循环里的 代码。比如把对变量,if/for 的处理封装到多带带的方法里。

下面展示了一种方法(可以从 Github 下载 template2d.py ):

def _parse_text(self):
    """解析模板"""
    tokens = self.re_tokens.split(self.raw_text)
    handlers = (
        (self.re_variable.match, self._handle_variable),   # {{ variable }}
        (self.re_tag.match, self._handle_tag),             # {% tag %}
        (self.re_comment.match, self._handle_comment),     # {# comment #}
    )
    default_handler = self._handle_string                  # 普通字符串

    for token in tokens:
        for match, handler in handlers:
            if match(token):
                handler(token)
                break
        else:
            default_handler(token)

def _handle_variable(self, token):
    """处理变量"""
    variable = token.strip("{} ")
    self.buffered.append("str({})".format(variable))

def _handle_comment(self, token):
    """处理注释"""
    pass

def _handle_string(self, token):
    """处理字符串"""
    self.buffered.append("{}".format(repr(token)))

def _handle_tag(self, token):
    """处理标签"""
    # 将前面解析的字符串,变量写入到 code_builder 中
    # 因为标签生成的代码需要新起一行
    self.flush_buffer()
    tag = token.strip("{%} ")
    tag_name = tag.split()[0]
    self._handle_statement(tag, tag_name)

def _handle_statement(self, tag, tag_name):
    """处理 if/for"""
    if tag_name in ("if", "elif", "else", "for"):
        # elif 和 else 之前需要向后缩进一步
        if tag_name in ("elif", "else"):
            self.code_builder.backward()
        # if True:, elif True:, else:, for xx in yy:
        self.code_builder.add_line("{}:".format(tag))
        # if/for 表达式部分结束,向前缩进一步,为下一行做准备
        self.code_builder.forward()
    elif tag_name in ("break",):
        self.code_builder.add_line(tag)
    elif tag_name in ("endif", "endfor"):
        # if/for 结束,向后缩进一步
        self.code_builder.backward()

这样处理后是不是比之前那个都放在 _parse_text 方法里要好很多?

至此,我们的模板引擎已经支持了如下语法:

变量: {{ variable }}

注释: {# comment #}

if 语句: {% if ... %} ... {% elif ... %} ... {% else %} ... {% endif %}

for 循环: {% for ... in ... %} ... {% break %} ... {% else %} ... {% endfor %}

之后的文章还将实现其他实用的模板语法,比如 include, extends 模板继承等。

include 的语法(item.html 是个独立的模板文件, list.html 中 include item.html):

{# item.html #}
  • {{ item }}
  • {# list.html #}
      {% for name in names %} {% include "item.html" %} {% endfor %}

    list.html 渲染后将生成类似下面这样的字符串:

    • Tom
    • Jim

    extends 的语法(base.html 是基础模板, child.html 继承 base.html 然后重新定义 base.html 中定义过的 block):

    {# base.html #}
    
    {% block content %} parent_content {% endblock content %}
    {% block footer %} (c) 2016 example.com {% endblock footer %}

    child.html:

    {% extends "base.html" %}
    
    {% block content %}
        child_content
        {{ block.super }}
    {% endblock content %}

    child.html 渲染后将生成类似下面这样的字符串:

    child_content parent_content
    (c) 2016 example.com

    那么,该如何实现 includeextends 功能呢? 我将在 第三篇文章 中向你详细的讲解。敬请期待。

    文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

    转载请注明本文地址:https://www.ucloud.cn/yun/37840.html

    相关文章

    • 我们起来构建模板引擎(四)

      摘要:在本文中我们将解决一些用于生成的模板引擎需要面对的一些安全问题。整个系列的所有文章地址让我们一起来构建一个模板引擎一让我们一起来构建一个模板引擎二让我们一起来构建一个模板引擎三让我们一起来构建一个模板引擎四文章中涉及的代码已经放到上了 在 上篇文章 中我们的模板引擎实现了对 include 和 extends 的支持, 到此为止我们已经实现了模板引擎所需的大部分功能。 在本文中我们将解...

      yuxue 评论0 收藏0
    • 我们起来构建模板引擎

      摘要:使用技术我们将使用将模板编译为代码的方式来解析和渲染模板。下面我们就一起来实现这个方法。 假设我们要生成下面这样的 html 字符串: welcome, Tom age: 20 weight: 100 height: 170 要求姓名以及 中的内容是根据变量动态生成的,也就是这样的: welco...

      zombieda 评论0 收藏0
    • 基于TmodJS的前端模板工程化解决方案

      摘要:原作者唐斌腾讯什么原名是一个简单易用的前端模板预编译工具。本文作者为来自腾讯团队的唐斌,他在本文中为我们分析了传统前端模板内嵌的弊端,如开发调试效率低下自动化构建复杂度比较高等特点,并针对目前现状给出了较好的解决方案。 原作者: 唐斌(腾讯)| TmodJS什么 TmodJS(原名atc)是一个简单易用的前端模板预编译工具。它通过预编译技术让前端模板突破浏览器限制,实现后端模板一样的同...

      zhaochunqi 评论0 收藏0
    • 我们起来构建模板引擎(三)

      摘要:在上篇文章中我们的模板引擎实现了对和对支持,同时在文章的最后我给大家留了一个问题如何实现支持和的标签功能。在本篇文章中我们将一起来动手实现这两个功能。 在 上篇文章 中我们的模板引擎实现了对 if 和 for 对支持,同时在文章的最后我给大家留了一个 问题:如何实现支持 include 和 extends 的标签功能。 在本篇文章中我们将一起来动手实现这两个功能。 include in...

      3fuyu 评论0 收藏0
    • webpack多页应用架构系列(十五):论前端如何在后端渲染开发模式下夹缝生存

      摘要:回到纯静态页面开发阶段,让页面不需要后端渲染也能跑起来。改造开始本文着重介绍如何将静态页面改造成后端渲染需要的模板。总结在后端渲染的项目里使用多页应用架构是绝对可行的,可不要给老顽固们吓唬得又回到传统前端架构了。 本文首发于Array_Huang的技术博客——实用至上,非经作者同意,请勿转载。原文地址:https://segmentfault.com/a/119000000820338...

      dinfer 评论0 收藏0

    发表评论

    0条评论

    最新活动
    阅读需要支付1元查看
    <