Python Web開(kāi)發(fā)之——構(gòu)建基于Flask框架的web后端項(xiàng)目

聲明:這篇文章主要面向python/Flask/web后端初級(jí)開(kāi)發(fā)者,文章主要講解了如何搭建一個(gè)基于Flask的純web后臺(tái)項(xiàng)目,以及相關(guān)的知識(shí)原理。不涉及部署相關(guān)操作。由于接觸web開(kāi)發(fā)不久,難免會(huì)有疏漏的地方,請(qǐng)讀者指正。

<br />

下面是文章中會(huì)涉及到的內(nèi)容:
  • HTTP請(qǐng)求發(fā)送到后端與響應(yīng)的過(guò)程
  • Why Flask & 概覽
  • Flask的常用工具和項(xiàng)目配置
  • pycharm
  • HTTP requester
  • redis
  • 從第一個(gè)路由注冊(cè)接口開(kāi)始
    • 使用裝飾器處理接口必填字段
    • Flask上下文獲取request參數(shù)
    • 錯(cuò)誤處理
    • 數(shù)據(jù)庫(kù)連接池
    • 循環(huán)引用的那些坑
    • 封裝加密方法
    • ORM與Model
    • celery多線程、異步任務(wù)
    • 設(shè)置response模板
    • 完整的注冊(cè)接口
    • 使用Blueprint進(jìn)行模塊劃分
  • 總結(jié)與展望

前言

前些日子轉(zhuǎn)了python后臺(tái)開(kāi)發(fā)=。= 說(shuō)實(shí)話現(xiàn)在對(duì)后端更有興趣。我很享受這種不管做什么都是在學(xué)習(xí)新的知識(shí)的感覺(jué),像新生兒一樣對(duì)這個(gè)世界充滿好奇。

這篇文章總結(jié)了我最近一段時(shí)間的學(xué)習(xí)成果:使用Flask框架搭建一個(gè)可擴(kuò)展的中小型web service,并在其中加上一些原理的闡述或者鏈接。在本文中以實(shí)際的用戶模塊為例。之所以寫(xiě)這篇文章是因?yàn)樽约涸谌腴T(mén)的時(shí)候遇到了很多坑,文檔或者個(gè)人博客并不能滿足我的需要,不是很基礎(chǔ)(毫無(wú)架構(gòu)可言,而且大多是不實(shí)用的博客項(xiàng)目)就是特別復(fù)雜。在此感謝我的同學(xué)/同事evolsnow,在開(kāi)發(fā)學(xué)習(xí)過(guò)程中給了我很大的幫助。也希望這篇文章能幫到想入門(mén)python/Flask的同學(xué)。

HTTP請(qǐng)求發(fā)送到后端與響應(yīng)過(guò)程

在進(jìn)行項(xiàng)目搭建之前,我們先大致回顧一下一個(gè)HTTP請(qǐng)求是如何發(fā)送至后端并且響應(yīng)的。

  1. 通訊雙方進(jìn)行連接
    首先通訊雙方遵從HTTP協(xié)議。當(dāng)我們輸入這樣一個(gè)請(qǐng)求:http://www.test.com/api/info ,首先請(qǐng)求端會(huì)進(jìn)行DNS解析,把http://www.test.com 變成一個(gè)ip地址,如果url里不包含端口號(hào),則會(huì)使用該協(xié)議的默認(rèn)端口號(hào)。通過(guò)ip地址和端口,三次握手建立一個(gè)tcp連接。
  2. 請(qǐng)求:連接成功建立后,開(kāi)始向web服務(wù)器發(fā)送HTTP請(qǐng)求。Flask通過(guò)wsgi協(xié)議傳遞請(qǐng)求。
  3. 響應(yīng):接到請(qǐng)求后,交給相應(yīng)路由處理,根據(jù)地址轉(zhuǎn)發(fā)給相應(yīng)的控制器(函數(shù))處理。后端大部分工作就是寫(xiě)這些處理過(guò)程。處理完成后將最終的response返回給用戶。這其中我們拿到的request與response都是由python的wsgi工具包werkzeug提供的。
  4. 關(guān)閉連接:通訊雙方均可關(guān)閉socket結(jié)束tcp/ip會(huì)話。

關(guān)于wsgi:wsgi協(xié)議將處理請(qǐng)求的組件按照功能及調(diào)用關(guān)系分成了三種:

  • server
  • middleware
  • application。

其中,server可以調(diào)用middleware和application,middleware可以調(diào)用application。
符合WSGI的框架對(duì)于一次HTTP請(qǐng)求的完整處理過(guò)程為:

  • server讀取解析請(qǐng)求,生成environ和start_response,然后調(diào)用middleware;
  • middleware完成自己的處理部分后,可以繼續(xù)調(diào)用下一個(gè)middleware或application,形成一個(gè)完整的請(qǐng)求鏈;
  • application位于請(qǐng)求鏈的最后一級(jí),其作用就是生成最終的響應(yīng)。
