章节索引 :

Flask 项目实战 3: 前端实现

待做清单程序的总体结构分为前端和后端两个部分,上一节介绍了后端的实现,本节讲解前端的实现。

1. 首页模板 templates/index.html

templates/index.html 是网站首页的模板,包括如下几个部分:

1.1 引入相关的库

<html>
<head>
  <meta charset='utf-8'>
  <script src="https://lib.baomitu.com/jquery/2.2.4/jquery.min.js"></script>
  <link href="https://lib.baomitu.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
  <link href="{{url_for('static', filename='style.css')}}" rel="stylesheet">
  <script src="{{url_for('static', filename='script.js')}}"></script>
  <title>每日清单</title>
</head>

在第 4 行和第 5 行,引入 JQuery 和 font-awesome 库。

在第 6 行,引入网站的样式文件 static/style.css,函数 url_for(‘static’, filename=‘style.css’) 表示 static 文件夹下的文件 style.css,它的输出为 static/style.css。

在第 7 行,引入 JS 文件 static/script.js,函数 url_for(‘static’, filename=‘script.js’) 表示 static 文件夹下的文件 static/script.js,它的输出为 static/script.js。

1.2 显示 “登录/注册” 按钮

<body>
  <div class='header'>
    <i class="fa fa-calendar-plus-o"></i> 待做清单
      
    {% if hasLogin %}
    <span class='login'>
      <i class="fa fa-sign-out"></i> 
      <a href='/users/logout'>退出</a>
    </span>
    {% else %}
    <span class='login'>
      <i class="fa fa-sign-in"></i> 
      <a href='/users/login'>登录</a>
       
      <i class="fa fa-user-plus"></i> 
      <a href='/users/register'>注册</a>
    </span>
    {% endif %}
  </div>

如果用户没有登录,网站首页的显示 “登录/注册” 按钮;如果用户已经登录,网站首页的显示 “退出” 按钮。

在第 5 行,变量 hasLogin 标记用户是否登录,根据 hasLogin 是否为真显示不同的界面。

1.3 输入待做事项的文本框

  {% if hasLogin %}
  <div>
    <input type="text" class="row" placeholder="输入待办事项">
    <i class="fa fa-fw fa-plus-square" onclick='onAddTodo(this);'></i>
  </div>
  {% endif %}

如果 hasLogin 为真,用户已经登录,显示一个文本框,用于输入待做事项。

在第 4 行,显示了一个 font awsome 图标 plus-square,点击该图标,调用函数 onAddTodo(this) 向后端请求新增一个待做事项。

1.4 待做清单和完成清单

  {% for todo in todos %}
  <div>
    <input type="text" class="row" value="{{todo.title}}">
    <i class="fa fa-fw fa-check" onclick='onUpdateTodo({{todo.todoId}});'></i>
  </div>
  {% endfor %}

  <div class='header'>
    <i class="fa fa-calendar-check-o"></i> 完成清单
  </div>

  {% for done in dones %}
  <div>
    <input type="text" class="row" value="{{done.title}}">
    <i class="fa fa-fw fa-trash" onclick='onDeleteTodo({{done.todoId}});'></i>
  </div>
  {% endfor %}
</body>
</html>

页面模板中有 2 个变量:todos 和 dones,todos 是 status 等于 ‘todo’ 的待做事项列表,dones 是 status 等于 ‘done’ 的待做事项列表。

在第 1 行,遍历 todos,展示所有的待做事项;在第 4 行,显示了一个 font awsome 图标 check,点击该图标表示已经完成该项,将待做事项移入到完成清单中,调用函数 onUpdateTodo(todo.todoId) 向后端请求更新待做事项的 status 为 ‘done’。

在第 12 行,遍历 dones,展示所有的完成事项;在第 15 行,显示了一个 font awsome 图标 delete,点击该图标,调用函数 onDeleteTodo(todo.todoId) 向后端请求删除待做事项。

2 注册页面模板 templates/register.html

