专栏名称: Python开发者
人生苦短,我用 Python。伯乐在线旗下账号「Python开发者」分享 Python 相关的技术文章、工具资源、精选课程、热点资讯等。
目录
相关文章推荐
Python中文社区  ·  牛回速归!央行降息降准,万亿重磅利好来袭 ·  4 天前  
Python爱好者社区  ·  来了!GPT4o接入个人微信! ·  2 天前  
Python爱好者社区  ·  yyds!Kaggle教程 ·  3 天前  
Python爱好者社区  ·  裁员了,很严重,大家做好准备吧... ·  1 周前  
51好读  ›  专栏  ›  Python开发者

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

Python开发者  · 公众号  · Python  · 2017-02-06 20:17

正文

(点击上方蓝字,快速关注我们)


来源:伯乐在线专栏作者 - mozillazg 

如有好文章投稿,请点击 → 这里了解详情

如需转载,发送「转载」二字查看说明


上篇文章 中我们的模板引擎实现了对 if 和 for 对支持,同时在文章的最后我给大家留了一个 问题:如何实现支持 include 和 extends 的标签功能。


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


include


include 标签对语法是这样的:假设有一个 item.html 模板文件,它的内容如下:


  • {{ item }}

  • 还有一个我们要渲染的模板 list.html 内容如下:


        {% for item in items %}

          {% include "item.html" %}

        {% endfor %}


    渲染 list.html 后的结果类似:


        

    • item1

        

    • item2

        

    • item3


    从上面可以看出来 include 标签的作用类似使用 include 所在位置的名字空间 渲染另一个模板然后再使用渲染后的结果。所以我们可以将 include 的模板文件 当作普通的模板文件来处理,用解析那个模板生成后的代码替换 include 所在的位置, 再将结果追加到 result_var 。 生成的代码类似:


    def func_name():

        result = []

     

        # 解析 include 的模板

        def func_name_include():

            result_include = []

            return ''.join(result_include)

        # 调用生成的 func_name_include 函数获取渲染结果

        result.append(func_name_include())

     

        return ''.join(result)


    生成类似上面的代码就是 include 的关键点,下面看一下实现 include 功能 都做了哪些改动 (可以从 Github 上下载 template3a.py):


    class Template:

     

        def __init__(self, ..., template_dir='', encoding='utf-8'):

            # ...

            self.template_dir = template_dir

            self.encoding = encoding

            # ...

     

        def _handle_tag(self, token):

            """处理标签"""

            # ...

            tag_name = tag.split()[0]

            if tag_name == 'include':

                self._handle_include(tag)

            else:

                self._handle_statement(tag)

     

        def _handle_include(self, tag):

            filename = tag.split()[1].strip('"\'')

            included_template = self._parse_another_template_file(filename)

            # 把解析 include 模板后得到的代码加入当前代码中

            # def __func_name():

            #    __result = []

            #    ...

            #    def __func_name_hash():

            #        __result_hash = []

            #        return ''.join(__result_hash)

            self.code_builder.add(included_template.code_builder)

            # 把上面生成的代码中函数的执行结果添加到原有的结果中

            # __result.append(__func_name_hash())

            self.code_builder.add_line(

                '{0}.append({1}())'.format(

                    self.result_var, included_template.func_name

                )

            )

     

        def _parse_another_template_file(self, filename):

            template_path = os.path.realpath(

                os.path.join(self.template_dir, filename)

            )

            name_suffix = str(hash(template_path)).replace('-', '_')

            func_name = '{}_{}'.format(self.func_name, name_suffix)

            result_var = '{}_{}'.format(self.result_var, name_suffix)

            with open(template_path, encoding=self.encoding) as fp:

                template = self.__class__(

                    fp.read(), indent=self.code_builder.indent,

                    default_context=self.default_context,

                    func_name=func_name, result_var=result_var,

                    template_dir=self.template_dir

                )

            return template


    首先是 __init__ 增加了两个参数 template_dir 和 encoding:


    • template_dir: 指定模板文件夹路径,因为 include 的模板是相对路径所以需要这个 选项来获取模板的绝对路径

    • encoding: 指定模板文件的编码,默认是 utf-8


    然后就是 _parse_another_template_file 了,这个方法是用来解析 include 中 指定的模板文件的,其中的 func_name 和 result_var 之所以加了个 hash 值 作为后缀是不想跟其他函数变量重名。


    _handle_include 实现的是解析 include 的模板, 然后将生成的代码和代码中函数的执行结果添加到当前代码中。


    下面来看一下实现的效果。还是用上面的模板文件:


    item.html:


  • {{ item }}

  • list.html:


        {% for item in items %}

          {% include "item.html" %}

        {% endfor %}


    先来看一下生成的代码:


    >>> from template3a import Template

    >>> text = open('list.html').read()

    >>> t = Template(text)

    >>> t.code_builder

    def __func_name():

        __result = []

        __result.extend(['

      \n  '])

          for item in items:

              __result.extend(['\n    '])

              def __func_name_7654650009897399020():

                  __result_7654650009897399020 = []

                  __result_7654650009897399020.extend(['

    • ',str(item),'\n'])

                  return "".join(__result_7654650009897399020)

              __result.append(__func_name_7654650009897399020())

              __result.extend(['\n  '])

          __result.extend(['\n\n'])

          return "".join(__result)


    然后是渲染效果:


    >>> print(t.render({'items': ['item1', 'item2', 'item3']}))

       

          

    • item1

       

       

          

    • item2

       

       

          

    • item3

       

       


    include 已经实现了,下面让我们一起来实现 extends 功能。


    extends


    extends 标签实现的是模板继承的功能,并且只能在第一行出现,语法如下:


    假设有一个 parent.html 文件它的内容是:


    id="header">{% block header %} parent_header {% endblock header %}

    id="footer">{% block footer %} parent_footer {% endblock footer %}


    还有一个 child.html 文件:


    {% extends "parent.html" %}

    {% block header %} child_header {{ block.super }} {% endblock header %}


    child.html 渲染后的结果:


    id="header"> child_header parent_header

    id="footer"> parent_footer


    可以看到 extends 的效果类似用子模板里的 block 替换父模板中定义的同名 block, 同时又可以使用 {{ block.super }} 引用父模板中定义的内容,有点类似 class 的继承效果。


    注意我刚才说的是: 类似用子模板里的 block 替换父模板中定义的同名 block 。


    这个就是 extends 的关键点,我们可以先找出子模板里定义的 block , 然后用子模板里的 block 替换父模板里的同名 block , 最后只处理替换后的父模板就可以了。


    暂时先不管 block.super ,支持 extends 的代码改动如下(可以从 Github 下载 template3b.py ):


    class Template:

        def __init__(self, ...):

            # extends

            self.re_extends = re.compile(r'\{% extends (?P.*?) %\}')

            # blocks

            self.re_blocks = re.compile(

                r'\{% block (?P\w+) %\}'

                r'(?P.*?)'

                r'\{% endblock \1 %\}', re.DOTALL)

     

        def _parse_text(self):

            # extends

            self._handle_extends()

     

            tokens = self.re_tokens.split(self.raw_text)

            # ...

     

        def _handle_extends(self):

            match_extends = self.re_extends.match(self.raw_text)

            if match_extends is None:

                return

     

            parent_template_name = match_extends.group('name').strip('"\' ')

            parent_template_path = os.path.join(

                self.template_dir, parent_template_name

            )

            # 获取当前模板里的所有 blocks

            child_blocks = self._get_all_blocks(self.raw_text)

            # 用这些 blocks 替换掉父模板里的同名 blocks

            with open(parent_template_path, encoding=self.encoding) as fp:

                parent_text = fp.read()

            new_parent_text = self._replace_parent_blocks(

                parent_text, child_blocks

            )

            # 改为解析替换后的父模板内容

            self.raw_text = new_parent_text

     

        def _replace_parent_blocks(self, parent_text, child_blocks):

            """用子模板的 blocks 替换掉父模板里的同名 blocks"""

            def replace(match):

                name = match.group('name')

                parent_code = match.group('code')

                child_code = child_blocks.get(name)

                return child_code or parent_code

            return self.re_blocks.sub(replace, parent_text)

     

        def _get_all_blocks(self, text):

            """获取模板内定义的 blocks"""

            return {

                name: code

                for name, code in self.re_blocks.findall(text)

            }


    从上面的代码可以看出来我们遵循的是使用子模板 block 替换父模板同名 block 然后改为解析替换后的父模板的思路. 即,虽然我们要渲染的是:


    {% extends "parent.html" %}

    {% block header %} child_header {% endblock header %}


    实际上我们最终渲染的是替换后的父模板:


    id="header"> child_header

    id="footer"> parent_footer


    依旧是来看一下实际效果:


    parent1.html:


    id="header">{% block header %} parent_header {% endblock header %}

    id="footer">{% block footer %} parent_footer {% endblock footer %}


    child1.html:


    {% extends "parent1.html" %}

    {% block header %} {{ header }} {% endblock header %}


    看看最后要渲染的模板字符串:


    >>> from template3b import Template

    >>> text = open('child1.html').read()

    >>> t = Template(text)

    >>> print(t.raw_text)

    id="header"> {{ header }}

    id="footer"> parent_footer


    可以看到确实是替换后的内容,再来看一下生成的代码和渲染后的效果:


    >>> t.code_builder

    def __func_name():

        __result = []

        __result.extend(['

    id="header"> ',str(header),'
    \n
    id="footer"> parent_footer
    \n'])

        return "".join(__result)

     

    >>> print(t.render({'header': 'child_header'}))

    id="header"> child_header

    id="footer"> parent_footer


    extends 的基本功能就这样实现了。下面再实现一下 {{ block.super }} 功能。


    block.super


    {{ block.super }} 类似 Python class 里的 super 用来实现对父 block 的引用,让子模板可以重用父 block 中定义的内容。 只要改一下 _replace_parent_blocks 中的 replace 函数让它支持 {{ block.super }} 就可以了(可以从 Github 下载 template3c.py):


    class Template:

        def __init__(self, ....):

            # blocks

            self.re_blocks = ...

            # block.super

            self.re_block_super = re.compile(r'\{\{ block\.super \}\}')

     

        def _replace_parent_blocks(self, parent_text, child_blocks):

            def replace(match):

                ...

                parent_code = match.group('code')

                child_code = child_blocks.get(name, '')

                child_code = self.re_block_super.sub(parent_code, child_code)

                new_code = child_code or parent_code

                return new_code


    效果:


    parent2.html:



    child2.html:


    {% extends "parent2.html" %}

    {% block header %} child_header {{ block.super }} {% endblock header %}


    >>> from template3c import Template

    >>> text = open('child2.html').read()

    >>> t = Template(text)

    >>> t.raw_text

    '

    id="header"> child_header  parent_header  
    \n'

     

    >>> t.render()

    '

    id="header"> child_header  parent_header  
    \n'


    到目前为主我们已经实现了现代 python 模板引擎应有的大部分功能了:


    • 变量

    • if

    • for

    • include

    • extends, block, block.super


    后面需要做的工作就是完善我们代码了。


    不知道大家有没有注意到,我之前都是用生成 html 来试验模板引擎的功能的, 这是因为模板引擎确实是在 web 开发中用的比较多,既然是生成 html 源码那就需要考虑 针对 html 做一点优化,比如去掉多余的空格,转义之类的,还有就是一些 Web 安全方面的考虑。


    觉得本文对你有帮助?请分享给更多人

    关注「Python开发者」

    看更多技术干货

    专栏作者简介点击 → 加入专栏作者 )


    mozillazg:好好学习,天天向上。

    打赏支持作者写出更多好文章,谢谢