flask开发一个留言板

注:本文主要是参照李辉大大《Flask Web开发实战》一书第7章——留言板所写,基本上都是照抄啦,主要是做笔记用,有不清楚的地方可以去查阅原文,同时附上留言板项目的Github仓库地址

1、使用包组织程序代码

使用包组织程序代码,创建程序实例,初始化扩展等操作都可以在程序包的构造文件(init.py)中实现,代码如下

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask('sayhello')  
# 在单脚本中可以直接传入__name__,Flask由此来确定程序路径,所以在使用包组织代码的时候,以硬编码的方式传入包名称作为程序名称
# 在创建程序对象后,使用config对象的from_pyfile方法即可加载配置文件,传入配置文件名作为参数
app.config.from_pyfile('settings.py')
app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True

db = SQLAlchemy(app)  # 使用SQLAIchemy读取app对象生成数据库对象

from sayhello import views,errors,commands

为什么要在最后其他脚本呢?
1、在init构造文件中导入这些模块,将这些函数和程序实例关联起来
2、因为这些模块也需要从构造函数中导入程序实例,所以为了避免循环依赖,这些导入语句在构造文件的末尾定义

  • 小贴士
    在其他文件中导入构造文件中的变量,不需要注明注明构造文件的路径,只需要通过通过包名称导入,举个例子,在构造文件中定义的程序实例app,可以使用from sayhello import app

Flask在通过FLASK_APP环境变量定义的模块中寻找程序实例。所以在程序启动之前,我们需要给.flaskenv中的环境变量FLASK_APP重新赋值,这里只写出包名称即可FLASK_APP=sayhello

2、Web程序开发流程 遇事不决,先写注释,哈哈哈哈

  1. 分析需求,列出功能清单或写需求说明书
  2. 设计程序功能,写功能规格书和技术规格书 虽然不懂是啥
  3. 进入开发和测试迭代
  4. 调试和性能等专项测试
  5. 部署上线
  6. 运行维护与营销

写好功能规格书后,我们就可以开始实际的代码编写。具体分为前端页面(front end)和后端程序(back end)。

  1. 前端开发主要流程
  • 根据功能规格书画页面草图
  • 根据草图做交互式原型图
  • 根据原型图开发前端页面(HTML CSS Javascript)
  1. 后端开发主要流程
  • 数据库建模
  • 编写表单类
  • 编写视图函数和相关的处理函数
  • 在页面使用Jinja2替换虚拟数据

前期考虑和规划越周全,在实际开发时就可以越高效和省力

程序功能设计

因为本程序比较简单,所以写一个非常简短的功能规格书就行

  • 简书咋居中
  • 概述:用户输入问候信息和姓名,按下提交按钮,就可以将问候加入到页面的消息列表中。
  • 主页:主页是SayHello唯一的页面,页面中包含创建留言的表单和所有的问候消息。页面上方是程序的标题"SayHello",使用大字号和鲜艳的颜色,页面底部包含程序的版权标志、编写者、源码等相关信息
  • 问候表单:这个表单包含姓名和问候消息两个字段,其中姓名是普通的文本字段<input type="text">,消息字段是文本区域字段<textarea></textarea>。为了获得良好的样式效果,需要对这两个字段进行长度限制,姓名最长为20个字符,消息最长为200个字符。
  • 用户提交发布表单以后:
  1. 如果验证出错,错误消息以红色小字的形式显示在字段下面
  2. 如果通过验证,在程序标题下面显示一个提示消息,用户可以通过消息右侧的按钮关闭提示。
  • 问候消息列表:问候列表的上方显示所有消息的数量。每一条问候消息都要包含发布者姓名、消息正文、发布时间、消息编号。消息发布时间要显示相对时间,比如"3分钟前",当鼠标悬停在时间上时,弹出窗口显示具体的时间值。消息根据时间先后顺序,最新发布的排在上面。为了方便用户查询最早的消息,我们提供一个前往页面底部的按钮,同时提供一个回到页面顶部的按钮。
  • 错误页面:错误页面包括404和500,和主页包含相同的部分——程序标题。程序标题下显示错误信息以及一个返回主页的"Go Back"链接。

3、前端页面开发

首先我们使用纸币画草图,然后使用原型设计软件画出原型图,最后编写对应的HTML页面。

  • 主页原型图


    image.png

常用的原型设计工具有Axure RPMockplus

  • 错误原型图


    image.png

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

4、后端页面开发

  1. 数据库建模
    编写完功能规格书以后,我们也就确定了需要使用哪些表来存储数据,表中需要创建哪些字段,以及各表之间的关系。对于复杂的数据库结构,可以通过建模工具来辅助建立数据库关系。
class Message(db.Model):  # 定义一个Message类用来保存信息,因为使用的是SQLAIchemy,所以创建的数据库类继承自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)

