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

打印 上一主题 下一主题

主题 631|帖子 631|积分 1893

上一章中为了尽快让 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 文件:
  1. // todo_list/todo/db/todo.json
  2. \[  
  3. {  
  4. "id": 1,  
  5. "content": "hello world"  
  6. },  
  7. {  
  8. "id": 2,  
  9. "content": "你好,世界!"  
  10. }  
  11. \]
复制代码
这是一个标准的 JSON 格式,每一个对象代表了一条 todo,content 字段即为 todo 内容,id 作为每条数据的索引不会展示在页面中,方便我们对数据进行排序、快速查找等操作。
为了简化程序,我将数据存储在 JSON 文件中而不是数据库中。存储到文件的格式多种多样,但 JSON 格式是一种非常流行且友爱的数据格式,在 Python 中也能够很方便的对 JSON 格式的文件进行读写操作。
留意:

  • JSON 文件不支持注释,以是假如你打算直接从上面示例中复制数据到 todo.json 文件时,须要去掉顶部文件名注释。
  • 假如 todo/db/todo.json 文件内容为空,使用 Python 读取时会抛出 JSONDecodeError 异常,起码要保证其内部有一个空数组 [] 存在,才气正常读取。
确定了 todo/db/todo.json 文件数据格式,就可以编写在模子层读取 todo 数据的代码了:
  1. \# todo_list/todo/models.py
  2. import os  
  3. import json
  4. from todo.config import BASE_DIR
  5. class Todo(object):  
  6. """  
  7. Todo 模型类  
  8. """
  9. def \_\_init\_\_(self, \*\*kwargs):  
  10. self.id = kwargs.get('id')  
  11. self.content = kwargs.get('content', '')
  12. @classmethod  
  13. def \_db_path(cls):  
  14. """获取存储 todo 数据文件的绝对路径"""  
  15. \# 返回 'todo_list/todo/db/todo.json' 文件的绝对路径  
  16. path = os.path.join(BASE_DIR, 'db/todo.json')  
  17. return path
  18. @classmethod  
  19. def \_load_db(cls):  
  20. """加载 JSON 文件中所有 todo 数据"""  
  21. path = cls.\_db_path()  
  22. with open(path, 'r', encoding='utf-8') as f:  
  23. return json.load(f)
  24. @classmethod  
  25. def all(cls, sort=False, reverse=False):  
  26. """获取全部 todo"""  
  27. \# 这一步用来将所有从 JSON 文件中读取的 todo 数据转换为 Todo 实例化对象,方便后续操作  
  28. todo_list = \[cls(\*\*todo_dict) for todo_dict in cls.\_load_db()\]  
  29. \# 对数据按照 id 进行排序  
  30. if sort:  
  31. todo_list = sorted(todo_list, key=lambda x: x.id, reverse=reverse)  
  32. 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 程序首页主体部门代码如下:
  1. <h1 class="container">Todo List</h1>  
  2. <div class="container">  
  3. <ul>  
  4. <li>  
  5. <div>Hello World</div>  
  6. </li>  
  7. <li>  
  8. <div>你好,世界!</div>  
  9. </li>  
  10. </ul>  
  11. </div>
复制代码
其中每一个 li 标签代表一条 todo,显然 todo 的条数是不确定的,以是每一个 li 标签都须要动态生成。根据这段 HTML 代码,可以编写出如下模板:
  1. <h1 class="container">Todo List</h1>  
  2. <div class="container">  
  3. <ul>  
  4. {% for todo in todo_list %}  
  5. <li>  
  6. <div>{{ todo.content }}</div>  
  7. </li>  
  8. {% endfor %}  
  9. </ul>  
  10. </div>
