用 Python 撸一个 Web 服务器-第6章:完善 Todo List 应用

简介: 用 Python 撸一个 Web 服务器-第6章:完善 Todo List 应用

这一章,我们来完成 todo 管理功能的剩余部分:新增、修改和删除功能。

新增 todo

首先实现 Todo List 程序的新增功能。新增 todo 的逻辑如下:

  1. 在首页顶部的输入框中输入 todo 内容。
  2. 然后点击新建按钮。
  3. 将输入框中的 todo 内容通过 POST 请求传递到服务器端。
  4. 服务器端解析请求中的 todo 内容并存储到文件。
  5. 重新返回到程序首页。

接下来对这些步骤进行具体实现。

首页 HTML 中添加新增 todo 的输入框和新建按钮:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!-- todo_list/todo/templates/index.html -->
<!DOCTYPE html>
<html>
<head>
<metacharset="UTF-8">
<title>Todo List</title>
<!-- 引入外部 CSS 样式 -->
<linkrel="stylesheet"href="/static/css/style.css">
</head>
<body>
<h1class="container">Todo List</h1>
<divclass="container">
<ul>
<li>
<formclass="new"action="/new"method="post">
<inputtype="text"name="content">
<buttonclass="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 文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* todo_list/todo/static/css/style.css */
.containerulli:first-child {
background-color: #ffffff;
padding: 0;
}
.containerbutton {
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;
}
forminput {
width: 90%;
height: 100%;
}

此时首页效果如下:

Todo List 首页

