共计 8912 个字符,预计需要花费 23 分钟才能阅读完成。
这一章,咱们来实现 todo 治理性能的残余局部:新增、批改和删除性能。
新增 todo
首先实现 Todo List 程序的新增性能。新增 todo 的逻辑如下:
- 在首页顶部的输入框中输出 todo 内容。
- 而后点击新建按钮。
- 将输入框中的 todo 内容通过
POST
申请传递到服务器端。 - 服务器端解析申请中的 todo 内容并存储到文件。
- 从新返回到程序首页。
接下来对这些步骤进行具体实现。
首页 HTML 中增加新增 todo 的输入框和新建按钮:
<!-- todo_list/todo/templates/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Todo List</title>
<!-- 引入内部 CSS 款式 -->
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<h1 class="container">Todo List</h1>
<div class="container">
<ul>
<li>
<form class="new" action="/new" method="post">
<input type="text" name="content">
<button class="save"> 新建 </button>
</form>
</li>
<br>
{% for todo in todo_list %}
<li>
<div>{{todo.content}}</div>
</li>
{% endfor %}
</ul>
</div>
</body>
</html>
代码中减少了一个 form
标签,用来新增 todo。申请门路地址为 /new
,申请办法为 post
。
将首页的 CSS 代码追加到 style.css
文件中:
/* todo_list/todo/static/css/style.css */
.container ul li:first-child {
background-color: #ffffff;
padding: 0;
}
.container button {
width: 40px;
height: 28px;
padding: 4px;
cursor: pointer;
}
.new {
width: 100%;
max-width: 600px;
height: 40px;
display: flex;
justify-content: space-between;
align-items: center;
}
form input {
width: 90%;
height: 100%;
}
此时首页成果如下:
批改 Request
类,使其可能解析 GET
、POST
申请中携带的参数:
# todo_list/todo/utils.py
from urllib.parse import unquote_plus
...
class Request(object):
"""申请类"""
def __init__(self, request_message):
method, path, headers, args, form = self.parse_data(request_message)
self.method = method # 申请办法 GET、POST
self.path = path # 申请门路 /index
self.headers = headers # 申请头 {'Host': '127.0.0.1:8000'}
self.args = args # 查问参数
self.form = form # 申请体
def parse_data(self, data):
"""解析申请数据"""
# 用申请报文中的第一个 '\r\n\r\n' 做宰割,将失去申请头和申请体
header, body = data.split('\r\n\r\n', 1)
method, path, headers, args = self._parse_header(header)
form = self._path_body(body)
return method, path, headers, args, form
def _parse_header(self, data):
"""解析申请头"""
# 拆分申请行和申请首部
request_line, request_header = data.split('\r\n', 1)
# 申请行拆包 'GET /index HTTP/1.1' -> ['GET', '/index', 'HTTP/1.1']
# 因为 HTTP 版本号没什么用,所以用一个下划线 _ 变量来接管
method, path_query, _ = request_line.split()
path, args = self._parse_path(path_query)
# 解析申请首部所有的键值对,组装成字典
headers = {}
for header in request_header.split('\r\n'):
k, v = header.split(':', 1)
headers[k] = v
return method, path, headers, args
@staticmethod
def _parse_path(data):
"""解析申请门路、申请参数"""
args = {}
# 申请门路和 GET 申请参数格局: /index?edit=1&content=text
if '?' not in data:
path, query = data, ''
else:
path, query = data.split('?', 1)
for q in query.split('&'):
k, v = q.split('=', 1)
args[k] = v
return path, args
@staticmethod
def _path_body(data):
"""解析申请体"""
form = {}
if data:
# POST 申请体参数格局: username=zhangsan&password=mima
for b in data.split('&'):
k, v = b.split('=', 1)
# 前端页面中通过 form 表单提交过去的数据会被自动编码,应用 unquote_plus 来解码
form[k] = unquote_plus(v)
return form
为 Request
类新增了两个静态方法,_parse_path
办法用来拆分申请门路和申请参数。_path_body
用来提取申请体中的参数并转换为 dict
。
在服务器端将新增的 todo 内存保留到文件后,程序须要从新回到首页。所以咱们须要定义一个重定向函数:
# todo_list/todo/utils.py
...
def redirect(url, status=302):
"""重定向"""
headers = {'Location': url,}
body = ''
return Response(body, headers=headers, status=status)
重定向原理大抵如下:
浏览器收到服务端的响应,如果状态码为 3XX
,则示意重定向,浏览器会解析出响应头中的 Location
首部字段的值,而后通过 GET 申请形式拜访这个 URL
地址。
重定向状态码最常见的两个就是 301
、302
,301
代表永恒重定向,302
代表长期重定向。咱们我的项目适宜应用长期重定向,所以 redirect
函数的 status
参数默认值为 302
。
同时还须要批改 Response
响应类,使其可能解决 302
状态码:
# todo_list/todo/utils.py
class Response(object):
"""响应类"""
# 依据状态码获取起因短语
reason_phrase = {
200: 'OK',
302: 'FOUND',
405: 'METHOD NOT ALLOWED',
}
...
在模型层,给 Todo
模型类新增一个 save
实例办法用来保留 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 _save_db(cls, data):
"""将 todo 数据保留到 JSON 文件"""
path = cls._db_path()
with open(path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4)
@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
def save(self):
"""保留 todo"""
# 查找出除 self 以外所有 todo
# todo.__dict__ 是保留了所有实例属性的字典
todo_list = [todo.__dict__ for todo in self.all(sort=True) if todo.id != self.id]
# 自增 id
if self.id is None:
# 如果 todo_list 长度大于 0 阐明不是第一条 todo,取最初一条 todo 的 id 加 1
if len(todo_list) > 0:
self.id = todo_list[-1]['id'] + 1
# 否则阐明是第一条 todo,id 为 1
else:
self.id = 1
# 将以后 todo 追加到 todo_list
todo_list.append(self.__dict__)
# 将所有 todo 保留到文件
self._save_db(todo_list)
在控制器层,编写一个 new
视图函数用来解决新增 todo 的业务逻辑:
# todo_list/todo/controllers.py
def new(request):
"""新建 todo 视图函数"""
form = request.form
print(f'form: {form}')
content = form.get('content')
# 这里判断前端传递过去的参数是否有内容,如果为空则阐明不是一个无效的 todo,间接重定向到首页
if content:
todo = Todo(content=content)
todo.save()
return redirect('/index')
在 new
视图函数中判断如果前端传递过去的 todo 无效,就将其保留到文件,而后重定向到首页。
其实对于查看 todo 是否无效的逻辑前端也能够解决,只须要在输出 todo 的 input
标签加上 required
属性即可。这样当用户未输出任何内容就间接点击新建按钮时,浏览器会主动给出 请填写此字段。
的提醒,在浏览器端做校验的益处是能够在申请未发送给后盾服务器之前,由浏览器主动实现输出校验,这样可能缩小发送申请的次数,防止有效的申请发送到服务器后盾造成资源的节约。
<form class="new" action="/new" method="post">
<input type="text" name="content" required>
<button class="save"> 新建 </button>
</form>
当初就能够应用 Todo List 程序的新增性能来增加 todo 了:
批改 todo
批改 todo 的逻辑如下:
- 点击首页中想要批改的 todo 右侧的编辑按钮。
- 页面跳转到编辑页面。
- 批改这条 todo 内容,点击保留按钮。
- 将输入框中的 todo 内容通过
POST
申请传递到服务器端。 - 服务器端将批改后的 todo 保留到文件。
- 从新返回到程序首页。
须要留神的是,批改 todo 仍然采纳了 POST
申请形式来提交数据。在后面介绍 HTTP 申请办法时,我提到过 HTTP 常见申请办法有四个:POST
、DELETE
、PUT
、GET
别离对应增、删、改、查四个操作。如果采纳 RESTful
格调来开发 Web API
最好齐全遵循 HTTP 申请办法和操作的对应关系,以便开发出更加语义化的接口。这里为了简略起见,Todo List 程序只应用了 GET
、POST
两种申请形式,除了获取数据时采纳 GET
申请,新增、批改、删除操作都采纳 POST
申请形式。
- todo 首页新增编辑按钮
编辑页面的 HTML 如下:
<!-- todo_list/todo/templates/edit.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Todo List | Edit</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<h1 class="container">Edit</h1>
<div class="container">
<form class="edit" action="/edit" method="post">
<input type="hidden" name="id" value="{{todo.id}}">
<input type="text" name="content" value="{{todo.content}}">
<button> 保留 </button>
</form>
</div>
</body>
</html>
将编辑页面的 CSS 代码追加到 style.css
文件中:
/* todo_list/todo/static/css/style.css */
.new, .edit {
width: 100%;
max-width: 600px;
height: 40px;
display: flex;
justify-content: space-between;
align-items: center;
}
在模型层,给 Todo
模型类新增两个查问 todo 的办法:
# todo_list/todo/models.py
class Todo(object):
"""Todo 模型类"""
...
@classmethod
def find_by(cls, limit=-1, ensure_one=False, sort=False, reverse=False, **kwargs):
"""查问 todo"""
result = []
todo_list = [todo.__dict__ for todo in cls.all(sort=sort, reverse=reverse)]
for todo in todo_list:
# 依据关键字参数查问 todo
for k, v in kwargs.items():
if todo.get(k) != v:
break
else:
result.append(cls(**todo))
# 查问给定条数的数据
if 0 < limit < len(result):
result = result[:limit]
# 查问后果集中的第一条数据
if ensure_one:
result = result[0] if len(result) > 0 else None
return result
@classmethod
def get(cls, id):
"""通过 id 查问 todo"""
result = cls.find_by(id=id, ensure_one=True)
return result
find_by
办法用来依据给定条件查问 todo,get
办法外部调用的还是 find_by
办法,只依据 id
来搜寻,查问后果为单条数据,这两个办法均为类办法。
在控制器层,编写一个 edit
视图函数用来解决编辑 todo 的业务逻辑:
# todo_list/todo/controllers.py
def edit(request):
"""编辑 todo 视图函数"""
# 解决 POST 申请
if request.method == 'POST':
form = request.form
print(f'form: {form}')
id = int(form.get('id', -1))
content = form.get('content')
if id != -1 and content:
todo = Todo.get(id)
if todo:
todo.content = content
todo.save()
return redirect('/index')
# 解决 GET 申请
args = request.args
print(f'args: {args}')
id = int(args.get('id', -1))
if id == -1:
return redirect('/index')
todo = Todo.get(id)
if not todo:
return redirect('/index')
context = {'todo': todo,}
return render_template('edit.html', **context)
routes = {
...
'/edit': (edit, ['GET', 'POST']),
}
edit
视图函数跟之前编写的其余视图函数有些不同,它同时反对两种申请办法,GET
申请返回 HTML 页面,POST
申请用来解决批改 todo 的逻辑,并在批改实现后重定向到首页。
当初就能够应用 Todo List 程序的编辑性能来批改 todo 了:
删除 todo
删除 todo 的逻辑如下:
- 点击首页中想要删除的 todo 右侧的删除按钮。
- 将这条 todo 的
id
通过POST
申请传递到服务器端。 - 服务器端通过失去的
id
查问并删除对应的 todo。 - 从新返回到程序首页。
首页 HTML 中增加删除按钮:
<!-- todo_list/todo/templates/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Todo List</title>
<!-- 引入内部 CSS 款式 -->
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<h1 class="container">Todo List</h1>
<div class="container">
<ul>
<li>
<form class="new" action="/new" method="post">
<input type="text" name="content" required>
<button class="save"> 新建 </button>
</form>
</li>
<br>
{% for todo in todo_list %}
<li>
<div>{{todo.content}}</div>
<div>
<a href="/edit?id={{todo.id}}">
<button> 编辑 </button>
</a>
<form class="delete" action="/delete" method="post">
<input type="hidden" name="id" value="{{todo.id}}">
<button> 删除 </button>
</form>
</div>
</li>
{% endfor %}
</ul>
</div>
</body>
</html>
将删除按钮的 CSS 代码追加到 style.css
文件中:
/* todo_list/todo/static/css/style.css */
.delete {display: inline-block;}
在模型层,给 Todo
模型类新增一个 delete
实例办法用来从文件中删除 todo 对象:
# todo_list/todo/models.py
class Todo(object):
"""Todo 模型类"""
...
def delete(self):
"""删除 todo"""
todo_list = [todo.__dict__ for todo in self.all() if todo.id != self.id]
self._save_db(todo_list)
在控制器层,编写一个 delete
视图函数用来解决删除 todo 的业务逻辑:
# todo_list/todo/controllers.py
def delete(request):
"""删除 todo 视图函数"""
form = request.form
print(f'form: {form}')
id = int(form.get('id', -1))
if id != -1:
todo = Todo.get(id)
if todo:
todo.delete()
return redirect('/index')
routes = {
...
'/delete': (delete, ['POST']),
}
当初就能够应用 Todo List 程序的删除性能来删除 todo 了:
至此,todo 的治理局部代码编写实现。
本章源码:chapter6
分割我:
- 微信:jianghushinian
- 邮箱:jianghushinian007@outlook.com
- 博客地址:https://jianghushinian.cn/