@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 defall(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 中的占位变量并正确替换为真实值的代码称为模板引擎。
其中每一个 li 标签代表一条 todo,显然 todo 的条数是不确定的,所以每一个 li 标签都需要动态生成。根据这段 HTML 代码,可以编写出如下模板:
1 2 3 4 5 6 7 8 9 10
<h1class="container">Todo List</h1> <divclass="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 循环和模板变量这两种语法,所以我们将要实现的模板引擎只需要能够解析这两种语法即可。
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 isFalse: # 去掉花括号和空格,获取变量名 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)
# 保证返回的变量值为字符串 ifnot 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'.'notin var: snippet = i # '.' 在变量名中(对象.属性),说明是要获取对象的属性 else: obj, attr = var.split('.') # 将对象的属性值赋值给 snippet snippet = getattr(i, attr) # 保证变量值为字符串 ifnot isinstance(snippet, str): snippet = str(snippet) # 将解析出来的循环变量结果追加到 for 语句片段解析结果列表中 result.append(snippet) return result
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 首页替换之前的静态首页了。