聲明:這篇文章主要面向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)的。
- 通訊雙方進(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連接。 - 請(qǐng)求:連接成功建立后,開(kāi)始向web服務(wù)器發(fā)送HTTP請(qǐng)求。Flask通過(guò)wsgi協(xié)議傳遞請(qǐng)求。
- 響應(yīng):接到請(qǐng)求后,交給相應(yīng)路由處理,根據(jù)地址轉(zhuǎn)發(fā)給相應(yīng)的控制器(函數(shù))處理。后端大部分工作就是寫(xiě)這些處理過(guò)程。處理完成后將最終的response返回給用戶。這其中我們拿到的request與response都是由python的wsgi工具包werkzeug提供的。
- 關(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ò)程,可以查看WSGI、Werkzeug。
<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打印。
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è)試。
項(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 !