如需更深入了解該過(guò)程,可以查看WSGIWerkzeug

<br />

一、Why Flask & 概覽

我負(fù)責(zé)項(xiàng)目web后端的用戶模塊,對(duì)并發(fā)量要求不高。考慮到Flask小巧簡(jiǎn)單易上手,同時(shí)具有強(qiáng)大的擴(kuò)展能力,使其功能可以不弱于django、Tornado等框架,我最終選擇了Flask。下面是Flask最簡(jiǎn)單的一個(gè)示例,這篇文章要做的就是將其充實(shí)、擴(kuò)展、拆分,使代碼具有良好的可擴(kuò)展性和可讀性。

from flask import Flask
app = Flask(__name__)

@app.route('api/test')
def hello():
    return 'Hello World!'

if __name__ == '__main__':
app.run()

本文假設(shè)你已有基礎(chǔ)的python語(yǔ)法知識(shí)。裝飾器是一種代碼運(yùn)行期間動(dòng)態(tài)增加功能的方式,本質(zhì)上是一個(gè)返回函數(shù)的高階函數(shù),也可以簡(jiǎn)單的將其理解為一個(gè)函數(shù)的包裝函數(shù)。上述代碼中的route方法是一個(gè)裝飾器,這個(gè)裝飾器的作用就是將地址(api/test)與方法名hello聯(lián)系起來(lái),當(dāng)HTTP請(qǐng)求的url為(api/test)時(shí)候?qū)⒄{(diào)用hello方法進(jìn)行處理。也就是建立了url與處理函數(shù)的映射。深入了解可以查看這篇文章

看起來(lái)不復(fù)雜,那就讓我們繼續(xù)吧!先從環(huán)境工具配置開(kāi)始:

常用工具

1.IDE

jetbrains家的pycharm,自帶終端(虛擬環(huán)境、安裝插件、啟動(dòng)redis等操作)、Python Console運(yùn)行python、Version Control版本控制、Even Log打印。

pycharm.png

2.請(qǐng)求工具

火狐瀏覽器插件—HTTP requester。可以自定義請(qǐng)求方式 request methods、請(qǐng)求內(nèi)容request body、請(qǐng)求頭 request header等,當(dāng)你寫(xiě)好一個(gè)接口時(shí),可以非常方便得進(jìn)行測(cè)試。


httprequester.png

項(xiàng)目配置

1.虛擬環(huán)境

可以按官方文檔使用終端進(jìn)行配置,也可以在pycharm的偏好設(shè)置里進(jìn)行設(shè)置,這里我們使用python3.5的解釋器。安裝后執(zhí)行命令進(jìn)入虛擬環(huán)境,其中yourvenv為你指定創(chuàng)建的虛擬環(huán)境目錄

$ source yourvenv/bin/activate

2.使用pip進(jìn)行包管理

pip是python的包管理工具,如果你是通過(guò)homebrew安裝python則會(huì)自動(dòng)安裝pip。其他情況參考stackoverflow。安裝好pip之后,在虛擬環(huán)境中通過(guò)

$ pip install flask(庫(kù)名)

安裝Flask以及其他三方庫(kù)。

3.配置redis

Reids是現(xiàn)在最流行的的非關(guān)系型數(shù)據(jù)庫(kù)(key-value)。其數(shù)據(jù)緩存在內(nèi)存中,因此效率很高。多用于存儲(chǔ)臨時(shí)的、高度動(dòng)態(tài)的數(shù)據(jù)。在用戶模塊中我們將會(huì)對(duì)驗(yàn)證碼進(jìn)行redis存儲(chǔ)(短時(shí)間內(nèi)進(jìn)行寫(xiě)入讀取操作,并且在緩存一定時(shí)間后刪除)。本文中將會(huì)已用戶注冊(cè)時(shí)生成的邀請(qǐng)碼為例,進(jìn)行redis存取操作。
從Redis官網(wǎng)下載安裝包并按文檔安裝后,終端執(zhí)行

$ redis-server

啟動(dòng)redis,在項(xiàng)目中pip安裝即可調(diào)用其API。

4.項(xiàng)目相關(guān)約定

  • 項(xiàng)目采用前后端分離的方式,只進(jìn)行數(shù)據(jù)交互,不使用python的jinja2模板去渲染頁(yè)面給web前端
  • web前端、iOS、Android與后端數(shù)據(jù)交互的格式均為json
  • 前端的請(qǐng)求頭默認(rèn)帶terminal(前端類(lèi)型)、version(版本號(hào)),后端返回的數(shù)據(jù)中包含“code”、“msg”參數(shù),code=0表示請(qǐng)求處理成功,當(dāng)code!=0時(shí),msg為錯(cuò)誤信息
  • 本文中的后端開(kāi)發(fā)環(huán)境為macOS系統(tǒng),使用python3.5版本

二、從第一個(gè)路由注冊(cè)接口開(kāi)始

在web開(kāi)發(fā)中,路由(route)是指根據(jù)url分配到對(duì)應(yīng)的處理程序。
在用戶模塊中,注冊(cè)接口無(wú)疑是最基礎(chǔ)的。我們先來(lái)分析注冊(cè)過(guò)程:

  • 前端(包括網(wǎng)頁(yè)端、iOS、Android)傳過(guò)來(lái)的參數(shù)中必須包含用戶名與密碼,考慮到產(chǎn)品有邀請(qǐng)人機(jī)制,因此可能會(huì)傳邀請(qǐng)碼過(guò)來(lái)。因此,我們首先要做的是,判斷該接口是否傳了所必須的參數(shù)包括手機(jī)號(hào)和密碼,其次判斷邀請(qǐng)碼是否存在并做相應(yīng)處理
  • 判斷請(qǐng)求中是否有邀請(qǐng)碼這一參數(shù),若有則通過(guò)邀請(qǐng)碼從數(shù)據(jù)庫(kù)中查找邀請(qǐng)人。此處邀請(qǐng)碼及邀請(qǐng)人存在redis數(shù)據(jù)庫(kù)中(此處用redis只是教程需要,存入mysql即可)。若沒(méi)有,則向用戶返回錯(cuò)誤碼及信息
  • 將密碼加密,存入數(shù)據(jù)庫(kù)并返回一個(gè)用戶id(默認(rèn)自增,用來(lái)創(chuàng)建邀請(qǐng)碼),若失敗則返回相應(yīng)錯(cuò)誤
  • 若用戶在請(qǐng)求參數(shù)中包含“idfa”參數(shù),則更新用戶來(lái)源渠道,該數(shù)據(jù)用于推廣運(yùn)營(yíng)
  • 上述過(guò)程處理完畢后,根據(jù)用戶id生成token并返回給用戶
from flask import Flask
app = Flask(__name__)

@app.route('user/register methods=['POST']')
def user_register():
    #判斷是否含有phone、password參數(shù),若沒(méi)有則返回400錯(cuò)誤
    #判斷是否有invitationCode參數(shù),若有則從redis數(shù)據(jù)庫(kù)中獲取邀請(qǐng)人,若獲取不到則返回錯(cuò)誤,參考REST
    #獲取phone參數(shù),加密密碼并將用戶信息存入mysql數(shù)據(jù),若成功則返回用戶id,失敗返回錯(cuò)誤
    #根據(jù)用戶id生成邀請(qǐng)碼,并存入redis數(shù)據(jù)庫(kù)中
    #判斷是否有idfa參數(shù),若有則在后臺(tái)線程在mysql中修改用戶來(lái)源(非必須同步操作,放后臺(tái)即可)
    #生成token
    return 注冊(cè)接口返回token(包含默認(rèn)的code、msg)

if __name__ == '__main__':
app.run()

1. 使用裝飾器處理接口必填字段

考慮到post請(qǐng)求都有檢測(cè)必填參數(shù)并返回信息的需求,因此我們可以寫(xiě)一個(gè)裝飾器來(lái)處理,避免每個(gè)接口都寫(xiě)一大段相同的判斷代碼。裝飾器是一個(gè)返回函數(shù)的高階函數(shù),可以在函數(shù)執(zhí)行之前執(zhí)行其他操作。在這里我們可以寫(xiě)一個(gè)接受多個(gè)參數(shù)(即一個(gè)請(qǐng)求的所有必填參數(shù))的裝飾器,如下:

def require(*required_args):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            for arg in required_args:
                if arg not in request.json:
                    return flask.jsonify(code=400, msg='參數(shù)不正確') 
           return func(*args, **kw) 
       return wrapper
    return decorator

我們只需要在請(qǐng)求方法之前添加裝飾器即可:

@app.route('user/register', methods=['POST'])
@require('phone','password')
def customer_register():
    xxxx
    return xxx

當(dāng)請(qǐng)求中包含了phone和password參數(shù)時(shí),不進(jìn)行任何操作,返回原函數(shù)即user_register(),當(dāng)有一個(gè)參數(shù)缺失時(shí),直接返回錯(cuò)誤信息400。注意,此處的400并不是http請(qǐng)求的statusCode狀態(tài)碼,而是后端與前端的一種約定,只是返回?cái)?shù)據(jù)的其中一個(gè)參數(shù)。

其中flask.jsonfy()方法可以將key-value參數(shù)轉(zhuǎn)換為json格式。jsonify()與dump()的區(qū)別在此

為了讓項(xiàng)目清晰易懂,我們將此類(lèi)為請(qǐng)求處理方法準(zhǔn)備的裝飾器單獨(dú)放在一個(gè)文件中。在項(xiàng)目下創(chuàng)建一個(gè)python包文件夾(python package)命名為handler,創(chuàng)建hd_base.py文件,并將裝飾器函數(shù)寫(xiě)在該文件中。當(dāng)需要時(shí),只需要引入該方法即可:

from handler.hd_base import required

看到這段代碼,大家可能會(huì)好奇,我是怎么拿到請(qǐng)求中的參數(shù)呢? 程序怎么知道代碼中的request就是我們現(xiàn)在正請(qǐng)求的request呢,這就涉及到了Flask的上下文。

2.Flask上下文獲取request參數(shù)

一般來(lái)講,想要拿到一個(gè)請(qǐng)求的內(nèi)容,如header、body等,需要將這個(gè)請(qǐng)求當(dāng)做參數(shù)傳給我們定義的user_register()函數(shù)。但是考慮到我們可能會(huì)調(diào)各種各樣的東西,為了避免大量可有可無(wú)的參數(shù)把函數(shù)搞的一團(tuán)糟,F(xiàn)lask使用了上下文(context)臨時(shí)把某些對(duì)象變成了全局可訪問(wèn)。Flask上下文分為應(yīng)用上下文(AppContext)和請(qǐng)求上下文(RequestContext),可以簡(jiǎn)單地理解為一個(gè)應(yīng)用運(yùn)行過(guò)程中/一次請(qǐng)求中的所有數(shù)據(jù)。在上面的裝飾器代碼中,我們用到了request.json.get()來(lái)獲取請(qǐng)求內(nèi)容,其中request對(duì)象就是全局可訪問(wèn)的。你又有疑問(wèn),在多線程服務(wù)器中,多個(gè)線程同時(shí)處理不同客戶端發(fā)送的不同請(qǐng)求時(shí),每個(gè)線程拿到的request能一樣嗎?在Flask中,同一時(shí)刻一個(gè)線程只處理一個(gè)請(qǐng)求,使用上下文讓特定的變量在一個(gè)線程中全局可訪問(wèn),不會(huì)干擾其他線程。詳見(jiàn)這篇文章
要使用上下文中的request全局可訪問(wèn)對(duì)象,需要引入:

