1. 程式人生 > 實用技巧 >基於Flask實現的前後端分離API

基於Flask實現的前後端分離API

目錄


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中呼叫app

    app=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.pybook.py

    • 把檢視函式get_user()get_book()拆分到user.pybook.py

      • 註冊檢視函式的路由時,需要Flask的核心物件app

        • ginger檔案中的app直接匯入,會導致迴圈匯入

        • 使用藍圖blueprint註冊路由

          • 例項化一個Blueprint(),第一個引數傳遞藍圖名稱,第二個引數指定位置資訊:book=Blueprint('book',__name__)
          • 使用藍圖下的route裝飾器註冊路由:@book.route('/v1/book/get')
        • 使藍圖生效

          • 把藍圖註冊到核心物件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)
    • 把藍圖註冊到核心物件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.pycreate_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
    

3.建立User模型

  • app下新建一個models包用來存放所有模型檔案,新建使用者模型檔案user.py

    • 使用SQLALchemy定義User模型類

      • 定義id emil,暱稱nickname,是否管理員auth,密碼_password
        • 密碼_password屬性在SQLALchemy中沒有定義驗證的方法,需要自定義
      • 添加註冊方法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
      
  • 使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),傳入註冊所需的accountsecret

    • 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.pyform=ClientForm(data=data)
    • 斷點2:client.pyreturn '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屬性中
  • 校驗不通過時,手動丟擲異常

    • 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"
          )
      
    • 驗證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)把字典格式改成文字資訊
      • 重寫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