资讯专栏INFORMATION COLUMN

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

3fuyu / 2403人阅读

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

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

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

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_direncoding:

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

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

    然后就是 _parse_another_template_file 了,这个方法是用来解析 include 中 指定的模板文件的,其中的 func_nameresult_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(["
      "]) for item in items: __result.extend([" "]) def __func_name_7654650009897399020(): __result_7654650009897399020 = [] __result_7654650009897399020.extend(["
    • ",str(item),"
    • "]) return "".join(__result_7654650009897399020) __result.append(__func_name_7654650009897399020()) __result.extend([" "]) __result.extend(["
    "]) return "".join(__result)

    然后是渲染效果:

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

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

    extends

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

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

    
    

    还有一个 child.html 文件:

    {% extends "parent.html" %}
    {% block header %} child_header {{ block.super }} {% endblock header %}

    child.html 渲染后的结果:

    
    

    可以看到 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 (?Pw+) %}"
                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 %}

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

    
    

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

    parent1.html:

    
    

    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)
    
    

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

    >>> t.code_builder
    def __func_name():
        __result = []
        __result.extend(["
    
    "])
        return "".join(__result)
    
    >>> print(t.render({"header": "child_header"}))
    
    

    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
    "
    "
    
    >>> t.render()
    "
    "

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

    变量

    if

    for

    include

    extends, block, block.super

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

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

    至于怎么实现这些优化项,我将在 第四篇文章 中向你详细的讲解。敬请期待。

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

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

    相关文章

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

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

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

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

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

      摘要:首先我们来实现对语句的支持。下面我们就一起来让我们的模板引擎的语法支持和可以从上下载可以看到,其实也是只增加了两行代码。效果就这样我们的模板引擎对的支持算是比较完善了。 在 上篇文章中我们的模板引擎实现了变量和注释功能,同时在文章的最后我给大家留了一个 问题:如何实现支持 if 和 for 的标签功能: {% if user.is_admin %} admin, {{ user...

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

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

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

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

      dinfer 评论0 收藏0

    发表评论

    0条评论

    3fuyu

    |高级讲师

    TA的文章

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