使用index参数为timestamp建立索引,一般来说,取值较多的列(比如姓名)以及经常被用来作为排序参照的列(比如时间戳),适合建立索引。使用default参数设置默认值,传入函数名,在执行时自动调用datetime.now()

  1. 创建表单类
    问候表单由表单类HelloForm表示,表单中使用了文本区域字段TextAreaField表示HTML中的<textarea>标签
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, TextAreaField
from wtforms.validators import Length, DataRequired


class HelloForm(FlaskForm):
    name = StringField('Name', validators=[DataRequired(), Length(1, 20)])
    body = TextAreaField('Message', validators=[DataRequired(), Length(1, 200)])
    submit = SubmitField()

创建表单类继承自FlaskForm,从wtforms中导入所需字段类型,从wtforms.validators导入所需验证器

  1. 编写视图函数
    编写index函数,它可以处理GET请求和POST请求
  • GET请求,从数据库中查询所有消息记录,返回渲染后包含消息列表的主页模板index.html
  • POST请求,问候表单提交以后,验证表单数据,通过验证后将表单数据保存至数据库,使用flash函数显示一条提示,然后重定向到index视图,渲染页面。
from flask import flash, redirect, url_for, render_template
from sayhello import app, db
from sayhello.models import Message
from sayhello.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)  # 实例化模型类,创建记录
        db.session.add(message)  # 添加记录到数据库会话
        db.session.commit()  # 提交会话
        flash('信息提交成功')
        return redirect(url_for('index'))  # 重定向到index视图
    return render_template('index.html', form=form, messages=messages)
  • 在route装饰器中可以使用methods方法,表明所装饰的函数可以接受的请求方法,不传此参数默认使用GET方法。order_by()过滤器对数据库记录进行排序,参数是排序规则,这里根据Message模型的timestamp字段值排序,字段上附加排序方法desc()代表降序,asc()代表升序。
  • 不管是POST请求还是GET请求都需要获取通过数据库模型类Message到所有的消息记录,以及实例化HelloForm表单模型,生成页面表单,将数据传给index.html模板,生成完整的HTML页面,使用render_template渲染。
  • 这里通过form.validate_on_submit()方法来判断当前是否提交数据(发送POST请求),这里因为使用的是wtform,所以直接使用表单变量的data属性获取内容。
  1. 编写模板
    可以将index.html,404.html和500.html中共有的部分抽离合并为基模板base.html。基模板包含一个完整的HTMl结构,我们在其中创建几个块,title、content和footer