注册页面 templates/register.html 显示一个注册表单,由如下部分构成:

2.1 引入相关文件

<html>
<head>
  <meta charset='UTF-8'>
  <link href="https://lib.baomitu.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
  <link href="{{url_for('static', filename='style.css')}}" rel="stylesheet">
  <title>注册</title>
</head>

在第 4 行和第 5 行,引入font-awesome 库。

在第 5 行,引入网站的样式文件 static/style.css,函数 url_for(‘static’, filename=‘style.css’) 表示 static 文件夹下的文件 style.css,它的输出为 static/style.css。

2.2 注册表单

<body>
  <h3><i class='fa fa-user-plus'></i> 注册</h3>
  <form action="/users/register" method="POST">
    <div class="row">
      {{ form.name.label }}
      {{ form.name() }}
      <b>{{ form.name.errors[0] }}</b>
    </div>

    <div class="row">
      {{ form.password.label }}
      {{ form.password() }}
      <b>{{ form.password.errors[0] }}</b>
    </div>

    <div class="row">
      {{ form.submit() }}
    </div>

    {{ form.hidden_tag() }}
  </form>
</body>
</html>

form 是注册表单,包括 3 个字段:name、password、隐藏字段,根据 form 中字段 email 和 password 的属性,它被渲染为如下的 HTML 文件:

<html>
<head>
  <meta charset='UTF-8'>
  <link href="https://lib.baomitu.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
  <link href="/static/style.css" rel="stylesheet">
  <title>登录</title>
</head>

<body>
  <div class="header"><i class='fa fa-sign-in'></i> 登录</div>
  <form action="/users/login" method="POST">
    <div class="row">
      <label for="name">姓名</label>
      <input id="name" name="name" required type="text" value="">
      <b></b>
    </div>

    <div class="row">
      <label for="password">密码</label>
      <input id="password" name="password" required type="password" value="">
      <b></b>
    </div>

    <div class="row">
      <input id="submit" name="submit" type="submit" value="登录">
    </div>

    <input id="csrf_token" name="csrf_token" type="hidden" value="ImRlYTZjZDEwZjU3YjNjNGY0MDVkMDc4ZDhiZTMwNWM1OTk2MjhiMzAi.X2LvVA.0x7iz2PGVHH-r8dWf7KQNMkuSAE">
  </form>
</body>
</html>

这里注意两点:

  • form.email.errors 和 form.password.errors 是一个错误信息列表,errors[0] 表示第一条错误信息;
  • form.hidden_tag() 用于防范 CSRF 攻击,生成 <input id=“csrf_token”/> 标签,请参考相关词条。

2.3 登录页面模板 templates/login.html

登录页面 templates/login.html 显示一个登录表单,代码如下:

<html>
<head>
  <meta charset='UTF-8'>
  <link href="https://lib.baomitu.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
  <link href="{{url_for('static', filename='style.css')}}" rel="stylesheet">
  <title>登录</title>
</head>

<body>
  <div class="header"><i class='fa fa-sign-in'></i> 登录</div>
  <form action="/users/login" method="POST">
    <div class="row">
      {{ form.name.label }}
      {{ form.name() }}
      <b>{{ form.name.errors[0] }}</b>
    </div>

    <div class="row">
      {{ form.password.label }}
      {{ form.password() }}
      <b>{{ form.password.errors[0] }}</b>
    </div>

    <div class="row">
      {{ form.submit() }}
    </div>

    {{ form.hidden_tag() }}
  </form>
</body>
</html>

登录页面 templates/login.html 与注册页面 templates/register.html 几乎完全相同,除了 title 标签不一样。请参考对 templates/register.html 的解释。

3. 调用后端服务 static/script.js

文件 script.js 定义了调用后端服务的 Ajax 函数,由如下部分构成:

3.1 配置 Ajax

function ajaxError()
{
    alert('ajax error');
}

function ajaxSuccess(result)
{
    if (result.error) {
        alert('操作失败');
        return;
    }
    location.reload();
}

