花瓣小跑 发表于 2024-6-10 00:31:38

用 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 格式的文件进行读写操作。
留意:

[*]JSON 文件不支持注释,以是假如你打算直接从上面示例中复制数据到 todo.json 文件时,须要去掉顶部文件名注释。
[*]假如 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 = \
\# 对数据按照 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 语句块从 竣事。todo_list 变量是在模板渲染阶段传进来的由全部 todo 对象构成的 list,list 中有多少个元素就会渲染多少个 li 标签。for 循环内部使用了循环变量 todo, 表示获取 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\.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\\.strip().split()
\# 从上下文变量中获取 for 语句中的可迭代对象
iter_obj = self.context.get(words\[-1\])
\# 遍历可迭代对象
for i in iter_obj:
\# 遍历 for 语句片段的代码块
for snippet in self.for_snippet\:
\# 解析模板变量
if snippet.startswith('{{'):
\# 去掉花括号和空格,获取变量名
var = snippet\.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 程序首页:
https://img-blog.csdnimg.cn/img_convert/1fc7d83f23d51353da241a10bdffd7c0.png
Todo List 首页
现在 Todo List 程序首页已经是动态渲染的了,下一章我们就来解决样式问题。
   本章源码:chapter4
原文出处: https://jianghushinian.cn

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 用 Python 撸一个 Web 服务器-第4章:动态渲染数据