关于python:用-Python-撸一个-Web-服务器第4章动态渲染数据

4次阅读

共计 6414 个字符,预计需要花费 17 分钟才能阅读完成。

上一章中为了尽快让 Todo List 程序跑起来,并没有齐全依照 MVC 模式编写程序。这一章就让咱们一起实现一个残缺的 MVC 模式 Todo List 程序首页。

应用模型操作数据

咱们来剖析下申请 Todo List 程序首页时,模型层须要做哪些事件。当一个申请达到首页视图函数 index 时,它须要做两件事件,首先调用模型层获取全副的 todo 数据,而后将 todo 数据动静填充到 index.html 模板中。

调用模型层获取全副的 todo 数据,只须要在模型层编写读取 todo/db/todo.json 文件数据的代码即可。在这之前,咱们须要先确定 todo 在文件中存储的格局。

Todo List 程序中 todo 须要存储的数据只有一个,就是 todo 的内容。所以咱们能够将 todo 以如下格局存储到 todo/db/todo.json 文件:

// todo_list/todo/db/todo.json

[
    {
        "id": 1,
        "content": "hello world"
    },
    {
        "id": 2,
        "content": "你好,世界!"
    }
]

这是一个规范的 JSON 格局,每一个对象代表了一条 todo,content 字段即为 todo 内容,id 作为每条数据的索引不会展现在页面中,不便咱们对数据进行排序、疾速查找等操作。

为了简化程序,我将数据存储在 JSON 文件中而不是数据库中。存储到文件的格局多种多样,但 JSON 格局是一种十分风行且敌对的数据格式,在 Python 中也可能很不便的对 JSON 格局的文件进行读写操作。

留神:

  1. JSON 文件不反对正文,所以如果你打算间接从下面示例中复制数据到 todo.json 文件时,须要去掉顶部文件名正文。
  2. 如果 todo/db/todo.json 文件内容为空,应用 Python 读取时会抛出 JSONDecodeError 异样,起码要保障其外部有一个空数组 [] 存在,能力失常读取。

确定了 todo/db/todo.json 文件数据格式,就能够编写在模型层读取 todo 数据的代码了:

# todo_list/todo/models.py

import os
import json

from todo.config import BASE_DIR


class Todo(object):
    """Todo 模型类"""

    def __init__(self, **kwargs):
        self.id = kwargs.get('id')
        self.content = kwargs.get('content', '')

    @classmethod
    def _db_path(cls):
        """获取存储 todo 数据文件的绝对路径"""
        # 返回 'todo_list/todo/db/todo.json' 文件的绝对路径
        path = os.path.join(BASE_DIR, 'db/todo.json')
        return path

    @classmethod
    def _load_db(cls):
        """加载 JSON 文件中所有 todo 数据"""
        path = cls._db_path()
        with open(path, 'r', encoding='utf-8') as f:
            return json.load(f)

    @classmethod
    def all(cls, sort=False, reverse=False):
        """获取全副 todo"""
        # 这一步用来将所有从 JSON 文件中读取的 todo 数据转换为 Todo 实例化对象,不便后续操作
        todo_list = [cls(**todo_dict) for todo_dict in cls._load_db()]
        # 对数据依照 id 进行排序
        if sort:
            todo_list = sorted(todo_list, key=lambda x: x.id, reverse=reverse)
        return todo_list

定义 Todo 模型类来操作 todo 数据。Todo 模型类的 all 办法用来读取全副的 todo 数据,在其外部将所有从 JSON 文件中读取的 todo 数据转换为 Todo 实例化对象并组装成 list 返回。all 办法还能够对数据进行排序,排序操作实际上转发给了 Python 内置的 sorted 函数来实现。

有了全副的 todo 数据,下一步操作就是将 todo 数据动静填充到 todo/templates/index.html 模板中。

应用模板引擎渲染 HTML

上一章实现的 Todo List 程序返回的首页数据都是固定写死在 todo/templates/index.html 代码中的。当初须要动静填充 todo 内容,咱们须要学习一个新的概念叫作 模板渲染

首先咱们编写的 HTML 页面不再是齐全应用 HTML 的标签来编写,而须要应用一些占位变量来替换须要动静填充的局部,这样编写进去的 HTML 页面通常称为模板。将 HTML 模板读取到内存中,应用实在的 todo 数据来替换掉占位变量而取得最终将要返回的字符串数据,这个过程称为渲染。可能实现读取 HTML 中的占位变量并正确替换为实在值的代码称为模板引擎。