from flask import request

若參數(shù)以json形式傳輸,則可通過(guò)request.json.get('phone')獲取某參數(shù)key的值,請(qǐng)求頭可以通過(guò)request.json.get('version')獲取,表單則可通過(guò)rquest.form獲取。

3.錯(cuò)誤處理

上述代碼中,裝飾器對(duì)參數(shù)不滿足的情況返回了包含錯(cuò)誤信息的json數(shù)據(jù)。那么如何直接拋錯(cuò),返回statusCode狀態(tài)碼錯(cuò)誤呢,F(xiàn)lask為我們提供了abort方法與errohandler裝飾器。將上述裝飾器代碼稍作修改即可:

def require(*required_args):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            for arg in required_args:
                if arg not in request.json:
                    return flask.abort(400)
           return func(*args, **kw) 
       return wrapper
    return decorator

@app.errorhandler(400)
def not_found(error): 
     return make_response(flask.jsonify({'error': '參數(shù)不正確'}), 400)

當(dāng)請(qǐng)求不包含必填參數(shù)時(shí),請(qǐng)求的發(fā)送端就會(huì)收到請(qǐng)求失敗的信息,http狀態(tài)碼為400(請(qǐng)求成功的狀態(tài)碼為200,更多查看http狀態(tài)碼)。

4.數(shù)據(jù)庫(kù)連接池

項(xiàng)目使用了MySQL數(shù)據(jù)庫(kù)和Redis數(shù)據(jù)庫(kù)。根據(jù)不同情況選擇使用。

MySQL

pip安裝flask-sqlalchemy。sqlalchemy是實(shí)現(xiàn)ORM的庫(kù),將會(huì)在下文進(jìn)行介紹。按照官方文檔進(jìn)行數(shù)據(jù)庫(kù)連接(此處為遠(yuǎn)程數(shù)據(jù)庫(kù)連接):

from flask import Flask
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://dbname:MrVg+X1ZwS4RiCh9@120.25.102.84:3306/db1'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True

from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy(app)

當(dāng)要使用數(shù)據(jù)庫(kù)時(shí),調(diào)用db即可,下文將會(huì)結(jié)合ORM使用。

Redis

pip 安裝Redis,查閱文檔,進(jìn)行連接(此處連接到本地Redis數(shù)據(jù)庫(kù)):

import redis
_redis_cache = redis.Redis(connection_pool=redis.ConnectionPool(host='127.0.0.1', port=6379, db=1))
_redis_db = redis.StrictRedis(host='127.0.0.1', port=6379, db=2)

我在這里使用兩種方式連接了Redis數(shù)據(jù)庫(kù),其作用是相同的。其中_redis_cache用于緩存數(shù)據(jù)(如用戶驗(yàn)證碼),_redis_db用做數(shù)據(jù)庫(kù)(消耗內(nèi)存,但是速度快),按需使用。為了方便使用,我們將redis常用的存取方法進(jìn)行封裝:

