乐趣区

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

上一章中为了尽快让 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/
退出移动版