共计 9711 个字符,预计需要花费 25 分钟才能阅读完成。
Todo List 程序介绍
咱们将要编写的 Todo List 程序蕴含四个页面,别离是注册页面、登录页面、首页、编辑页面。以下别离为四个页面的截图。
注册页面:
登录页面:
首页:
编辑页面:
程序页面十分简洁,甚至有些 Low。但这足够咱们学习开发 Web 服务器程序原理,页面款式的问题并不是咱们本次学习的重点,所以读者不用纠结于此。
Todo List 程序性能大略分为两个局部,一部分是 todo 治理,蕴含增删改查根底性能;另一部分是用户治理,蕴含注册和登录性能。
初识 MVC
介绍了 Todo List 程序的页面和性能,接下来咱们就要思考如何设计程序。
以客户端通过浏览器向服务器发送一个获取利用首页的申请为例,来剖析下服务器在收到这个申请后都须要做哪些事件:
- 首先服务器须要对申请数据进行解析,发现客户端是要获取利用首页。
- 而后找到代表首页的 HTML 文件,读取 HTML 文件中的内容。
- 最初将 HTML 内容组装成合乎 HTTP 标准的数据进行返回。
这是一个较为理想的状况,因为 HTML 页面内容是固定的,咱们不须要对其进行其余解决,间接返回给浏览器即可。通常咱们管这种页面叫动态页面。
但理论状况中,Todo List 程序首页内容并不是变化无穷的,而是动态变化的。首页 HTML 文件中只定义根底构造,具体的 todo 数据须要动静填充进去。所以一个更加残缺的服务器解决申请的过程应该像上面这样:
- 首先服务器须要对申请数据进行解析,发现客户端是要获取利用首页。
- 而后从数据库中读取 todo 数据。
- 接着找到代表首页的 HTML 文件,读取 HTML 文件中的内容。
- 再将 todo 数据动静增加到 HTML 内容中。
- 最初将解决好的 HTML 内容组装成合乎 HTTP 标准的数据进行返回。
当初曾经晓得了服务器解决申请的残缺过程,咱们就能够设计服务器程序了。试想一下,如果 Todo List 程序都像 Hello World 程序一样把代码都写在一个 Python 文件中也不是不能够。但这样的代码显然不具备良好的扩展性和可维护性。那么更好的设计模式是什么呢?
其实对于 Web 服务器程序的设计,业界早已达成了一个广泛的共识,那就是 MVC
模式:
M(Model):模型,用来存储和解决 Web 利用数据。
V(View):视图,格式化显示 Web 利用页面。
C(Controller):控制器,负责根底逻辑,如从模型层读取数据并将数据填充到视图层,而后返回响应。
通过 MVC
的分层构造,可能让 Web 利用设计更加清晰,能够很容易的构建可扩大、易保护的代码。模型层,说直白些其实就是用来读写数据库的 Python 代码,新增 todo 的时候,能够通过模型层的代码将数据保留到数据库中,拜访首页时须要展现所有已保留的 todo,这时能够通过模型层的代码从数据库中读取所有 todo。视图层,能够将其简略的了解为 HTML 模板文件的汇合。控制器起到粘合的作用,它将从模型层读取过去的数据填充到视图层并返回给浏览器,或者将浏览器通过 HTML 页面提交过去的数据解析进去再通过模型层写入数据库中。
我画了一个示例图,来帮忙你了解 MVC
模式。图中标注了浏览器发动一个申请到取得响应,两头经验的残缺过程。
还是以客户端申请 Todo List 程序首页为例,一个残缺的申请过程如下:
- 浏览器发动申请。
- 申请达到服务器后首先进入控制器,而后控制器从模型获取 todo 数据。
- 模型操作数据库,查问 todo 数据。
- 数据库返回 todo 数据。
- 模型将从数据库中查问的 todo 数据返回给控制器,控制器临时将数据保留在内存中。
- 控制器从视图中获取首页 HTML 模板。
- 控制器将从模型查出来的 todo 数据填充到首页 HTML 模板中,并组装成合乎 HTTP 标准的数据。
- 服务器返回响应。
其实 MVC
是一个宏观上的分层,具体细节局部还须要依据咱们设计程序的粒度来进行解决,比方有些逻辑既能够写在控制器层,也能够写在模型层,甚至咱们还能够在 MVC
的根底上扩大更多的分层。这些都须要联合具体的业务逻辑来决定。
构建 Todo List 程序
学习了 MVC
模式,咱们就能够依据 MVC
模式来试着构建 Todo List 程序了。
Todo List 程序分两局部:todo 治理、用户治理。在我的项目初期,咱们必定不会思考的太过全面。所以能够先不思考用户治理性能局部的实现,先只思考如何实现 todo 治理性能。
Todo List 程序目录结构设计如下:
todo_list
├── server.py
└── todo
├── __init__.py
├── config.py
├── controllers.py
├── db
│ └── todo.json
├── models.py
├── templates
│ ├── edit.html
│ └── index.html
└── utils.py
这里以 todo_list/
作为程序的根目录,根目录下蕴含 server.py
文件和 todo/
目录。其中 server.py
次要性能就是作为一个 Web Server
来接管申请和返回响应,它是 Todo List 程序的入口和进口。而 todo/
目录则是 Todo List 程序处理业务逻辑的外围。
todo/
目录下的 __init__.py
将 todo/
文件夹标记为一个 Python 包。config.py
用于存储一些我的项目的根底配置。utils.py
是一个工具集,外面能够定义一些供其余模块调用的类和办法。db/
目录作为 Todo List 程序存储数据的目录,db/todo.json
用来存储所有的 todo 内容。剩下还有两个 .py
文件和一个目录没有介绍,置信你曾经猜到了 models.py
、templates/
、controllers.py
别离对应了 MVC
模式中的模型、视图、控制器。models.py
中编写操作 todo 数据的代码,templates/
目录用来寄存 HTML 模板文件,templates/index.html
是首页,templates/edit.html
是编辑页面,controllers.py
编写负责程序控制的根底逻辑代码。
咱们对我的项目的目录构造有了一个概览,这里我要强调一下 db/
目录的作用。咱们在开发整个 Todo List 程序的过程中都不会应用理论的数据库程序,我的项目中所有须要存储的数据都保留在 db/
目录下的文件中。在开发 Web 程序时,须要用到数据库的目标就是为了存储数据,对于 Todo List 程序来说应用文件同样能满足需要,同时可能关照到对数据库不理解的读者。
Todo List 首页开发
Todo List 程序目录构造构建实现后就能够入手开发程序了,咱们能够从一个申请经验的过程来着手。
一个申请发送到服务器,首先服务器须要有一个可能接管申请的入口,在程序根目录 todo_list/
下的 server.py
就是这个入口。server.py
文件代码如下:
# todo_list/server.py
import socket
import threading
from todo.config import HOST, PORT, BUFFER_SIZE
def process_connection(client):
"""解决客户端申请"""
# 接管申请报文数据
request_bytes = b''
while True:
chunk = client.recv(BUFFER_SIZE)
request_bytes += chunk
if len(chunk) < BUFFER_SIZE:
break
# 申请报文
request_message = request_bytes.decode('utf-8')
print(f'request_message: {request_message}')
# TODO: 解析申请
# TODO: 返回响应
# 敞开连贯
client.close()
def main():
"""入口函数"""
with socket.socket() as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((HOST, PORT))
s.listen(5)
print(f'running on http://{HOST}:{PORT}')
while True:
client, address = s.accept()
print(f'client address: {address}')
# 创立新的线程来解决客户端连贯
t = threading.Thread(target=process_connection, args=(client,))
t.start()
server.py
程序简直就是之前实现的多线程版 Hello World 服务器程序照搬过去的。为了程序代码更加清晰,这里将服务器的 IP 地址、端口、接管申请的缓冲区大小定义为变量写在了配置文件 todo/config.py
中,所以须要在 server.py
文件顶部从配置文件中导入 HOST
、PORT
、BUFFER_SIZE
。在 main
函数中,实例化 socket
对象局部的代码也有所扭转,这里采纳了 with
语句来实例化 socket
对象,这样可能保障任何状况下退出程序时 socket
都可能被正确敞开。此处 with
语句的用法能够类比文件操作时的 with
语句。解决客户端连贯申请的 process_connection
函数外部根本逻辑没有扭转,其中有两个 TODO
正文示意解析申请和返回响应的性能暂未实现。
从 server.py
入口程序接管到客户端的申请当前,须要解析申请报文,并依据解析进去的申请报文来决定如何解决申请并返回响应。所以接下来咱们须要编写解析申请的代码。
不过在这之前,我先给出 todo/config.py
配置文件的代码,毕竟之后还会用到:
# todo_list/todo/config.py
import os
# todo/ 目录绝对路径
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# IP
HOST = '127.0.0.1'
# 端口
PORT = 8000
# 缓冲大小
BUFFER_SIZE = 1024
配置文件中除了蕴含后面介绍过的示意 IP 地址、端口、接管申请的缓冲区大小的几个变量,还有一个 BASE_DIR
变量用来示意 todo/
目录的绝对路径,不便在程序中获取我的项目门路。
当初来看下如何解析申请,咱们能够定义一个 Request
类用来专门解析申请报文,代码写在 todo/utils.py
文件中:
# todo_list/todo/utils.py
class Request(object):
"""申请类"""
def __init__(self, request_message):
method, path, headers = self.parse_data(request_message)
self.method = method # 申请办法 GET、POST
self.path = path # 申请门路 /index
self.headers = headers # 申请头 {'Host': '127.0.0.1:8000'}
def parse_data(self, data):
"""解析申请报文数据"""
# 用申请报文中的第一个 '\r\n\r\n' 做宰割,将失去申请头和申请体
# 申请体临时用不到先不解决
header, body = data.split('\r\n\r\n', 1)
method, path, headers = self._parse_header(header)
return method, path, headers
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, _ = request_line.split()
# 解析申请首部所有的键值对,组装成字典
headers = {}
for header in request_header.split('\r\n'):
k, v = header.split(':', 1)
headers[k] = v
return method, path, headers
Request
类的初始化办法 __init__
接管申请报文字符串作为参数。在其外部调用 parse_data
办法将申请报文字符串解析成咱们须要的结构化数据。
解析完申请报文,咱们须要依据申请报文信息来判断如何返回响应。根底逻辑判断局部的代码能够写在 todo/controllers.py
中:
# todo_list/todo/controllers.py
from todo.utils import render_template
def index():
"""首页视图函数"""
return render_template('index.html')
定义在控制器层的函数也叫视图函数,因为它们通常返回视图层的 HTML 内容。index
视图函数用来解决申请首页的逻辑,它返回 render_template
函数的调用后果,render_template
函数的作用是将 HTML 内容读取成字符串并返回,其定义如下:
# todo_list/todo/utils.py
import os
from todo.config import BASE_DIR
def render_template(template):
"""读取 HTML 内容"""
# 读取 '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 = f.read()
return html
在 todo/controllers.py
文件底部还定义了一个 routes
字典,字典的键为申请门路,值为一个元组,元组的第一个元素作为解决申请的函数,第二个元素是一个列表,外面定义解决申请的函数所容许的申请办法。index
视图函数可能同时匹配两个门路:/
、/index
,因为这两个门路通常都代表首页。
# todo_list/todo/controllers.py
routes = {'/': (index, ['GET']),
'/index': (index, ['GET']),
}
读取出 HTML 内容当前,咱们就能够结构响应报文并返回给浏览器了。在 utils.py
文件下,编写一个 Response
类用来结构响应:
# todo_list/todo/utils.py
class Response(object):
"""响应类"""
# 依据状态码获取起因短语
reason_phrase = {
200: 'OK',
405: 'METHOD NOT ALLOWED',
}
def __init__(self, body, headers=None, status=200):
# 默认响应首部字段,指定响应内容的类型为 HTML
_headers = {'Content-Type': 'text/html; charset=utf-8',}
if headers is not None:
_headers.update(headers)
self.headers = _headers # 响应头
self.body = body # 响应体
self.status = status # 状态码
def __bytes__(self):
"""结构响应报文"""
# 状态行 'HTTP/1.1 200 OK\r\n'
header = f'HTTP/1.1 {self.status} {self.reason_phrase.get(self.status,"")}\r\n'
# 响应首部
header += ''.join(f'{k}: {v}\r\n' for k, v in self.headers.items())
# 空行
blank_line = '\r\n'
# 响应体
body = self.body
response_message = header + blank_line + body
return response_message.encode('utf-8')
Response
类的初始化办法 __init__
接管三个参数,别离为响应体、响应首部字段、状态码。其中响应体为 str
类型,首页的响应体实际上就是 index.html
文件内容。响应首部字段为 dict
类型,在结构响应报文时,所有的响应首部字段最终依照 HTTP 标准拼接到一起作为响应首部。状态码为数值类型,目前只思考了状态码为 200
失常响应和 405
申请办法不被容许。
须要留神的是,Response
类定义了 __bytes__
魔法办法作为结构响应报文的办法。当应用 Python 内置的 bytes
办法转换 Response
实例对象时(bytes(Response())
),会主动调用 __bytes__
魔法办法。
从解析申请到结构响应报文的代码当初曾经根本编写实现。接下来咱们将整个解决申请的流程串联起来,回到 server.py
文件,持续欠缺代码:
# todo_list/server.py
import socket
import threading
from todo.config import HOST, PORT, BUFFER_SIZE
from todo.utils import Request, Response
from todo.controllers import routes
def process_connection(client):
"""解决客户端申请"""
# 接管申请报文数据
request_bytes = b''
while True:
chunk = client.recv(BUFFER_SIZE)
request_bytes += chunk
if len(chunk) < BUFFER_SIZE:
break
# 申请报文
request_message = request_bytes.decode('utf-8')
print(f'request_message: {request_message}')
# 解析申请报文,结构申请对象
request = Request(request_message)
# 依据申请对象结构响应报文
response_bytes = make_response(request)
# 返回响应
client.sendall(response_bytes)
# 敞开连贯
client.close()
def make_response(request, headers=None):
"""结构响应报文"""
# 默认状态码为 200
status = 200
# 获取匹配以后申请门路的处理函数和函数所接管的申请办法
# request.path 等于 '/' 或 '/index' 时,routes.get(request.path) 将返回 (index, ['GET'])
route, methods = routes.get(request.path)
# 如果申请办法不被容许,返回 405 状态码
if request.method not in methods:
status = 405
data = 'Method Not Allowed'
else:
# 申请首页时 route 实际上就是咱们在 controllers.py 中定义的 index 视图函数
data = route()
# 获取响应报文
response = Response(data, headers=headers, status=status)
response_bytes = bytes(response)
print(f'response_bytes: {response_bytes}')
return response_bytes
def main():
"""入口函数"""
with socket.socket() as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((HOST, PORT))
s.listen(5)
print(f'running on http://{HOST}:{PORT}')
while True:
client, address = s.accept()
print(f'client address: {address}')
# 创立新的线程来解决客户端连贯
t = threading.Thread(target=process_connection, args=(client,))
t.start()
if __name__ == '__main__':
main()
首先实现之前未写完的 process_connection
函数。将原来标记 TODO
正文的中央替换成了如下代码:
# 解析申请报文,结构申请对象
request = Request(request_message)
# 依据申请对象结构响应报文
response_bytes = make_response(request)
# 返回响应
client.sendall(response_bytes)
新增了一个 make_response
函数,不便用来依据申请对象结构响应报文。函数中我写了比拟具体的正文,你能够依据正文内容读懂代码逻辑。
最初给出首页 todo/templates/index.html
的 HTML 代码:
<!--todo_list/todo/templates/index.html-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Todo List</title>
<style>
* {
margin: 0;
padding: 0;
}
ul {list-style: none;}
a {
text-decoration: none;
outline: none;
color: #000000;
}
h1 {margin: 20px auto;}
.container {
display: flex;
justify-content: center;
align-items: center;
}
.container ul {
width: 100%;
max-width: 600px;
}
.container ul li {
height: 40px;
line-height: 40px;
margin-bottom: 4px;
padding: 0 6px;
display: flex;
justify-content: space-between;
background-color: #d2d2d2;
}
</style>
</head>
<body>
<h1 class="container">Todo List</h1>
<div class="container">
<ul>
<li>
<div>Hello World</div>
</li>
</ul>
</div>
</body>
</html>
HTML 代码比较简单,其中顶部写了一些根底的 CSS 款式,都很容易看懂,这里不再解说。
接下来在终端中,进入 todo_list/
目录下,应用 Python 运行 server.py
文件,看到如下打印后果阐明程序曾经失常启动:
关上浏览器,地址栏输出 http://127.0.0.1:8000/
或者 http://127.0.0.1:8000/index
,你将看到 Todo List 程序首页:
至此,Todo List 程序首页初步实现。不过我想很多读者看到这里会产生纳闷,说好的 MVC
呢,目前为止咱们并没有编写一行模型层的代码,并且首页的 HTML 内容也不是动静填充的。没错,为了可能尽快让 Todo List 程序跑起来,我无意的避开了这两个问题,下一章咱们再来解决这两个问题。
本章源码:chapter3
分割我:
- 微信:jianghushinian
- 邮箱:jianghushinian007@outlook.com
- 博客地址:https://jianghushinian.cn/