基於Flask實現的前後端分離API
- 專案放在github上,以下是
1.起步與紅圖
1.新建入口檔案
-
在
ginger
目錄下新建入口檔案ginger.py
-
例項化
Flask
物件:- 在
ginger
目錄下新建app
的包,再在其包下新建app.py
檔案- 和
Flask
物件相關的初始化或操作都放入app.py
檔案,使專案擁有良好拓展性 app=Flask(__name__)
- 和
- 在
-
匯入專案配置檔案到
Flask
物件:-
在
app
目錄下新建config
的包,再在其包下新建secury.py
(敏感配置)檔案setting.py
(通用配置) -
把配置檔案裝載到
app.py
中:app.config.from_object('app.config.setting') app.config.from_object('app.config.secure')
-
-
在入口檔案
ginger.py
中呼叫appapp=create_app()
-
判斷當前檔案是入口檔案,呼叫app的run方法啟動web伺服器
if __name__=='__main__': app.run(debug=True)
-
使用postman進行測試:
- 在GET中輸入localhost:5000按回車測試
- 因為還未編寫檢視函式,所以返回404
2.藍圖分離檢視函式的缺陷
-
在入口檔案
ginger.py
中新建檢視函式get_user
-
利用
app.route()
裝飾器中傳遞檢視函式get_user
的URL:@app.route('/v1/user/get') def get_user(): return 'i am kikyo'
-
使用postman進行測試:
- 在GET中輸入localhost:5000//v1/user/get按回車測試
- 返回i am kikyo
-
-
為什麼不在入口檔案
ginger.py
中新建檢視函式- 檢視函式較多,寫在一個檔案中不方便
- 不同的檢視函式有不同的作用物件,大型專案中同一個作用物件可能有很多檢視物件,應該分門別類放在不同檔案中
-
所有檢視函式都是對api的操作,在
app
的包下新建api
包,在api
包下再新建v1
包-
v1
包下新建user.py
和book.py
-
把檢視函式
get_user()
和get_book()
拆分到user.py
和book.py
中-
註冊檢視函式的路由時,需要Flask的核心物件app
-
把
ginger
檔案中的app
直接匯入,會導致迴圈匯入 -
使用藍圖blueprint註冊路由
- 例項化一個Blueprint(),第一個引數傳遞藍圖名稱,第二個引數指定位置資訊:
book=Blueprint('book',__name__)
- 使用藍圖下的route裝飾器註冊路由:
@book.route('/v1/book/get')
- 例項化一個Blueprint(),第一個引數傳遞藍圖名稱,第二個引數指定位置資訊:
-
使藍圖生效
-
把藍圖註冊到核心物件app上
-
在
app.py
檔案中定義register_blueprint( )
函式註冊def register_blueprint(app): from app.api.v1.user import user from app.api.v1.book import book app.register_blueprint(user) app.register_blueprint(book)
-
在
app=create_app()
中呼叫register_blueprint( )
函式呼叫藍圖
-
-
使用使用postman進行測試看藍圖是否能拆分:
- 在GET中輸入localhost:5000//v1/user/get按回車測試
- 返回i am kikyo
-
-
藍圖Blueprint不是用來拆分檢視函式,而是一種模組級別的拆分
-
-
-
3.建立自己的紅圖
-
在
app
下新建一個directory,命名為libs
,用於存放自己定義的模組-
在
libs
下新建redprint.py
,定義redprint類-
在
book.py
下例項化redprint
:api=Redprint('book')
-
建立
v1
藍圖被所有紅圖公用,在v1
的___init__.py
下定義藍圖:def create_blueprint(): bp_v1=Blueprint('v1',__name__) pass
-
-
將紅圖註冊到藍圖上:
- 在藍圖定義函式create_blueprint()下使用
user.api.register(bp_v1)
- 在藍圖定義函式create_blueprint()下使用
-
把藍圖註冊到核心物件app上
-
在
app.py
檔案中定義register_blueprint( )
函式註冊def register_blueprint(app): from app.api.v1 import create_blueprint_v1 app.register_blueprint(create_blueprint_v1())
-
-
-
把檢視函式中的
v1
掛載到藍圖上,把book
掛載到紅圖上 -
註冊藍圖時給其附加一個字首:
app.register_blueprint(create_blueprint_v1(),url_prefix='/v1')
-
把檢視函式中的
book
掛載到紅圖上- 註冊紅圖時給其附加一個字首:
user.api.register(bp_v1,url_prefix='/user')
- 註冊紅圖時給其附加一個字首:
4.實現Redprint
-
傳入紅圖的名字
-
定義建構函式
__init_
方法傳入名字def __init__(self,name): self.name=name
-
實現
api.route()
的裝飾器-
參考blueprint的route函式,藍圖把檢視函式註冊到藍圖上,自定義的紅圖Redprint裡也需要檢視函式註冊到藍圖上
-
原
blueprint.py
中的self是藍圖,但自定義的redprint.py
中的self是紅圖,route中拿不到藍圖,需要先把相關引數先儲存起來#rule註冊的URL,option其他可選選擇 def route(self,rule,**options): #f裝飾器作用的函式 def decorator(f): self.mound.append((f,rule,options)) return f return decorator
-
-
將紅圖註冊到藍圖時使用了register方法,所以還需要定義register方法
-
register方法中傳入了藍圖引數,在register方法中完成檢視函式向藍圖的註冊
def register(self,bp,url_prefix=None): for f,rule,options in self.mound: endpoint = options.pop("endpoint", f.__name__) #藍圖註冊到檢視函式上 bp.add_url_rule(rule,endpoint,f,**options)
-
-
5.優化Redprint
-
註冊紅圖時url_prefix與Redprint中傳入的名字一致,可以省去
user.api.register(bp_v1,url_prefix='/user')
中的url_prefix,在Redprint中定義好def register(self,bp,url_prefix=None): if url_prefix is None: url_prefix='/'+self.name
2.自定義異常物件
1.構建client驗證器
-
在
v1
包下新建client.py
構建客戶端路由create_client()
-
from app.libs.redprint import Redprint api=Redprint('client') @api.route('/register') def create_client(): #註冊 登陸 #引數 校驗 接受引數 #WTForms 校驗表單 pass
-
-
在
libs
包下新建enums.py
定義客戶端不同方式的各種列舉-
from enum import Enum #客戶端型別 class ClientTypeEnum(Enum): USER_EMAIL=100 USER_MOBLE=101 #微信小程式 USER_MINA=200 #微信公眾號 USER_WX=201 pass
-
-
在
app
包下新建validators
包進行客戶端引數校驗-
新建
forms.py
使用WTForms 校驗表單-
定義
ClientForm(Form)
類對客戶端表單驗證-
驗證時(
wtforms.validators
)賬號和登陸型別必須傳入(DataRequired
) -
客戶登陸型別,WTForms 表單驗證中沒有,需要自定義傳入的是列舉型別
enums.py
中的一種#自定義客戶端型別驗證 def validate_tppe(self,value): try: client=ClientTypeEnum(value.data) except ValueError as e: raise e pass
-
-
-
2.處理不同客戶端註冊的方案
-
在
client.py
的create_client()
檢視函式中使用client驗證器進行引數的校驗- 用json獲取提交物件
- 表單的提交物件用於網頁中,json物件用於移動端中
- 用例項化的表單驗證類
ClientForm(Form)
接收穫取到的json資料對錶單進行驗證 - 定義一個字典
promise
為不同的客戶端編寫不同的註冊程式碼- 鍵:登陸的列舉物件
ClientTypeEnum.USER_EMAIL
- 值:該登陸方式下使用者註冊的函式
- 鍵:登陸的列舉物件
#client.py from flask import request from app.libs.enums import ClientTypeEnum from app.libs.redprint import Redprint from app.validators.forms import ClientForm api=Redprint('client') @api.route('/register',methods=['POST']) def create_client(): data=request.json#獲得客戶端引數 form=ClientForm(data=data)#例項化validators的forms客戶端表單驗證類 if form.validate(): promise={ ClientTypeEnum.USER_EMAIL:__register_user_by_email, ClientTypeEnum.USER_WX: __register_user_by_wx } #switch不同的客戶端編寫不同的註冊程式碼 #request.args.to_dict() #表單 json #註冊 登陸 #引數 校驗 接受引數 #WTForms 校驗表單 pass #使用者用emil註冊的相關程式碼 def __register_user_by_email(): pass def __register_user_by_wx(): pass
- 用json獲取提交物件
3.建立User模型
-
在
app
下新建一個models
包用來存放所有模型檔案,新建使用者模型檔案user.py
-
使用SQLALchemy定義
User
模型類- 定義id emil,暱稱nickname,是否管理員auth,密碼_password
- 密碼_password屬性在
SQLALchemy
中沒有定義驗證的方法,需要自定義
- 密碼_password屬性在
- 添加註冊方法
register_by_email()
#models.user.py #User模型,繼承自定義的base.py中的Base類 class User(Base): id=Column(Integer,primary_key=True) emil=Column(String(24),unique=True,nullable=False) nickname=Column(String(24),unique=True) auth=Column(SmallInteger,default=1) _password=Column('password',String(100)) #把類中定義的例項方法變成類屬性 @property def password(self): return self._password #@property對於新式類來說定義的屬性是一個只讀屬性,如果需要可寫,則需要一個@屬性.setter裝飾器裝飾該函式 @password.setter def password(self): self._password=generate_password_hash(raw) #註冊方法 @staticmethod#在物件下面再建立物件本身不合理,要用靜態方法 def register_by_email(nickname,account,secret): #在資料庫中使用auto_commit()方法新增使用者 with db.auto_commit(): user=User() user.nickname=nickname user.emil=account user.password=secret db.session.add(user) pass
- 定義id emil,暱稱nickname,是否管理員auth,密碼_password
-
-
使
SQLALchemy
生效-
在
app.py
定義register_plugin()方法- 匯入
SQLALchemy
的例項化物件db
- 進行db註冊
- 建立所有資料庫的資料表
#使SQLALcjhemy生效 def register_plugin(app): from app.models.base import db#匯入db db.init_app(app)#db註冊 #create_all要在app的上下文環境中進行操作 with app.app_context(): db.create_all()#建立所有資料庫的資料表 pass
- 匯入
-
4.完成客戶端註冊
-
使用者註冊方法
__register_user_by_email()
-
呼叫
User.py
表單模型類中register_by_email()
方法完成註冊 -
通過傳入表單驗證的例項化物件
form=ClientForm(data=data)
,傳入註冊所需的account
和secret
-
nickname
無法直接從form
中獲取,json中有使用者提交的所有資料,有nickname
,但是拿到的資料沒有通過校驗,需要在form.py
中新建一個User
驗證的類#使用者註冊類驗證 class UserEmailForm(ClientForm): account = StringField(validators=[ Email(message='invalidate email') ]) secret = StringField(validators=[ DataRequired(), # password can only include letters , numbers and "_" Regexp(r'^[A-Za-z0-9_*&$#@]{6,22}$') ]) nickname = StringField(validators=[DataRequired(), length(min=2, max=22)]) #驗證賬號是否已經被註冊過 def validate_account(self, value): if User.query.filter_by(email=value.data).first(): raise ValidationError()
-
User
驗證的類UserEmailForm
接收使用者提交的資料request.json
進行驗證,傳入登錄檔單模型User.register_by_email
中#使用者用emil註冊的相關程式碼 def __register_user_by_email():#從form的驗證器中獲取註冊需要的引數 #request.json['nickname'] form=UserEmailForm(data=request.json) #驗證通過 if form.validate(): User.register_by_email(form.nickname.data,form.account.data,form.secret.data)
-
-
呼叫註冊方法
__register_user_by_email()
- 通過字典拿到
__register_user_by_email()
form.py
中將登陸型別的數字轉換成列舉物件,用form.type.data可獲取登陸型別的列舉物件,再通過字典[鍵]獲取鍵值promise[form.type.data]()
- 通過字典拿到
5.生成使用者資料
-
建立資料庫
ginger
:CREATE DATABASE ginger; -
在配置檔案
secure.py
中用SQLALCHEMY_DATABASE_URI
連線資料庫,並用SECRET_KEY
設定金鑰保證會話安全#連線資料庫 SQLALCHEMY_DATABASE_URI = 'mysql+cymysql://root:[email protected]:3306/ginger' # 設定金鑰,保證會話安全 SECRET_KEY = '\x8d\x7f\xaf\xc8"a\xa1]c\xba\xcb\x80x\xbc\x97s'
-
REST 細節特性:輸入輸出都要是
json
格式 -
使用postman進行測試:
- 在POST中輸入localhost:5000/v1/client/register
- 在Body中選擇raw,選擇格式為json
- 在下方輸入json格式的註冊資料
- 按send傳送資料,在最底下顯示success
-
檢視資料新增
- 選擇資料庫use ginger;
- 顯示所有表格show tables;
- 顯示錶格所有內容:select * from user;
- 資料未新增對時無法在資料庫中顯示
6.自定義異常物件
-
pycharm執行時顯示
Adress already in use
解決方法:- 用
sudo lsof -i:5000
檢視哪些埠被佔用 - 再使用
sudo kill (PID)
結束程序,釋放埠
- 用
-
使用postman進行測試
-
在下方輸入錯誤的json格式的註冊資料
{"account":"[email protected]","secret":"1234567","type":99,"nickname":"***"}
-
按send傳送資料,在最底下也能顯示success
-
-
在pycharm程式碼左側點選設定斷點進行debug
- 斷點1:
client.py
的form=ClientForm(data=data)
- 斷點2:
client.py
的return 'sucess'
- 斷點3:
forms.py
的客戶端型別驗證try
- 點選
ginger.py
的debug,點選run to cursor執行到下一個斷點,可以看到列舉型別是99,點選step into my code執行到raise e檢視異常顯示’99 is not a vaild ClientTypeEnum’,再點選run to cursor執行到下一個斷點可以直接執行到return ‘sucess’
而並不會在異常處中斷。 - form.validate()異常不會被wtform丟擲,只會把異常資訊記錄在form的error屬性中
- 斷點1:
-
校驗不通過時,手動丟擲異常
-
from werkzeug.exceptions import HTTPException
進入exceptions.py檢視werkzeug自帶的異常403 Not found -
繼承
HTTPException
自定義異常:在libs
下新建error_code.py
- 建立自定義的error類
ClientTypeError(HTTPException)
- 新增狀態碼和描述
#客戶端型別錯誤 class ClientTypeError(HTTPException): #401未授權 403禁止訪問 404沒有找到資源 #500伺服器產生一個未知的錯誤 #200查詢成功 201建立、更新成功 204刪除成功 #301 302重定向 code=400#請求引數錯誤 description = ( "client is invalid" )
- 建立自定義的error類
-
驗證form.validate()不通過時raise ClientTypeError()
-
-
使用postman進行測試:
- 在POST中輸入localhost:5000/v1/client/register
- 在Body中選擇raw,選擇格式為json
- 在下方輸入json格式的錯誤註冊資料
- 按send傳送資料,在最底下選擇preview顯示自定義的錯誤描述client is invalid,狀態碼為400
7.異常返回的標準性
- 返回資訊分類
- 業務資料資訊
- 操作成功提示資訊
- 錯誤異常資訊{‘msg’:xxx,’error_code’:xxx,’request’:url}
8.自定義json格式的APIException
-
API輸入輸出資料都必須是json格式,但繼承
HTTPException
自定義的錯誤只能輸出HTML
格式的錯誤資訊,需要重寫HTTPException
-
在
libs
下新建error.py
自定義APIException(HTTPException)
類繼承HTTPException,對其重寫APIException
要有些預設的msg(錯誤資訊),error_code(錯誤碼),code(錯誤狀態碼)- 有機制改變預設值
- 重寫建構函式,改變預設值
- 用if判斷是否傳了引數,用傳的引數替代預設引數
- 用super繼承父類
HTTPException
的構造方法
- 重新get_body函式,改變
HTML
內容為json格式- 字典儲存:錯誤資訊,錯誤碼,訪問哪個api介面產生的(請求的http動詞+’ ‘+當前請求的URL路徑(不包括主機名和埠號))
- 通過一個靜態方法(和類本身沒有互動)用
request.full_path
拿到完整路徑,再用split去除掉問號後的路徑
- 通過一個靜態方法(和類本身沒有互動)用
- 通過
json.dumps(body)
把字典格式改成文字資訊
- 字典儲存:錯誤資訊,錯誤碼,訪問哪個api介面產生的(請求的http動詞+’ ‘+當前請求的URL路徑(不包括主機名和埠號))
- 重寫get_headers函式,使輸出的http頭是json
- 把
text/html
改為application/json
- 把
- 重寫建構函式,改變預設值
from flask import request, json from werkzeug.exceptions import HTTPException class APIException(HTTPException): code=500#錯誤狀態碼500伺服器產生一個未知的錯誤 msg='sorry,we have a mistake