url_for('index'),url_for函数指向指定的字符串视图函数

  • 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 %}Say Hello!{% endblock %}</title>
    <link rel="icon" href="{{ url_for('static',filename='favicon.ico') }}">
    {# 此处使用url_for函数定位到端点,通过filename来指定具体的文件,rel="icon"表示网页左上角的网站图标 #}
{#    <link rel="stylesheet" href="{{ url_for('static',filename='css/bootstrap.min.css') }}" type="text/css">#}
    <link href="https://cdn.bootcss.com/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
    <link rel="stylesheet" href="{{ url_for('static',filename='css/style.css') }}" type="text/css">
    {# 导入bootstrap和自己写的style.css,设置界面样式 #}
</head>
<body>
<main class="container">
    <header>
        <h1 class="text-center display-4">
            <a href="{{ url_for('index') }}" class="text-success"><strong>Say Hello</strong></a>
            <small style="font-size: 24px" class="text-muted">to the world</small>
        </h1>
    </header>
    {#  使用get_flashed_messages函数获取到返回的flash消息  #}
    {% 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 %}
        ...
        <p><a href="#" id="bottom" 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 src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/popper.js/1.16.1/esm/popper.min.js"></script>
<script src="https://cdn.bootcss.com/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
<script type="text/javascript" src="{{ url_for('static',filename='js/script.js') }}"></script>
</body>
</html>

这里个人比较懒,就没有下载jquery和bootstrap的文件,直接使用cdn加载。
对基模板进行说明,首先在head标签中引入了css样式文件,通过meta设置页面属性,这里使用到main标签创建一个container容器,其中包含header、content和footer,定义了两个块分别用来修改content和footer,最后导入js文件。

  • index.html
{% extends 'base.html' %}
{% from 'macros.html' import form_field %}

{% block content %}
<div class="hello-form">
    <form action="{{ request.full_path }}" method="post">
        {{ 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-primary') }}
    </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 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>
                    {{ message.timestamp.strftime('%Y/%m/%d %H:%M') }}
                </small>
            </div>
            <p class="mb-1">{{ message.body }}</p>
        </a>
    {% endfor %}
</div>
{% endblock %}

使用form_field宏渲染表单,然后迭代传入messages变量,渲染消息列表

5、使用Bootstrap-Flask简化页面编写

扩展Bootstrap-Flask内置了快速渲染Bootstrap样式HTML组件的宏,并提供了内置的Bootstrap资源,方便快速开发,使用它可以简化在Web程序李使用Bootstrap的过程。
首先需要使用pip安装bootstrap-flask,使用方法同其他的flask插件

from flask import Flask
from flask_bootstrap import Bootstrap

app = Flask(__name__)
bootstrap = Bootstrap(app)
  • 加载资源文件
    Bootstrap-Flask在模板中提供了一个bootstrap对象,这个对象提供了两个方法可以用来生成资源引用代码:用来加载css文件的bootstrap.load_css(),用来加载js文件的bootstrap.load_js()方法。Bootstrap-Flask默认使用CDN来加载资源,同时也提供了内置的本地资源。如果你想使用Bootstrap-Flask提供的本地资源,可以将配置变量BOOTSTRAP_SERVE_LOCAL设为True。同时,当FLASK_ENV环境变量设为development时,会自动使用本地资源。
  • 快捷渲染表单
    Bootstrap-Flask内置了两个用于渲染WTForms表单类的宏,一个是与form_field类似的render_field()宏,另一个是用来快速渲染整个表单的render_form()宏。render_form()宏直接从bootstrap/form.html模板导入,这两个宏都会自动渲染错误的消息,渲染表单的验证状态样式。render_form()宏使用起来非常简单,使用一行代码就可以渲染整个表单,而且会自动帮我们渲染CSRF令牌字段form.csrf_token。下面就利用这个宏在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>
{% endblock %}

6、使用Flask-Moment本地化日期和时间

  1. 本地化之前的准备
    为了使世界各地的用户访问都能看到自己的本地时间,我们需要在服务器端提供纯正的,不包含时区信息的时间戳。datetime.utcnow()方法可以生成当前的UTC,我们将使用它来替代之前的datetime.now方法,作为时间戳timestamp字段的默认值。
  2. 使用Flask-Moment集成Moment.js
    Moment.js是一个用于处理时间和日期的开源JavaScript库,它可以对时间和日期进行各种方式的处理。它会根据用户电脑中的时区设置在客户端使用JavaScript来渲染时间和日期,另外它还提供了丰富的时间渲染格式支持。
    为了使用Moment.js,我们需要在基模板中加载Moment.js资源。Flask-Moment在模板中提供了moment对象,这个对象提供了两个方法来加载资源:moment.include_moment()方法用来加载Moment.js的JavaScript资源,moment.include_jquery()方法用来加载jQuery。这两个方法默认从CDN加载资源,传入local_js参数可以指定本地资源URL。

因为我们在使用Bootstrap时已经加载了jQuery,所以这里只需要加载Moment.js的JavaScript文件。

{{ moment.include_moment() }}

Flask-Moment默认以英文显示时间,我们可以通过locale()方法来设置显示的语言,将auto_detect参数设为True,会自动探测客户端语言设置并选择合适的区域设置。

  1. 渲染时间日期
    Moment.js提供了非常丰富、灵活的时间日期格式化方式。在模板中我们可以通过对moment类调用format()方法来格式化日期和时间,moment的构造方法接收使用utcnow()方法创建的datetime对象作为参数,也就是Message对象的timestamp属性。format()方法接收特定格式字符串来渲染时间格式。
    {{ moment(timestamp).format('格式字符串') }}
  • Moment.js提供的一些内置格式字符串


    Moment.js内置格式字符串
  • 除了输出普通的时间日期,Moment.js还支持输出相对时间,只需要通过fromNow()方法实现,设置refresh参数为True可以让时间戳在不重载页面的情况下,随着时间的变化自动刷新。
    { moment(message.timestamp).fromNow(refresh=True) }
    在显示相对时间的HTML元素中创建一个data-timestamp属性储存原始的时间戳,以便在JavaScript中获取。

7. 使用Faker生成虚拟数据

from faker import Faker

fake=Faker('zh_CN')  #传入locale参数用来设置生成数据的语言
@app.cli.command()
@click.option('--count', default=20, help="创建数据,默认为20条")
def forge(count):
    """生成虚假的消息"""
    from faker import Faker
    db.drop_all()
    db.create_all()
    fake = Faker('zh_CN')
    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(f'创建了{count}条虚假信息')

最后使用flask forge生成一些虚拟的数据,flask run运行就可以了。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 229,327评论 6 537
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 98,996评论 3 423
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 177,316评论 0 382
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,406评论 1 316
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,128评论 6 410
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 55,524评论 1 324
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 43,576评论 3 444
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 42,759评论 0 289
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,310评论 1 335
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,065评论 3 356
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,249评论 1 371
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 38,821评论 5 362
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,479评论 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 34,909评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,140评论 1 290
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 51,984评论 3 395
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,228评论 2 375

推荐阅读更多精彩内容