复制代码
这段模板代码中只保存了一对 li 标签,它被嵌套在 for 循环中,for 语句块从 竣事。todo_list 变量是在模板渲染阶段传进来的由全部 todo 对象构成的 list,list 中有多少个元素就会渲染多少个 li 标签。for 循环内部使用了循环变量 todo, 表示获取 todo 变量的 content 属性,这与 Python 中获取对象的属性语法雷同。
相识了模板语法,我们还须要有一个能够读懂模板语法的模板引擎。Todo List 程序的 HTML 模板只会用到 for 循环和模板变量这两种语法,以是我们将要实现的模板引擎只须要能够分析这两种语法即可。
  1. \# todo_list/todo/utils.py
  2. class Template(object):  
  3. """模板引擎"""
  4. def \_\_init\_\_(self, text, context):  
  5. \# 保存最终结果  
  6. self.result = \[\]  
  7. \# 保存从 HTML 中解析出来的 for 语句代码片段  
  8. self.for_snippet = \[\]  
  9. \# 上下文变量  
  10. self.context = context  
  11. \# 使用正则匹配出所有的 for 语句、模板变量  
  12. self.snippets = re.split('({{.*?}}|{%.*?%})', text, flags=re.DOTALL)  
  13. \# 标记是否为 for 语句代码段  
  14. is_for_snippet = False
  15. \# 遍历所有匹配出来的代码片段  
  16. for snippet in self.snippets:  
  17. \# 解析模板变量  
  18. if snippet.startswith('{{'):  
  19. if is_for_snippet is False:  
  20. \# 去掉花括号和空格,获取变量名  
  21. var = snippet\[2:-2\].strip()  
  22. \# 获取变量的值  
  23. snippet = self.\_get_var_value(var)  
  24. \# 解析 for 语句  
  25. elif snippet.startswith('{%'):
  26. \# for 语句开始代码片段 -> {% for todo in todo_list %}  
  27. if 'in' in snippet:  
  28. is_for_snippet = True  
  29. self.result.append('{}')  
  30. \# for 语句结束代码片段 -> {% endfor %}  
  31. else:  
  32. is_for_snippet = False  
  33. snippet = ''
  34. if is_for_snippet:  
  35. \# 如果是 for 语句代码段,需要进行二次处理,暂时保存到 for 语句片段列表中  
  36. self.for_snippet.append(snippet)  
  37. else:  
  38. \# 如果是模板变量,直接将变量值追加到结果列表中  
  39. self.result.append(snippet)
  40. def \_get_var_value(self, var):  
  41. """根据变量名获取变量的值"""  
  42. \# 如果 '.' 不在变量名中,直接在上下文变量中获取变量的值  
  43. if '.' not in var:  
  44. value = self.context.get(var)  
  45. \# '.' 在变量名中(对象.属性),说明是要获取对象的属性  
  46. else:  
  47. obj, attr = var.split('.')  
  48. value = getattr(self.context.get(obj), attr)
  49. \# 保证返回的变量值为字符串  
  50. if not isinstance(value, str):  
  51. value = str(value)  
  52. return value
  53. def \_parse_for_snippet(self):  
  54. """解析 for 语句片段代码"""  
  55. \# 保存 for 语句片段解析结果  
  56. result = \[\]  
  57. if self.for_snippet:  
  58. \# 解析 for 语句开始代码片段  
  59. \# '{% for todo in todo\_list %}' -> \['for', 'todo', 'in', 'todo_list'\]  
  60. words = self.for_snippet\[0\]\[2:-2\].strip().split()  
  61. \# 从上下文变量中获取 for 语句中的可迭代对象  
  62. iter_obj = self.context.get(words\[-1\])  
  63. \# 遍历可迭代对象  
  64. for i in iter_obj:  
  65. \# 遍历 for 语句片段的代码块  
  66. for snippet in self.for_snippet\[1:\]:  
  67. \# 解析模板变量  
  68. if snippet.startswith('{{'):  
  69. \# 去掉花括号和空格,获取变量名  
  70. var = snippet\[2:-2\].strip()  
  71. \# 如果 '.' 不在变量名中,直接将循环变量 i 赋值给 snippet  
  72. if '.' not in var:  
  73. snippet = i  
  74. \# '.' 在变量名中(对象.属性),说明是要获取对象的属性  
  75. else:  
  76. obj, attr = var.split('.')  
  77. \# 将对象的属性值赋值给 snippet  
  78. snippet = getattr(i, attr)  
  79. \# 保证变量值为字符串  
  80. if not isinstance(snippet, str):  
  81. snippet = str(snippet)  
  82. \# 将解析出来的循环变量结果追加到 for 语句片段解析结果列表中  
  83. result.append(snippet)  
  84. return result
  85. def render(self):  
  86. """渲染"""  
  87. \# 获取 for 语句片段解析结果  
  88. for_result = self.\_parse_for_snippet()  
  89. \# 将渲染结果组装成字符串并返回  
  90. return ''.join(self.result).format(''.join(for_result))
  91. def render_template(template, \*\*context):  
  92. """渲染模板"""  
  93. \# 读取 'todo_list/todo/templates' 目录下的 HTML 文件内容  
  94. template_dir = os.path.join(BASE_DIR, 'templates')  
  95. path = os.path.join(template_dir, template)
  96. with open(path, 'r', encoding='utf-8') as f:  
  97. \# 将从 HTML 中读取的内容传递给模板引擎  
  98. t = Template(f.read(), context)
  99. \# 调用模板引擎的渲染方法,实现模板渲染  
  100. 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 代码为一个模板:
  1. <!\-\- 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>  
  2. <div class="container">  
  3. <ul>  
  4. {% for todo in todo_list %}  
  5. <li>  
  6. <div>{{ todo.content }}</div>  
  7. </li>  
  8. {% endfor %}  
  9. </ul>  
  10. </div>
  11.   </body>  </html>
复制代码
这里我暂时去掉了 HTML 顶部的 CSS 样式,因为我们的模板引擎不支持这种直接将 CSS 嵌入在 HTML 中的写法,之后我会介绍如何通过 link 标签来引入外部样式。
我们还要对 index 视图函数做些修改,在视图函数内部调用 Todo 模子的 all 方法来获取全部 todo,然后传递给模板引擎对 HTML 进行渲染,得到终极结果。修改后的代码如下:
  1. \# todo_list/todo/controllers.py
  2. from todo.utils import render_template  
  3. from todo.models import Todo
  4. def index():  
  5. """首页视图函数"""  
  6. \# 倒序排序,最近添加的 todo 排在前面  
  7. todo_list = Todo.all(sort=True, reverse=True)  
  8. context = {  
  9. 'todo_list': todo_list,  
  10. }  
  11. return render_template('index.html', \*\*context)
复制代码
在终端中进入项目根目录 todo_list/ 下,使用 Python 运行 server.py 文件,将得到经过动态渲染的 Todo List 程序首页:

Todo List 首页
现在 Todo List 程序首页已经是动态渲染的了,下一章我们就来解决样式问题。
   本章源码:chapter4
  原文出处: https://jianghushinian.cn

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

花瓣小跑

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表