这一章,咱们来实现 todo 治理性能的残余局部:新增、批改和删除性能。

新增 todo

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

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

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

首页 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 类,使其可能解析 GETPOST 申请中携带的参数:

# todo_list/todo/utils.pyfrom 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 地址。

重定向状态码最常见的两个就是 301302301 代表永恒重定向,302 代表长期重定向。咱们我的项目适宜应用长期重定向,所以 redirect 函数的 status 参数默认值为 302

同时还须要批改 Response 响应类,使其可能解决 302 状态码:

# todo_list/todo/utils.pyclass Response(object):    """响应类"""    # 依据状态码获取起因短语    reason_phrase = {        200: 'OK',        302: 'FOUND',        405: 'METHOD NOT ALLOWED',    }        ...

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

# todo_list/todo/models.pyimport osimport jsonfrom todo.config import BASE_DIRclass 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.pydef 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 的逻辑如下:

  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 如下:

<!-- 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.pyclass 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.pydef 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 的逻辑如下:

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

首页 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.pyclass 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.pydef 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/