class RedisDB:
    def __init__(self, conn=_redis_db):
        self.conn = conn
    def set(self, key, value, expire=None):
        self.conn.set(key, value, expire)
    def hget(self, name, key):
        ret = self.conn.hget(name, key) 
        if ret: 
           ret = ret.decode('utf-8')
        return ret
    def hset(self, name, key, value):
        self.conn.hset(name, key, value)

class RedisCache(RedisDB):
    def __init__(self):
        super().__init__(_redis_cache)

我們?cè)陧?xiàng)目中創(chuàng)建一個(gè)python pacage命名為common,創(chuàng)建文件redisdb.py,將封裝的redis相關(guān)類(lèi)與方法寫(xiě)在其中。
當(dāng)需要使用Redis時(shí),實(shí)例化后進(jìn)行操作:

rdb = RedisDB()
rdb.set('a',123)     #(key,value)
a = rdb.get('a')
print(a)             #輸出123

連接好數(shù)據(jù)庫(kù),我們已經(jīng)可以完成注冊(cè)接口的一部分代碼了:

from handler.hd_base import require
from common import redisdb
from flask import Flask
app = Flask(__name__)

rdb = redisdb.RedisDB()

@app.route('user/register', methods=['POST'])
@require('phone','password')
def customer_register():
    #邀請(qǐng)碼相關(guān)處理
    inviter = None
    if request.json.get('invitationCode'):
    inviter = rdb.hget('invitationCode',request.json['invitationCode'].upper())
    if not inviter:
        return flask.jsonify(error=400,msg='邀請(qǐng)碼無(wú)效')
    #其他處理

if __name__ == '__main__':
    app.run()

從項(xiàng)目文件目錄的角度來(lái)講,你可能發(fā)覺(jué),所有的請(qǐng)求都寫(xiě)在這個(gè)我們創(chuàng)建項(xiàng)目時(shí)的第一個(gè)文件中,有些不太合適。因此我們可以將這個(gè)文件拆分,將實(shí)例化應(yīng)用、接口方法、應(yīng)用啟動(dòng)三個(gè)部分分離。

app/__init__.py中:

from flask import Flask
app = Flask(__name__)

app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://dbname:MrVg+X1ZwS4RiCh9@120.25.102.84:3306/db1'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True

from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy(app)

app/run.py中:

from app import app
if __name__ == '__main__':
    app.run()

handler/hd_user.py中:

@app.route('user/register', methods=['POST'])
@require('phone','password')
def customer_register():
     xxxxxx
     return xxxxx

創(chuàng)建python包(python package)為app、handler,將實(shí)例化應(yīng)用代碼寫(xiě)入自動(dòng)生成的app包下的init.py中,當(dāng)需要用到app時(shí),引入即可。同理,在app包下創(chuàng)建run.py,將應(yīng)用啟動(dòng)代碼寫(xiě)入。在handler下創(chuàng)建hd_user.py,寫(xiě)入路由方法。

5.循環(huán)引用的那些坑

在你為了項(xiàng)目可讀性將代碼進(jìn)行分離時(shí),可能會(huì)遇到一個(gè)奇怪的錯(cuò)誤:cannot import xxxx,無(wú)法引入某個(gè)模塊、方法或變量。如果你確認(rèn)自己沒(méi)有犯一些低級(jí)錯(cuò)誤,那么可能就是產(chǎn)生了循環(huán)引用。這也是項(xiàng)目初期我遇到的浪費(fèi)時(shí)間最多的坑。

其實(shí)問(wèn)題出在模塊導(dǎo)入的順序上。 比如:在A文件頭執(zhí)行到語(yǔ)句 from B import XXX,程序馬上就會(huì)轉(zhuǎn)到B文件中去,從頭到尾順序?qū)ふ褺文件中的XXX函數(shù),而A文件就暫停執(zhí)行,直到把XXX函數(shù)復(fù)制到內(nèi)存中。但在這個(gè)過(guò)程中,如果B文件頭中又導(dǎo)入了A文件中的函數(shù),由于XXX函數(shù)還沒(méi)有被復(fù)制,A文件就會(huì)因?yàn)闀和?zhí)行而無(wú)法導(dǎo)入,就會(huì)出現(xiàn)上面的錯(cuò)誤。你可以選擇嘗試修改import順序消除循環(huán)引用,但解決這個(gè)問(wèn)題最好的方法就是重新梳理邏輯,將重復(fù)使用的東西單獨(dú)提取出來(lái)的。當(dāng)然,上面代碼中,將app單獨(dú)放在一個(gè)文件里定義,就是為了避免循環(huán)引用,每個(gè)文件需要app對(duì)象時(shí),單獨(dú)import它即可。

6.封裝加密方法