客户端使用 ajax 技术请求服务端的服务。当 ajax 请求失败时,调用 ajaxError,提示用户 ajax 请求服务失败;当 ajax 请求成功时,调用 ajaxSuccess,提示用户 ajax 请求服务成功。

3.2 新增待做事项

function onAddTodo(button)
{
    var children = $(button).parent().children();
    var title = children.eq(0).val();
    var data = JSON.stringify({'title': title});
    
    $.ajax({
        'url': '/todos/add',
        'type': 'POST',
        'contentType': 'application/json',
        'data': data,
        'dataType': 'json',
        'error': ajaxError,
        'success': ajaxSuccess
    });
}

点击 “新增” 按钮后,执行函数 onAddTodo(button),button 指向的是 “新增” 按钮。在 templates/index.html 中,按钮、待做事项位于相同的 DIV 中,如下所示:

  <div>
    <input type="text" class="row" placeholder="输入待办事项">
    <i class="fa fa-fw fa-plus-square" onclick='onAddTodo(this);'></i>
  </div>

在第 3 行到第 4 行,表达式的含义如下所示:

表达式 含义
$(button).parent() 指向按钮的父节点
$(button).parent().children() 表示 div 的 2 个子节点
children.eq(0) 指向待做事项的文本框
children.eq(0).val() 待做事项的文本框的值

在第 7 行,通过 JQuery 的 ajax 函数调用后端服务,设置 url 为 ‘/todos/add’、type 为 ‘POST’ ,表示新增一条待做事项。

3.3 更新待做事项

function onUpdateTodo(todoId) 
{
    var data = JSON.stringify({'todoId': todoId});
    
    $.ajax({
        'url': '/todos/update',
        'type': 'POST',
        'contentType': 'application/json',
        'data': data,
        'dataType': 'json',
        'error': ajaxError,
        'success': ajaxSuccess
    });
}

当用户完成一个待做事项后,将待做事项的 status 从 ‘todo’ 更改为 ‘done’。

在第 5 行,通过 JQuery 的 ajax 函数调用后端服务,设置 url 为 ‘/todos/update’、type 为 ‘POST’ ,更新待做事项的 status。

3.4 删除待做事项

function onDeleteTodo(todoId)
{
    var data = JSON.stringify({'todoId': todoId});
    
    $.ajax({
        'url': '/todos/delete',
        'type': 'POST',
        'contentType': 'application/json',
        'data': data,
        'dataType': 'json',
        'error': ajaxError,
        'success': ajaxSuccess
    });
}

在第 5 行,通过 JQuery 的 ajax 函数调用后端服务,设置 url 为 ‘/todos/delete’、type 为 ‘POST’ ,删除待做事项。

4. 样式文件 static/style.css

style.css 是网站的样式文件,设置尺寸、字体大小等信息。

body {
    width: 400px;
    margin: auto;
}

a {
    text-decoration: none;
}

.fa {
    cursor: pointer;
}

.login {
    font-size: 80%;
    font-weight: normal;
}

.header {
    padding-top: 8px;
    padding-bottom: 8px;
    font-size: 120%;
    font-weight: bold;
}

.row {
    width: 360px;
    padding-top: 3px;
    padding-bottom: 3px;
    margin-top: 2px;
    margin-bottom: 2px;
}

对标签 body 设置,margin: auto 表示将内容居中显示,两侧的 margin 相等,根据内容的宽度和屏幕的宽度自动计算 margin 的大小。

对标签 a 设置,不显示下划线。

对 class 等于 fa 的元素设置,显示光标的形状为手形。

登录、注册、退出的按钮的 class 等于 login,显示的 font-size 为 80%,略小于正常的尺寸。

待做事项和完成事项的 class 等于 row,设置上下的 padding 和 margin,让它们互相之间存在一个间隔。

5. 小结

本节讲解了前端的实现,使用思维导图概括如下:

图片描述