修改 Request 类,使其能够解析 GETPOST 请求中携带的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# todo_list/todo/utils.py
from urllib.parse import unquote_plus
...
classRequest(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  # 请求体
defparse_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'?'notin 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 内存保存到文件后,程序需要重新回到首页。所以我们需要定义一个重定向函数:

1
2
3
4
5
6
7
8
9
10
11
# todo_list/todo/utils.py
...
defredirect(url, status=302):
"""重定向"""
    headers = {
'Location': url,
    }
    body = ''
return Response(body, headers=headers, status=status)

重定向原理大致如下:

浏览器收到服务端的响应,如果状态码为 3XX,则表示重定向,浏览器会解析出响应头中的 Location 首部字段的值,然后通过 GET 请求方式访问这个 URL 地址。

重定向状态码最常见的两个就是 301302301 代表永久重定向,302 代表临时重定向。我们项目适合使用临时重定向,所以 redirect 函数的 status 参数默认值为 302

同时还需要修改 Response 响应类,使其能够处理 302 状态码:

1
2
3
4
5
6
7
8
9
10
11
12
13
# todo_list/todo/utils.py
classResponse(object):
"""响应类"""
# 根据状态码获取原因短语
    reason_phrase = {
200: 'OK',
302: 'FOUND',
405: 'METHOD NOT ALLOWED',
    }
    ...

在模型层,给 Todo 模型类新增一个 save 实例方法用来保存 todo 对象到文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# todo_list/todo/models.py
import os
import json
from todo.config import BASE_DIR
classTodo(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
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
defsave(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 isNone:
# 如果 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 的业务逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
# todo_list/todo/controllers.py
defnew(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 属性即可。这样当用户未输入任何内容就直接点击新建按钮时,浏览器会自动给出 请填写此字段。 的提示,在浏览器端做校验的好处是可以在请求未发送给后台服务器之前,由浏览器自动完成输入校验,这样能够减少发送请求的次数,避免无效的请求发送到服务器后台造成资源的浪费。

1
2
3
4
<formclass="new"action="/new"method="post">
<inputtype="text"name="content"required>
<buttonclass="save">新建</button>
</form>

input rquired

现在就可以使用 Todo List 程序的新增功能来添加 todo 了:

新增 todo

新增 todo

修改 todo

修改 todo 的逻辑如下:

  1. 点击首页中想要修改的 todo 右侧的编辑按钮。
  2. 页面跳转到编辑页面。
  3. 修改这条 todo 内容,点击保存按钮。
  4. 将输入框中的 todo 内容通过 POST 请求传递到服务器端。
  5. 服务器端将修改后的 todo 保存到文件。
  6. 重新返回到程序首页。

需要注意的是,修改 todo 依然采用了 POST 请求方式来提交数据。在前面介绍 HTTP 请求方法时,我提到过 HTTP 常见请求方法有四个:POSTDELETEPUTGET 分别对应增、删、改、查四个操作。如果采用 RESTful 风格来开发 Web API 最好完全遵照 HTTP 请求方法和操作的对应关系,以便开发出更加语义化的接口。这里为了简单起见,Todo List 程序只使用了 GETPOST 两种请求方式,除了获取数据时采用 GET 请求,新增、修改、删除操作都采用 POST 请求方式。

  • todo 首页新增编辑按钮

编辑页面的 HTML 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- todo_list/todo/templates/edit.html -->
<!DOCTYPE html>
<html>
<head>
<metacharset="UTF-8">
<title>Todo List | Edit</title>
<linkrel="stylesheet"href="/static/css/style.css">
</head>
<body>
<h1class="container">Edit</h1>
<divclass="container">
<formclass="edit"action="/edit"method="post">
<inputtype="hidden"name="id"value="{{ todo.id }}">
<inputtype="text"name="content"value="{{ todo.content }}">
<button>保存</button>
</form>
</div>
</body>
</html>

将编辑页面的 CSS 代码追加到 style.css 文件中:

1
2
3
4
5
6
7
8
9
10
/* 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 的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# todo_list/todo/models.py
classTodo(object):
"""
    Todo 模型类
    """
    ...
    @classmethod
deffind_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))
# 查询给定条数的数据
if0 < limit < len(result):
            result = result[:limit]
# 查询结果集中的第一条数据
if ensure_one:
            result = result[0] if len(result) > 0elseNone
return result
    @classmethod
defget(cls, id):
"""通过 id 查询 todo"""
        result = cls.find_by(id=id, ensure_one=True)
return result

find_by 方法用来根据给定条件查询 todo,get 方法内部调用的还是 find_by 方法,只根据 id 来搜索,查询结果为单条数据,这两个方法均为类方法。

在控制器层,编写一个 edit 视图函数用来处理编辑 todo 的业务逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# todo_list/todo/controllers.py
defedit(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 != -1and 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)
ifnot 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

删除 todo 的逻辑如下:

  1. 点击首页中想要删除的 todo 右侧的删除按钮。
  2. 将这条 todo 的 id 通过 POST 请求传递到服务器端。
  3. 服务器端通过得到的 id 查询并删除对应的 todo。
  4. 重新返回到程序首页。

首页 HTML 中添加删除按钮:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!-- todo_list/todo/templates/index.html -->
<!DOCTYPE html>
<html>
<head>
<metacharset="UTF-8">
<title>Todo List</title>
<!-- 引入外部 CSS 样式 -->
<linkrel="stylesheet"href="/static/css/style.css">
</head>
<body>
<h1class="container">Todo List</h1>
<divclass="container">
<ul>
<li>
<formclass="new"action="/new"method="post">
<inputtype="text"name="content"required>
<buttonclass="save">新建</button>
</form>
</li>
<br>
        {% for todo in todo_list %}
<li>
<div>{{ todo.content }}</div>
<div>
<ahref="/edit?id={{ todo.id }}">
<button>编辑</button>
</a>
<formclass="delete"action="/delete"method="post">
<inputtype="hidden"name="id"value="{{ todo.id }}">
<button>删除</button>
</form>
</div>
</li>
        {% endfor %}
</ul>
</div>
</body>
</html>

将删除按钮的 CSS 代码追加到 style.css 文件中:

1
2
3
4
5
/* todo_list/todo/static/css/style.css */
.delete {
display: inline-block;
}

在模型层,给 Todo 模型类新增一个 delete 实例方法用来从文件中删除 todo 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
# todo_list/todo/models.py
classTodo(object):
"""
    Todo 模型类
    """
   ...
defdelete(self):
"""删除 todo"""
        todo_list = [todo.__dict__ for todo in self.all() if todo.id != self.id]
        self._save_db(todo_list)

在控制器层,编写一个 delete 视图函数用来处理删除 todo 的业务逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# todo_list/todo/controllers.py
defdelete(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

删除 todo

至此,todo 的管理部分代码编写完成。

本章源码:chapter6

相关文章
|
1天前
|
缓存 移动开发 前端开发
【专栏:HTML与CSS前端技术趋势篇】HTML与CSS在PWA(Progressive Web Apps)中的应用
【4月更文挑战第30天】PWA(Progressive Web Apps)结合现代Web技术,提供接近原生应用的体验。HTML在PWA中构建页面结构和内容,响应式设计、语义化标签、Manifest文件和离线页面的创建都离不开HTML。CSS则用于定制主题样式、实现动画效果、响应式布局和管理字体图标。两者协同工作,保证PWA在不同设备和网络环境下的快速、可靠和一致性体验。随着前端技术进步,HTML与CSS在PWA中的应用将更广泛。
|
1天前
|
前端开发 JavaScript 搜索推荐
【专栏:HTML 与 CSS 前端技术趋势篇】HTML 与 CSS 在 Web 组件化中的应用
【4月更文挑战第30天】本文探讨了HTML和CSS在Web组件化中的应用及其在前端趋势中的重要性。组件化提高了代码复用、维护性和扩展性。HTML提供组件结构,语义化标签增进可读性,支持用户交互;CSS实现样式封装、布局控制和主题定制。案例展示了导航栏、卡片和模态框组件的创建。响应式设计、动态样式、CSS预处理器和Web组件标准等趋势影响HTML/CSS在组件化中的应用。面对兼容性、代码复杂度和性能优化挑战,需采取相应策略。未来,持续发掘HTML和CSS潜力,推动组件化开发创新,提升Web应用体验。
|
1天前
|
缓存 前端开发 JavaScript
探索现代Web应用的性能优化策略移动应用开发的未来之路:跨平台与原生之争
【4月更文挑战第30天】随着互联网技术的迅猛发展,Web应用已成为信息交流和商业活动的重要平台。用户对Web应用的响应速度和稳定性有着极高的期望,这促使开发者不断寻求提升应用性能的有效途径。本文将深入探讨针对现代Web应用进行性能优化的关键策略,包括前端优化、后端优化以及数据库层面的调优技巧,旨在为开发者提供一套全面的优化工具箱,帮助他们构建更快速、更高效的Web应用。
|
1天前
|
开发框架 JavaScript 前端开发
【JavaScript 与 TypeScript 技术专栏】TypeScript 在 Web 开发中的前沿应用
【4月更文挑战第30天】TypeScript在Web开发中日益重要,以其强大的类型系统提升代码质量,支持组件化开发,与React、Vue、Angular等框架良好集成。在大型项目管理中,TypeScript助于代码组织和优化,提高团队协作效率。此外,它提升开发体验,提供智能提示和错误检测。众多成功案例证明其前沿应用,未来将在Web开发领域持续发挥关键作用。
|
1天前
|
移动开发 JavaScript 前端开发
【JavaScript技术专栏】Web Worker在JavaScript中的应用
【4月更文挑战第30天】HTML5的Web Worker API解决了JavaScript单线程性能瓶颈问题,允许在后台线程运行JS代码。本文介绍了Web Worker的基本概念、类型、用法和应用场景,如复杂计算、图像处理和数据同步。通过实例展示了搜索建议、游戏开发和实时数据分析等应用,并提醒注意其无法直接访问DOM、需消息传递通信以及移动端资源管理。Web Worker为前端开发提供了多线程能力,提升了Web应用性能和用户体验。
|
1天前
|
开发框架 Dart 前端开发
【Flutter前端技术开发专栏】Flutter中的Web支持:构建跨平台Web应用
【4月更文挑战第30天】Flutter,Google的开源跨平台框架,已延伸至Web领域,让开发者能用同一代码库构建移动和Web应用。Flutter Web通过将Dart代码编译成JavaScript和WASM运行在Web上。尽管性能可能不及原生Web应用,但适合交互性强、UI复杂的应用。开发者应关注性能优化、兼容性测试,并利用Flutter的声明式UI、热重载等优势。随着其发展,Flutter Web为跨平台开发带来更多潜力。
【Flutter前端技术开发专栏】Flutter中的Web支持:构建跨平台Web应用
|
2天前
|
存储 前端开发 数据库
搭建轻量级Web应用
【4月更文挑战第14天】本文介绍了使用Flask快速搭建轻量级Web应用的步骤。首先,通过`pip install Flask`安装Flask,然后创建基础应用结构,包含路由和简单的Hello, Flask!页面。接着,学习如何添加更多页面、使用模板引擎(如Jinja2)和处理表单。此外,文章还涉及管理静态文件、集成SQLite数据库、进行数据库迁移以及添加用户认证功能,使用Flask-Login实现登录和登出。通过这些步骤,读者能掌握构建完整Flask应用的基本知识,了解其灵活性和扩展性。
8 0
|
2天前
|
缓存 前端开发 安全
Python web框架fastapi中间件的使用,CORS跨域详解
Python web框架fastapi中间件的使用,CORS跨域详解
|
2天前
|
API 数据库 Python
Python web框架fastapi数据库操作ORM(二)增删改查逻辑实现方法
Python web框架fastapi数据库操作ORM(二)增删改查逻辑实现方法
|
2天前
|
关系型数据库 MySQL API
Python web框架fastapi数据库操作ORM(一)
Python web框架fastapi数据库操作ORM(一)