對(duì)密碼進(jìn)行md5加鹽加密是一種常見(jiàn)的安全手段。MD5加密算法是不可逆的,如果需要驗(yàn)證密碼是否正確,需要對(duì)待驗(yàn)證的密碼進(jìn)行同樣的MD5加密,然后和數(shù)據(jù)庫(kù)中存放的加密后的結(jié)果進(jìn)行對(duì)比。但是MD5加密依然不夠安全,因此我們?cè)诖嘶A(chǔ)上采用加鹽加密
在common包中新建security.py文件,封裝加密方法:

#md5加鹽加密
def _hashed_with_salt(info, salt):        
    m = hashlib.md5()
    m.update(info.encode('utf-8'))
    m.update(salt)
    return m.hexdigest()

#對(duì)登錄密碼進(jìn)行加密
def hashed_login_pwd(pwd):      
    return _hashed_with_salt(pwd, const.login_pwd_salt)

之所以將加鹽加密方法單獨(dú)拿出來(lái),是因?yàn)轫?xiàng)目后期可能會(huì)對(duì)其他信息進(jìn)行加密,比如交易密碼。到時(shí)復(fù)用加密方法,修改參數(shù)即可,可以保證代碼簡(jiǎn)潔。通過(guò)如下代碼即可對(duì)用戶登錄密碼進(jìn)行加密:

from common import security
password = security.hashed_login_pwd(request.json['password'])

7.ORM與Model

ORM全稱(chēng)Object Relation Mapping,即對(duì)象關(guān)系映射。用于實(shí)現(xiàn)面向?qū)ο缶幊陶Z(yǔ)言里不同類(lèi)型系統(tǒng)的數(shù)據(jù)之間的轉(zhuǎn)換。簡(jiǎn)單來(lái)講,就是你不再需要寫(xiě)sql語(yǔ)句對(duì)數(shù)據(jù)庫(kù)進(jìn)行CRUD操作,只需建立每張表對(duì)應(yīng)的模型Model類(lèi),調(diào)用相應(yīng)方法即可達(dá)到相同的效果。
以用戶表為例,我們新建Model包,創(chuàng)建user.py,添加user模型:

from app import db
from common import utils
from model import userconst

class User(db.Model):
    __tablename__ = 'user'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(80),unique=False)
    phone = db.Column(db.String(80), unique=True)
    password = db.Column(db.String(80),unique=False)
    source = db.Column(db.Integer,unique=False)
    terminal = db.Column(db.Integer,unique=False)
    invited_from = db.Column(db.String(80),unique=False)

    #重寫(xiě)該方法,方便輸出user信息
    def __repr__(self):
        user = ''
        user += 'name: %s\n' % (self.name)
        user += 'phone: %s\n' % (self.phone)
        user += 'password: %s\n' % (self.password)
        ········
        return user

def create_user(phone,password,invited_from,terminal):
    user = User(phone=phone,password=password,source=userconst.SOURCE_DEFAULT,
                  terminal=terminal,invited_from=invited_from)
    db.session.add(user)
    try:
        db.session.commit()
    except BaseException:
        return 0
    else:
        return user.id    

其中userconst.py 定義了一些user相關(guān)的常量。

我們定義了User類(lèi),并定義了一個(gè)創(chuàng)建用戶的方法。若數(shù)據(jù)庫(kù)添加用戶成功,則返回默認(rèn)自增id,若有任何異常錯(cuò)誤,返回0,代表創(chuàng)建用戶失敗,即注冊(cè)失敗。

本例我們使用ORM代替了sql語(yǔ)句"INSERT INTO user VALUES (phone, name,....)"。
現(xiàn)在,讓我們將注冊(cè)路由的方法更進(jìn)一步。在user_register方法中添加如下代碼:


·········
def customer_register():
    #邀請(qǐng)碼相關(guān)操作,已省略
    ········
    phone = request.json['phone']
    #對(duì)密碼進(jìn)行MD5加鹽加密
    password = security.hashed_login_pwd(request.json['password'])
    #寫(xiě)入數(shù)據(jù)庫(kù)并處理返回
    uid = create_user(phone,password,inviter,request.headers['terminal'])
    if uid == 0:
        return error_resp(500,'注冊(cè)失敗,請(qǐng)核對(duì)信息后重新輸入')
    #其他處理
········


了解了ORM后,我們可以試著從數(shù)據(jù)庫(kù)中獲取手機(jī)號(hào)為“18012341234”的用戶的姓名并修改:

user = db.session().query(User).filter_by(phone='18012341234').first()
print(user.name)     #輸出姓名
user.name = '張三'
db.session.add(user)
db.session.commit()    #提交修改

可以看到,簡(jiǎn)單、精確、易用是ORM的特點(diǎn),但是ORM映射會(huì)消耗內(nèi)存,當(dāng)數(shù)據(jù)變得復(fù)雜且龐大時(shí),使用ORM會(huì)帶來(lái)不小的性能損耗,我們要根據(jù)實(shí)際情況進(jìn)行選擇。