Todo List 程序首页主体局部代码如下:

<h1 class="container">Todo List</h1>
<div class="container">
    <ul>
        <li>
            <div>Hello World</div>
        </li>
        <li>
            <div> 你好,世界!</div>
        </li>
    </ul>
</div>

其中每一个 li 标签代表一条 todo,显然 todo 的条数是不确定的,所以每一个 li 标签都须要动静生成。依据这段 HTML 代码,能够编写出如下模板:

<h1 class="container">Todo List</h1>
<div class="container">
    <ul>
        {% for todo in todo_list %}
            <li>
                <div>{{todo.content}}</div>
            </li>
        {% endfor %}
    </ul>
</div>

这段模板代码中只保留了一对 li 标签,它被嵌套在 for 循环中,for 语句块从 {% for todo in todo_list %} 开始,到 {% endfor %} 完结。todo_list 变量是在模板渲染阶段传进来的由所有 todo 对象组成的 listlist 中有多少个元素就会渲染多少个 li 标签。for 循环外部应用了循环变量 todo{{todo.content}} 示意获取 todo 变量的 content 属性,这与 Python 中获取对象的属性语法雷同。

理解了模板语法,咱们还须要有一个可能读懂模板语法的模板引擎。Todo List 程序的 HTML 模板只会用到 for 循环和模板变量这两种语法,所以咱们将要实现的模板引擎只须要可能解析这两种语法即可。

# todo_list/todo/utils.py

class Template(object):
    """模板引擎"""

    def __init__(self, text, context):
        # 保留最终后果
        self.result = []
        # 保留从 HTML 中解析进去的 for 语句代码片段
        self.for_snippet = []
        # 上下文变量
        self.context = context
        # 应用正则匹配出所有的 for 语句、模板变量
        self.snippets = re.split('({{.*?}}|{%.*?%})', text, flags=re.DOTALL)
        # 标记是否为 for 语句代码段
        is_for_snippet = False

        # 遍历所有匹配进去的代码片段
        for snippet in self.snippets:
            # 解析模板变量
            if snippet.startswith('{{'):
                if is_for_snippet is False:
                    # 去掉花括号和空格,获取变量名
                    var = snippet[2:-2].strip()
                    # 获取变量的值
                    snippet = self._get_var_value(var)
            # 解析 for 语句
            elif snippet.startswith('{%'):
                # for 语句开始代码片段 -> {% for todo in todo_list %}
                if 'in' in snippet:
                    is_for_snippet = True
                    self.result.append('{}')
                # for 语句完结代码片段 -> {% endfor %}
                else:
                    is_for_snippet = False
                    snippet = ''

            if is_for_snippet:
                # 如果是 for 语句代码段,须要进行二次解决,临时保留到 for 语句片段列表中
                self.for_snippet.append(snippet)
            else:
                # 如果是模板变量,间接将变量值追加到后果列表中
                self.result.append(snippet)

    def _get_var_value(self, var):
        """依据变量名获取变量的值"""
        # 如果 '.' 不在变量名中,间接在上下文变量中获取变量的值
        if '.' not in var:
            value = self.context.get(var)
        # '.' 在变量名中(对象. 属性),阐明是要获取对象的属性
        else:
            obj, attr = var.split('.')
            value = getattr(self.context.get(obj), attr)

        # 保障返回的变量值为字符串
        if not isinstance(value, str):
            value = str(value)
        return value

    def _parse_for_snippet(self):
        """解析 for 语句片段代码"""
        # 保留 for 语句片段解析后果
        result = []
        if self.for_snippet:
            # 解析 for 语句开始代码片段
            # '{% for todo in todo_list %}' -> ['for', 'todo', 'in', 'todo_list']
            words = self.for_snippet[0][2:-2].strip().split()
            # 从上下文变量中获取 for 语句中的可迭代对象
            iter_obj = self.context.get(words[-1])
            # 遍历可迭代对象
            for i in iter_obj:
                # 遍历 for 语句片段的代码块
                for snippet in self.for_snippet[1:]:
                    # 解析模板变量
                    if snippet.startswith('{{'):
                        # 去掉花括号和空格,获取变量名
                        var = snippet[2:-2].strip()
                        # 如果 '.' 不在变量名中,间接将循环变量 i 赋值给 snippet
                        if '.' not in var:
                            snippet = i
                        # '.' 在变量名中(对象. 属性),阐明是要获取对象的属性
                        else:
                            obj, attr = var.split('.')
                            # 将对象的属性值赋值给 snippet
                            snippet = getattr(i, attr)
                    # 保障变量值为字符串
                    if not isinstance(snippet, str):
                        snippet = str(snippet)
                    # 将解析进去的循环变量后果追加到 for 语句片段解析后果列表中
                    result.append(snippet)
        return result

    def render(self):
        """渲染"""
        # 获取 for 语句片段解析后果
        for_result = self._parse_for_snippet()
        # 将渲染后果组装成字符串并返回
        return ''.join(self.result).format(''.join(for_result))


def render_template(template, **context):
    """渲染模板"""
    # 读取 'todo_list/todo/templates' 目录下的 HTML 文件内容
    template_dir = os.path.join(BASE_DIR, 'templates')
    path = os.path.join(template_dir, template)

    with open(path, 'r', encoding='utf-8') as f:
        # 将从 HTML 中读取的内容传递给模板引擎
        t = Template(f.read(), context)

    # 调用模板引擎的渲染办法,实现模板渲染
    return t.render()

Template 类就是咱们为 Todo List 程序实现的模板引擎。模板引擎的代码有些简单,我写了比拟具体的正文来帮忙你了解。模板渲染的大略过程如下:

首先实例化 Template 对象,Template 对象的初始化办法 __init__ 须要传递两个参数,别离是 HTML 字符串和保留了模板所需变量的 dict,在初始化时会解析出 HTML 中所有的 for 语句和模板变量,模板变量间接被替换为对应的值,for 语句代码段则被暂存起来,等到须要真正渲染模板时,调用模板引擎实例对象的 render 办法,实现 for 语句的解析和值替换,最终将渲染后果组装成字符串并返回。

render_template 函数的代码也做了相应的调整,它的性能不再只是读取 HTML 内容,而是须要在外部调用模板引擎获取渲染后果。

对于基础薄弱的读者来说可能模板引擎局部的代码不太好了解,那么临时先不用深究,你只须要晓得模板引擎干了什么,明确它的原理无非是将 HTML 字符串中的模板语法全副找进去,而后依据语法规定将其替换成真正的变量值,最初渲染成正确的 HTML。实质上还是字符串的拼接,就像 Python 字符串的 format 办法一样,它可能找到字符串中的花括号 {},而后替换成传递给它的参数值。

MVC 模式的 Todo List 程序首页

咱们曾经介绍了应用模型操作数据和应用模板引擎渲染 HTML,当初就能够用动静渲染的 HTML 首页替换之前的动态首页了。

批改首页 todo/templates/index.html 的 HTML 代码为一个模板:

<!-- todo_list/todo/templates/index.html -->

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Todo List</title>
</head>
<body>
<h1 class="container">Todo List</h1>
<div class="container">
    <ul>
        {% for todo in todo_list %}
            <li>
                <div>{{todo.content}}</div>
            </li>
        {% endfor %}
    </ul>
</div>
</body>
</html>

这里我临时去掉了 HTML 顶部的 CSS 款式,因为咱们的模板引擎不反对这种间接将 CSS 嵌入在 HTML 中的写法,之后我会介绍如何通过 link 标签来引入内部款式。

咱们还要对 index 视图函数做些批改,在视图函数外部调用 Todo 模型的 all 办法来获取所有 todo,而后传递给模板引擎对 HTML 进行渲染,失去最终后果。批改后的代码如下:

# todo_list/todo/controllers.py

from todo.utils import render_template
from todo.models import Todo


def index():
    """首页视图函数"""
    # 倒序排序,最近增加的 todo 排在后面
    todo_list = Todo.all(sort=True, reverse=True)
    context = {'todo_list': todo_list,}
    return render_template('index.html', **context)

在终端中进入我的项目根目录 todo_list/ 下,应用 Python 运行 server.py 文件,将失去通过动静渲染的 Todo List 程序首页:

当初 Todo List 程序首页曾经是动静渲染的了,下一章咱们就来解决款式问题。

本章源码:chapter4

分割我:

  • 微信:jianghushinian
  • 邮箱:jianghushinian007@outlook.com
  • 博客地址:https://jianghushinian.cn/
正文完
 0