• flask实战-留言板-Web程序开发流程 --


    Web程序开发流程

    在实际的开发中,一个Web程序的开发过程要设计多个角色,比如客户(提出需求)、项目经理(决定需求的实现方式)、开发者(实现需求)等,在这里我们假设自己是一个人全职开发。一般来说一个web程序的开发流程如下所示:

    1)  分析需求,列出功能清单或写需求说明书

    2)  设计程序功能,写功能规格书和技术规格书

    3)  进入开发和测试的迭代

    4)  调试和性能等专项测试

    5)  部署上线

    6)  运行维护与营销等

    写好功能规格书后,我们就可以进行实际的代码编写。在具体的开发中,代码编写主要分为前端页面(front end)和后端程序(back end)。前端开发的主要流程如下:

    1)  根据功能规格书画页面草图(sketching)

    2)  根据草图做交互式原型图(prototyping)

    3)  根据原型图开发前端页面(HTML、CSS、 JavaScript)

    后端开发的主要流程如下:

    1)  数据库建模

    2)  编写表单类

    3)  编写视图函数和相关的处理函数

    4)  在页面中使用Jinja2替换虚拟数据

    采用这个流程并不是必须的,对于简单的程序,你可以根据情况来省略某些步骤。如果不是只有简单的几个页面的玩具程序,那么最好遵循这个过程进行开发。因为如果没有规划,就像没头苍蝇一样乱飞乱撞,最终开发出不完善的程序,或是添加了无关紧要的功能。从一开始就遵循开发流程,可以让你很容易适应大型程序的开发。想象一下,在大型程序里常常有着复杂的数据库关系,大量的页面和功能,想到哪写到哪会将大量时间都浪费在无意义的调试和删改中。前期考虑和规划越周全,在实际开发时就可以约高效和省力。

    为了便于组织内容,开发时非常重要的测试,后续在介绍,但是在实际开发中应该讲测试融入整个开发流程中:编写一部分代码,立刻编写对应的测试。

    程序功能设计

    规划和设计程序功能时,我们通常会使用思维导图工具或是清单工具。因为messageBoard很简单,这里创建一个非常简短的功能规格书,如下所示:

    概述

    messageBoard是一个类似于留言板的程序,用来让用户发表问候,对任何人任何事的问候。比如,用户A想问候这个世界,就可以在页面上发表依据”Hello, World!”。messageBoard的使用流程非常简单,我们甚至不需要画流程图。用户输入问候信息和姓名,按下提交按钮,就可以将问候加入页面的消息列表中。

    主页

    主页是messageBoard唯一的页面,页面中包含创建留言的表达单以及所有的问候消息。页面上方是程序的标题”messageBoard”,使用大字号和鲜艳的颜色。页面底部包含程序的版权标志、编写者、源码等相关信息。

    问候表单

    这个表单包含姓名和问候消息两个字段,其中姓名字段是普通的文本字段<input type=”text”>,而消息字段是文本区域字段<textarea></textarea>。为了获得良好的样式效果,对这两个字段的输入值进行长度上的限制,姓名最长为20个字符,而问候消息最长为200个字符。

    用户提交发布表单后:

    1)  如果验证出错,错误消息以红色小字的形式显示在字段下面

    2)  如果通过验证,则在程序标题下面显示一个提示消息,用户可以通过消息右侧的按钮关闭提示

    问候消息列表

    问候消息列表的上方显示所有消息的数量。每一条问候消息要包含的消息有发布者姓名、消息正文、发布的时间、消息的编号。消息发布时间要显示相对时间,比如“3分钟前”,当鼠标悬停在时间上时,弹出窗口显示具体的时间值。消息根据时间先后排序,最新发表的排在最上面。为了方便用户查询最早的消息,我们提供了一个前往页面底部的按钮,同时提供一个回到页面顶部的按钮。

    错误页面

    错误页面包括404错误页面和500错误,和主页包含相同的部分—程序标题。程序标题下显示错误信息以及一个返回主页的“Go Back”连接。为了保持简单,错误页面不加入页脚信息。

    前端页面开发

    在前面列出的流程中,我们首先使用纸笔画草图,然后使用原型设计软件画出原型图,最后编写对应的HTML页面。根据程序的页面数量和复杂程度,可以按需调整。

    在传统的Flask程序中,后段完成功能后会操作HTML代码,在其中添加Jinja2语句。比如,将页面中的临时URL替换为url_for()函数调用,把虚拟数据替换成通过视图函数传入模板的变量,或是使用模板继承等技术组织这些HTML文件。

    下面是原作者设计的界面:

     

    后端程序开发
    1、数据库建模

    编写完功能规格书后,我们也就确定了需要使用那些表来存储数据,表中需要创建哪些字段以及各个表之间的关系。对于复杂的数据库结构,你可以使用建模工具来辅助建立数据库关系。在messageBoard中,用于保存留言的Message模型如下所示:

    messageBoard/messageBoard/models.py:

    from datetime import datetime
    from messageBoard import db
    
    class Message(db.Model):
        id = db.Column(db.Integer, primary_key=True)
        body = db.Column(db.String(200))
        name = db.Column(db.string(20))
        timestamp = db.Column(db.DateTime, default=datetime.now, index=True)
     

    timestamp字段用来存储每一条留言的发表时间(时间戳),这个字段存储Python的datetime对象。在这个字段中,我们将index设为True来开启索引,并使用default参数设置了字段默认值。

    timestamp字段的默认值是datetime.now而不是datetime.now(),前者是可调用的函数/方法对象(即名称),而后者是函数/方法的调用(即动作)。SQLAlchemy会在创建新的数据记录时(即用户提交表单实例化Message类时)调用该对象来设置默认值,这也是我们期待的效果。如果传入的不是方法对象,那么这个方法在加载模块式就会被执行,这将不是正确的时间戳。

    为了方便在开发时重新创建数据库表,我们还添加了一个初始化数据库的initdb命令,和前面介绍过的initdb()命令函数完全相同。

    2、创建表单类

    问候表单由表单类HelloForm表示,表单中使用了文本区域字段TextAreaField,表示HTML中的<textarea>标签,如下所示:

    messageBoard/messageBoard/forms.py:

    from flask_wtf import FlaskForm
    from wtforms import StringField, SubmitField, TextAreaField
    from wtforms.validators import DataRequired, Length
    
    class HelloForm(FlaskForm):
        name = StringField('Name', validators=[DataRequired(), Length(1, 20)])
        body = TextAreaField('Message', validators=[DataRequired(), Length(1,200)])
        submit = SubmitField()
    3、编写视图函数

    重点介绍一下index视图。index视图有两个作用:

    1)  处理GET请求,从数据库中查询所有的消息记录,返回渲染后的包含消息列表的主页模板index.html

    2)  处理POST请求,问候表单提交后,验证表单数据,通过验证后将数据保存到数据库中,使用flash()函数显示一条提示,然后重定向到index视图,渲染页面

    index视图如下:

    messageBoard/messageBoard/views.py:

    from flask import flash, redirect, url_for, render_template
    
    from messageBoard import app, db
    from messageBoard.models import Message
    from messageBoard.forms import HelloForm
    
    @app.route('/', methods=['GET', 'POST'])
    def index():
        #  加载所有的记录
        messages = Message.query.order_by(Message.timestamp.desc()).all()
        form = HelloForm()
        if form.validate_on_submit():
            name = form.name.data
            body = form.body.data
            message = Message(body=body, name = name)  #实例化Message模型类(表),创建记录
            db.session.add(message)  #添加记录到数据库会话
            db.session.commit()  #提交会话
            flash('Your message have been sent to the world!')
            return redirect(url_for('index'))  #重定向到index视图
        return render_template('index.html', form=form, messages=messages)  #渲染模板,处理get请求

    在获取message记录时,我们使用order_by()过滤器对数据库记录进行排序,参数是排序的规则。我们根据Message模型的timestamp字段值排序,字段上附加的排序方法为desc(),代表降序(descending),同样还有一个asc()方法表示升序(ascending)。

    4、编写模板

    我们将 index.html和404.html,以及500.html中的共有部分抽出合并为基模板base.html。

    基模板包含一个完整的HTML结构,我们在其中创建了几个块:title、content和footer,如下所示:

    messageBoard/messageBoard/templates/base.html:基模板

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <title>{% block title %}How you are doing ?{% endblock %}</title>
        <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
        <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}" type="text/css">
    </head>
    <body>
    <main class="container">
        <header>
            <h1 class="text-center display-4">
                <a href="{{ url_for('index') }}" class="text-success"><strong>Leave Message</strong></a>
                <small style="font-size:24px" class="text-muted">to the world</small>
            </h1>
        </header>
        {% for message in get_flashed_messages() %}
        <div class="alert alert-info">
            <button type="button" class="close" data-dismiss="alert">&times;</button>
            {{ message }}
    
        </div>
        {% endfor %}
        {% block content %}{% endblock %}
        <footer class="text-center">
            {% block footer %}
                <small> &copy; 2019 <a href="https://www.cnblogs.com/xiaxiaoxu/" title="xiaxiaoxu's blog">夏晓旭的博客</a> /
                    <a href="https://github.com/xiaxiaoxu/hybridDrivenTestFramework" title="Contact me on GitHub">GitHub</a> /
                    <a href="http://helloflask.com" title="A HelloFlask project">Learning from GreyLi's HelloFlask</a>
                </small>
            {% endblock %}
        </footer>
    </main>
    
    </body>
    </html>
    
    
    在head里引入了之前例子中用的style.css(需要修改)
    在主页模板index.html中,我们使用form_field()宏渲染表单,然后遍历传入的messages变量,渲染消息列表,如下所示:

    templates/index.html:渲染表单和留言列表

    {% extends 'base.html' %}
    {% from 'macros.html' import form_field %}
    
    {% block content %}
    <div class="hello-form">
        <form method="post" action="{{ request.full_path }}">
            {{ form.csrf_token }}
            <div class="form-group required">
                {{ form_field(form.name, class='form-control') }}
            </div>
            <div class="form-group required">
                {{ form_field(form.body, class='form-control') }}
            </div>
            {{ form.submit(class='btn btn-secondary') }}
        </form>
    </div>
    <h5>{{ messages|length }} messages
        <small class="float-right">
            <a href="#bottom" title="Go Bottom">&darr;</a>
        </small>
    </h5>
    <div class="list-group">
        {% for message in messages %}
            <a class="list-group-item list-group-item-action flex-column">
                <div>
                    <h5 class="mb-1 text-success">{{ message.name }}
                    <small class="text-muted">#{{ loop.revindex }}</small>
                    </h5>
                    <small>
                           {{ message.timestamp.strftime('%Y/%m/%d %H:%M') }}
                    </small>
                </div>
                <p class="mb-1">{{ message.body }}</p>
            </a>
        {% endfor %}
    </div>
    {% endblock %}
    
    
    表单默认提交到当前URL,如果用户单击了向下按钮,会在URL中添加URL片段(后面会了解),比如“#bottom”,它指向页面底部的a元素(其id值为bottom),所以会跳转到页面底部。当表单被提交后,页面加载时仍会跳转到URL片段对应的位置,为了避免这个行为,可以显示地使用action属性指定表单提交的目标URL,使用request.full_path获取没有URL片段的当前请求URL。
    {{ loop.revindex }}是jinja2的循环内置变量,表示循环迭代倒序计数(从len开始,到1结束),常用的jinja2循环内置变量如下图:

    
    
    渲染时间戳时,我们使用datetime.strftime()方法将时间戳输出格式定义为:“年/月/日时:分”,这显然不是我们设计功能时想要的时间,在后面我们借助其他工具来获取相对时间并显示绝对时间弹窗。除了时间戳外,我们还渲染了loop.revindex变量,用来表示留言的反向序号标记。
    目前的效果是这样的:很丑

    
    
    其他程序文件:

    messageBoard/messageBoard/__init__.py:

    #encoding=utf-8
    
    from flask import Flask
    from flask_sqlalchemy import SQLAlchemy
    
    app = Flask('messageBoard')
    app.config.from_pyfile('settings.py')
    app.jinja_env.trim_blocks = True
    app.jinja_env.lstrip_blocks = True
    
    db = SQLAlchemy(app)
    
    from messageBoard import views, errors, commands
    
    

    messageBoard/commands.py

     
    #encoding=utf-8
    import click
    
    from messageBoard import app, db
    from messageBoard.models import Message
    
    @app.cli.command()
    @click.option('--drop', is_flag=True, help='Create after drop.')
    def initdb(drop):
        """Initialize the database."""
        if drop:
            click.confirm('This operation will delete the database, do you want to continue?', abort=True)
            db.drop_all()
            click.echo('Drop tables.')
        db.create_all()
        click.echo('Initialized database.')
    
    @app.cli.command()
    @click.option('--count', default=20, help='Quantity of messages, default is 20.')
    def forge(count):
        """Generate fake messages."""
        from faker import Faker
    
        db.drop_all()
        db.create_all()
    
        fake = Faker()
        click,echo('Working...')
    
        for i in range(count):
            message = Message(
                name = fake.name(),
                body = fake.sentence(),
                timestamp = fake.date_time_this_year()
            )
            db.session.add(message)
    
        db.session.commit()
        click.echo('Created %d fake messages.' % count)
    
    

    messageBoard/forms.py

    #encoding=utf-8
    
    from flask_wtf import FlaskForm
    from wtforms import StringField, SubmitField, TextAreaField
    from wtforms.validators import DataRequired, Length
    
    class HelloForm(FlaskForm):
        name = StringField('Name', validators=[DataRequired(), Length(1, 20)])
        body = TextAreaField('Message', validators=[DataRequired(), Length(1,200)])
        submit = SubmitField()
    加了bootstrap和js文件的index.html和base.html:
    base.html:
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <title>{% block title %}How you are doing ?{% endblock %}</title>
        <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
        <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}" type="text/css">
        <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}" type="text/css">
    </head>
    <body>
    <main class="container">
        <header>
            <h1 class="text-center display-4">
                <a href="{{ url_for('index') }}" class="text-success"><strong>Leave Message</strong></a>
                <small class="text-muted sub-title">to the world</small>
            </h1>
        </header>
        {% for message in get_flashed_messages() %}
        <div class="alert alert-info">
            <button type="button" class="close" data-dismiss="alert">&times;</button>
            {{ message }}
    
        </div>
        {% endfor %}
        {% block content %}{% endblock %}
        <footer class="text-center">
            {% block footer %}
                <small> &copy; 2019 <a href="https://www.cnblogs.com/xiaxiaoxu/" title="xiaxiaoxu's blog">夏晓旭的博客</a> /
                    <a href="https://github.com/xiaxiaoxu/hybridDrivenTestFramework" title="Contact me on GitHub">GitHub</a> /
                    <a href="http://helloflask.com" title="A HelloFlask project">Learning from GreyLi's HelloFlask</a>
                </small>
                <p><a id="bottom" href="#" title="Go Top">&uarr;</a></p>
            {% endblock %}
        </footer>
    </main>
    <script type="text/javascript" src="{{ url_for('static', filename='js/jquery-3.2.1.slim.min.js') }}"></script>
    <script type="text/javascript" src="{{ url_for('static', filename='js/popper.min.js') }}"></script>
    <script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap.min.js') }}"></script>
    <script type="text/javascript" src="{{ url_for('static', filename='js/script.js') }}"></script>
    {{ moment.include_moment(local_js=url_for('static', filename="js/moment-with-locales.min.js")) }}
    </body>
    </html>

    index.html:

    {% extends 'base.html' %}
    {% from 'bootstrap/form.html' import render_form %}
    
    {% block content %}
        <div class="hello-form">
            {{ render_form(form, action=request.full_path) }}
        </div>
        <h5>{{ messages|length }} messages
            <small class="float-right">
                <a href="#bottom" title="Go Bottom">&darr;</a>
            </small>
        </h5>
        <div class="list-group">
            {% for message in messages %}
                <a class="list-group-item list-group-item-action flex-column">
                    <div class="d-flex w-100 justify-content-between">
                        <h5 class="mb-1 text-success">{{ message.name }}
                            <small class="text-muted"> #{{ loop.revindex }}</small>
                        </h5>
                        <small data-toggle="tooltip" data-placement="top"
                               data-timestamp="{{ message.timestamp.strftime('%Y-%m-%dT%H:%M:%SZ') }}"
                               data-delay="500">
                            {{ moment(message.timestamp).fromNow(refresh=True) }}
                        </small>
                    </div>
                    <p class="mb-1">{{ message.body }}</p>
                </a>
            {% endfor %}
        </div>
    {% endblock %}
    
    
    在head标签和body标签内,引入了Bootstrap所需的CSS和JavaScript文件
    ,以及Bootstrap所依赖的jQuery和Popper.js。另外,我们还引入自定义的style.css和script.js文件,这两个文件分别用来存储自定义的CSS样式定义和JavaScript代码。
    这里为消息应用了Bootstrap提供的alert-info样式(蓝色背景),后边会学习对flash消息添加分类,以便对不同类别的消息应用不同的样式。
     
  • 相关阅读:
    第十周作业
    课堂练习之十字链表
    第三次实验报告
    第九周学习
    队列课下作业
    第七周作业
    第五周作业总结(内含用Junit测试ArrayStack和LinkedStack课堂练习报告)
    20155336 《信息安全系统设计基础》课程总结
    2017-2018-1 20155336 《信息安全系统设计基础》第十四周学习总结
    2017-2018-1 20155336 《信息安全系统设计基础》第十三周学习总结
  • 原文地址:https://www.cnblogs.com/xiaxiaoxu/p/10771664.html
Copyright © 2020-2023  润新知