8.celery多線程、異步任務(wù)

Celery是一個(gè)異步任務(wù)隊(duì)列,一個(gè)基于分布式消息傳遞的作業(yè)隊(duì)列。它支持使用任務(wù)隊(duì)列的方式在分布的機(jī)器、進(jìn)程、線程上執(zhí)行任務(wù)調(diào)度。總的想法就是你的應(yīng)用程序可能需要執(zhí)行任何消耗資源的任務(wù)都可以交給任務(wù)隊(duì)列,讓你的應(yīng)用程序自由和快速地響應(yīng)客戶端請(qǐng)求。

在handler中新建tasks.py,配置celery,添加相關(guān)方法:

from celery import Celery

def make_celery(app):
    celery = Celery(app.import_name, broker=app.config['redis://localhost'])
    celery.conf.update(app.config)
    TaskBase = celery.Task
    class ContextTask(TaskBase):
        abstract = True
        def __call__(self, *args, **kwargs):
            with app.app_context():
                return TaskBase.__call__(self, *args, **kwargs)
    celery.Task = ContextTask
    return celery

celery = make_celery(app)

當(dāng)我們需要實(shí)現(xiàn)后臺(tái)任務(wù)時(shí),只需要在方法前添加celery的裝飾器。將下列代碼寫(xiě)入tasks.py中:

@celery.task
def update_user_source(ifa, phone):
    records = db.session().query(Advertisement).filter_by(ifa=ifa).all()
    if len(records) == 0:
        return
    selected = sorted(records, key=lambda x: x.create_time, reverse=True)[0]
    source = Advertisement.AD_SOURCE_MAP[selected.ad_service]
    user = db.session().query(Customer).filter_by(phone=phone).first()
    user.source = source
    db.session.add(user)
    db.session.commit()

在終端中啟動(dòng)celery worker

$ celery -A tasks worker

在注冊(cè)接口中,我們可以在用戶數(shù)據(jù)插入成功之后,后臺(tái)更改用戶的source來(lái)源:

def customer_register():
    ········
    idfa = request.headers.get('idfa')
    if idfa:
        update_user_source.delay(idfa,phone)
        rdb.hset(const.user_idfa,uid,idfa)
    ········

其中const.py在common包下,存放字符串常量。
delay() 方法是強(qiáng)大的 apply_async() 調(diào)用的快捷方式。這樣相當(dāng)于使用 apply_async():

update_user_source.apply_async(args=[idfa,phone])

當(dāng)使用 apply_async(),你可以給 Celery 后臺(tái)任務(wù)如何執(zhí)行的更詳細(xì)的說(shuō)明。一個(gè)有用的選項(xiàng)就是要求任務(wù)在未來(lái)的某一時(shí)刻執(zhí)行。例如,這個(gè)調(diào)用將安排任務(wù)運(yùn)行在大約一分鐘后:

update_user_source.apply_async(args=[idfa,phone], countdown=60)

9.設(shè)置response模板

按照我們本項(xiàng)目的約定,接口返回的數(shù)據(jù)中帶code和msg字段,當(dāng)code為0說(shuō)明請(qǐng)求成功,msg為空;當(dāng)code不為0時(shí),msg為錯(cuò)誤信息。在handler中創(chuàng)建template.py:

import flask

_base_dic = {
    'code':0,
}

def error_resp(code,msg):
    return flask.jsonify(error=code,msg=msg)

def register_teml(token):
    return dict({
        'token':token,
    },**_base_dic)

設(shè)置注冊(cè)接口的response:

def user_register()
    #所有處理
    ············
    return flask.jsonify(**template.register_teml(token=security.generate_token(uid)))

10.完整的注冊(cè)接口

至此,注冊(cè)接口我們就完成了。完整的路由方法如下:

@app.route('user/register', methods=['POST'])
@require('phone','password')
def customer_register():
    #邀請(qǐng)碼相關(guān)處理
    inviter = None
    if request.json.get('invitationCode'):
    inviter = rdb.hget('invitationCode',request.json['invitationCode'].upper())
    if not inviter:
        return flask.jsonify(error=400,msg='邀請(qǐng)碼無(wú)效')
    phone = request.json['phone']
    #對(duì)密碼進(jìn)行MD5加鹽加密
    password = security.hashed_login_pwd(request.json['password'])
    #寫(xiě)入數(shù)據(jù)庫(kù)并處理返回
    uid = create_user(phone,password,inviter,request.headers['terminal'])
    if uid == 0:
        return error_resp(500,'注冊(cè)失敗,請(qǐng)核對(duì)信息后重新輸入')
    #用戶來(lái)源處理
    idfa = request.headers.get('idfa')
    if idfa:
        update_user_source.delay(idfa,phone)
        rdb.hset(const.user_idfa,uid,idfa)
     return flask.jsonify(**template.register_teml(token=security.generate_token(uid)))

現(xiàn)在,一切看起來(lái)都很順利。但是當(dāng)用戶模塊的接口添加了登陸、驗(yàn)證碼相關(guān)、重置密碼、獲取用戶信息等接口后,hd_user會(huì)變得很大,這時(shí)再添加其他模塊的接口時(shí),文件就會(huì)變得難以維護(hù)。而且當(dāng)你嘗試著新建一個(gè)文件寫(xiě)其他模塊的接口時(shí),會(huì)產(chǎn)生循環(huán)引用、代碼重復(fù)、耦合性太強(qiáng)等等問(wèn)題。這還僅僅是純后端,不涉及用模板渲染html頁(yè)面。這時(shí)我們應(yīng)該就想到將項(xiàng)目模塊化,在Flask中Blueprint很好的幫我們解決了這個(gè)問(wèn)題。

10.使用Blueprint進(jìn)行模塊劃分

Blueprint通常譯作藍(lán)圖或藍(lán)本。藍(lán)本允許你將不同路由分開(kāi),提供一些規(guī)范標(biāo)準(zhǔn),并且附帶了很多好處:讓程序更加松耦合,更加靈活,增加復(fù)用性,提高查錯(cuò)效率,降低出錯(cuò)概率。如果你是純萌新,沒(méi)有大中型項(xiàng)目經(jīng)驗(yàn),對(duì)藍(lán)圖可能會(huì)理解困難。我在知乎找到一篇回答,從『小白』和『專(zhuān)業(yè)』兩個(gè)方面解釋了藍(lán)圖是什么,大家可以去看看:如何理解Flask中的藍(lán)本?

Blueprint使用也非常簡(jiǎn)單。從避免循環(huán)導(dǎo)入、減少代碼耦合的角度看,我們?cè)赼pp包下創(chuàng)建bpurls.py,注冊(cè)藍(lán)圖:

from flask import Blueprint

userBP = Blueprint('user', __name__,url_prefix='/user')
productBP = Blueprint('product', __name__,url_prefix='/product')

每個(gè)藍(lán)圖代表一個(gè)模塊,可以設(shè)置前綴。在hd_user中導(dǎo)入userBP,此時(shí)對(duì)方法進(jìn)行修改:

#@app.route('user/register', methods=['POST'])
@userBP.route('register', methods=['POST'])
def customer_register():
      ·······

同理,當(dāng)你寫(xiě)product模塊相關(guān)接口時(shí),創(chuàng)建hd_product.py,導(dǎo)入productBP即可。
藍(lán)本在使用模板的項(xiàng)目中用處更加突出,有興趣的同學(xué)可以查閱相關(guān)文檔

總結(jié)及展望

這篇文章我最近一段時(shí)間的項(xiàng)目實(shí)踐總結(jié)。雖然內(nèi)容很簡(jiǎn)單,但是由于自己知識(shí)漏洞較多,還是學(xué)到了不少新知識(shí)。在寫(xiě)文章的時(shí)候查閱了很多資料,眼界也更加開(kāi)闊。雖說(shuō)iOS還未精通,python web也才入門(mén),但是我對(duì)其他技術(shù)也充滿好奇。下一步可能會(huì)在下班空閑時(shí)間深入已有知識(shí)體系的同時(shí),學(xué)習(xí)下數(shù)據(jù)相關(guān)的技術(shù)。總之,stay hungry ,stay fool !

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,494評(píng)論 3 416
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,283評(píng)論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,953評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,714評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,186評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,410評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,940評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,776評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,976評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,210評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,642評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,878評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,654評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,958評(píng)論 2 373

推薦閱讀更多精彩內(nèi)容

  • 22年12月更新:個(gè)人網(wǎng)站關(guān)停,如果仍舊對(duì)舊教程有興趣參考 Github 的markdown內(nèi)容[https://...
    tangyefei閱讀 35,200評(píng)論 22 257
  • # Python 資源大全中文版 我想很多程序員應(yīng)該記得 GitHub 上有一個(gè) Awesome - XXX 系列...
    aimaile閱讀 26,526評(píng)論 6 427
  • GitHub 上有一個(gè) Awesome - XXX 系列的資源整理,資源非常豐富,涉及面非常廣。awesome-p...
    若與閱讀 18,691評(píng)論 4 418
  • 環(huán)境管理管理Python版本和環(huán)境的工具。p–非常簡(jiǎn)單的交互式python版本管理工具。pyenv–簡(jiǎn)單的Pyth...
    MrHamster閱讀 3,808評(píng)論 1 61
  • 對(duì)視的那一剎那 我讀懂了自己為愛(ài)的付出 卻讀不懂你 每一次輕聲的問(wèn)候 那掩飾的隨意中 都蘊(yùn)積了我千個(gè)日夜的乞盼 看...
    從心活過(guò)閱讀 329評(píng)論 3 9