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消息添加分類,以便對不同類別的消息應用不同的樣式。
 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM