Flask前後端分離專案案例

JonPan發表於2020-07-24

簡介

學習慕課課程,Flask前後端分離API後臺介面的實現demo,前端可以接入小程式,暫時已經完成後臺API基礎架構,使用postman除錯.
專案地址git

重構部分:

  1. token校驗模組
  2. auths認證模組
  3. scope許可權模組,增加全域性掃描器(參考flask HTTPExceptions模組)

收穫

  1. 我們可以接受定義時的複雜,但不能接受呼叫時的複雜
  2. 如果你覺得寫程式碼厭倦,無聊,那你只是停留在功能的實現上,功能的實現很簡單,你要追求的是更好的寫法,抽象的藝術,不是機械的勞動而是要創造,要有自己的思考
  3. Sqlalchemy中對類的建立都是用元類的方式,所以呼叫的時候都不用例項化,當我們重寫__init__方法是需要呼叫orm.reconstrcut裝飾器,才會執行例項化物件的建構函式
  4. 許可權等級模組的設計(api訪問許可權),如超級管理員,管理員,普通使用者,訪客,這四者之間的關係,有包含的關係,所以可以考慮合併也可以考慮排除的方式來構建許可權控制模組. 參考本專案中的app.libs.scope
  5. 學的是解決問題的方法,首先要有深度,在去考慮廣度,還要懂得遷移應用,形成自己的思維模型。

推薦閱讀:
工作中如何做好技術積累
沒有技術深度的苦惱

知識點覆盤

初始化flask應用程式

app = Flask(__name__, static_folder='views/statics', static_url_path='/static', template_folder="templates")  

建立Flask應用程式例項物件, 如果模組存在,會根據模組所在的目錄去尋找靜態檔案和模組檔案, 如果模組不存在,會預設使用app物件所在的專案目錄

  • __name__ 表示以此模組所在的目錄作為工作目錄,就是靜態文等從這個目錄下去找
  • static_folder 指定靜態檔案存放相對路徑 flask預設會用/進行分割然後取最後一個作為訪問url 類似Django中的STATICFILES_DIRS
  • static_url_path 指定訪問靜態檔案的url地址字首, 類似Django 中的 STATIC_URL
  • template_folder 指定模板檔案的目錄
	@property
    def static_url_path(self):
        """The URL prefix that the static route will be accessible from.

        If it was not configured during init, it is derived from
        :attr:`static_folder`.
        """
        if self._static_url_path is not None:
            return self._static_url_path

        if self.static_folder is not None:
            basename = os.path.basename(self.static_folder)
            return ("/" + basename).rstrip("/")

    @static_url_path.setter
    def static_url_path(self, value):
        if value is not None:
            value = value.rstrip("/")

        self._static_url_path = value

Flask 中url相關底層類

  • BaseConverter子類:儲存提取url引數匹配規則
  • Rule類:記錄一個url和一個檢視函式的對應關係
  • Map類:記錄所有url地址和試圖函式對應的關係 Map(Rule, Rule, ....)
  • MapAdapter類:執行url匹配的過程,其中有一個match方法,Rule.match(path, method)

自定義路由管理器

from flask import Flask

app = Flask(__name__)

from werkzeug.routing import BaseConverter

class RegexUrl(BaseConverter):
    # 指定匹配引數時的正規表示式
    # 如: # regex = '\d{6}'
    def __init__(self, url_map, regex):
        """
        :param url_map: flask會自動傳遞該引數
        :param regex: 自定義的匹配規則
        """
        super(RegexUrl, self).__init__(url_map)
        self.regex = regex
    
    # 在對應的試圖函式之前呼叫
    # 從url中提取出引數之後,會先呼叫to_python
    # 會把提取出的值作為引數傳遞給to_pthon在返回給對應的試圖
    def to_python(self, value):
        """可以在這裡做一些引數的型別轉換"""
        return value
    
    # 呼叫url_for時會被呼叫, 用來處理url反向解析時url引數處理
	# 返回值用來拼接url
    def to_url(self, value):
        """對接收到引數做一些過濾等"""
        return value
        
# 將自定義路由轉換器類新增到轉換器字典中
app.url_map.converters['re'] = RegexUrl


# 案例
@app.route('/user/<re("[a-z]{3}"):id>')
def hello(id):
    return f'hello {id}'


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

全域性異常捕獲

AOP程式設計思想,面向切面程式設計,把事件統一在一個地方處理,在一個統一的出口做處理

errorhandler 在flask 1.0版本之前只支援填寫對應的錯誤碼,比如 @app.errorhandler(404)

在flask1.0版本之後就支援全域性的異常捕獲了@app.errorhandler(code_or_exception),有了這個之後,就可以在全域性做一個異常捕獲了,不用每個檢視函式都做異常捕獲。

@app.errorhandler(Exception)
def framework_error(e):
    if isinstance(e, APIException):
        return e
    elif isinstance(e, HTTPException):
        code = e.code
        msg = e.description
        error_code = 1007
        return APIException(msg, code, error_code)

    else:
        if not current_app.config['DEBUG']:
            return ServerError()
        else:
            raise e

異常型別

  • 可預知的異常(已知異常)
  • 完全沒有意識的異常(未知異常)

abort函式

  • abort(狀態碼) 是一個預設的丟擲異常的方法
  • 呼叫abort函式可以丟擲一個指定狀態碼對應的異常資訊
  • abort函式會立即終止當前檢視函式的執行**

模型物件的序列化

場景:我們有時候可能需要返回模型物件中的某些欄位,或者全部欄位,平時的做法就是將物件中的各個欄位轉為字典在返回jsonnify(data), 但是這樣的寫法可能在每個需要返回資料的試圖函式中都寫一個對應的字典。。物件轉字典在返回。json預設是不能序列化物件的,一般我們的做法是 json.dumps(obj, default=lambda o: o.__dict__)但是 __dict__中只儲存例項屬性,我們的模型類基本定義的類屬性。解決這個問題就要看jsonify中是如何做序列化的,然後怎麼重寫。

  1. 重寫JSONEncoder
from datetime import date
from flask import Flask as _Flask
from flask.json import JSONEncoder as _JSONEncoder

class JSONEncoder(_JSONEncoder):
    """
    重寫json序列化,使得模型類的可序列化
    """
    def default(self, o):
        if hasattr(o, 'keys') and hasattr(o, '__getitem__'):
            return dict(o)
        if isinstance(o, date):
            return o.strftime('%Y-%m-%d')
        
   		super(JSONEncoder, self).default(o)
        

# 需要將重寫的類繫結到應用程式中
class Flask(_Flask):
    json_encoder = JSONEncoder
  1. 模型類的定義
class User(Base):
    id = Column(Integer, primary_key=True)
    email = Column(String(24), unique=True, nullable=False)
    nickname = Column(String(24), unique=True)
    auth = Column(SmallInteger, default=1)
    _password = Column('password', String(100))
    
    def keys(self):
        return ['id', 'email', 'nickname', 'auth']
    
    def __getitem__(self, item):
        return getattr(self, item)

注意: 修改了json_encode方法後,只要呼叫到flask.json 模組的都會走這個方法

為什麼要寫keys__getitem__方法

當我們使用dict(object) 操作一個物件的時候,dict首先會到例項中找keys的方法,將其返回列表的值作為key, 然後會根據object[key] 獲取對應的值,所以例項要實現__getitem__方法才可以使用中括號的方式呼叫屬性

進階寫法 - 控制返回的欄位

場景:當我們有一個Book的模型類,我們的api介面可能需要返回book的詳情頁所以就要返回所有字典,但另外一個介面可能只需要返回某幾個欄位。

class Book(Base):
    id = Column(Integer, primary_key=True, autoincrement=True)
    title = Column(String(50), nullable=False)
    author = Column(String(30), default='未名')
    binding = Column(String(20))
    publisher = Column(String(50))
    price = Column(String(20))
    pages = Column(Integer)
    pubdate = Column(String(20))
    isbn = Column(String(15), nullable=False, unique=True)
    summary = Column(String(1000))
    image = Column(String(50))
	
    # orm例項化物件, 欄位需要寫在建構函式中,這樣每個例項物件都會有自己的一份,刪除增加都不會互相影響
    @orm.reconstructor
    def __init__(self):
        self.fields = ['id', 'title', 'author', 'binding',
                       'publisher', 'price', 'pages', 'pubdate',
                       'isbn', 'summary', 'image']
        
   	def keys(self):
        return self.fields if hasattr(self, 'fields') else []
    
    def hide(self, *keys):
        for key in keys:
            self.fields.remove(key)
        return self
    
    def append(self, *keys):
        for key in keys:
            self.fields.append(key)
        return self


@api.route('/search')
def search():
    books = Book.query.filter().all()  # 根據某些條件搜尋的
   	books = [book.hide('summary') for book in books]
    return jsonify(books)
    
    
@api,route('/<isbn>/detail')
def detail(isbn):
    book = Book.query.filter_by(isbn=isbn).first_or_404()
    return jsonify(book)

請求鉤子函式

  • before_first_request:在處理第一個請求前執行。
  • before_request:在每次請求前執行。
  • after_request:如果沒有未處理的異常丟擲,在每次請求後執行。
  • teardown_request:在每次請求後執行,即使有未處理的異常丟擲。

全域性掃描器

模仿flask exceptions 預載入各個異常類的方式,將使用者組自動載入進記憶體中,這樣獲取的話就更方便

str2obj = {}
level2str = {}


def iteritems(d, *args, **kwargs):
    return iter(d.items(*args, **kwargs))


def _find_scope_group():
    for _name, obj in iteritems(globals()):
        try:
            is_scope_obj = issubclass(obj, BaseScope)
        except TypeError:
            is_scope_obj = False
        if not is_scope_obj or obj.level < 1:
            continue

        old_obj = str2obj.get(_name, None)
        if old_obj is not None and issubclass(obj, old_obj):
            continue
        str2obj[_name] = obj
        level2str[obj.level] = _name


# 模仿flask exceptions 預載入各個異常類的方式,將使用者組自動載入進記憶體
_find_scope_group()
del _find_scope_group

常見bug

  1. form正則校驗注意事項
r'^[A-Za-z0-9_]{6, 25}$'

# 帶空格和不帶空格是兩碼事, 正則裡面{,} 連續不帶空格 

r'^[A-Za-z0-9_]{6,25}$'

參考

Python Flask高階程式設計之RESTFul API前後端分離精講
七月老師的課程挺好的,不是純寫程式碼,而是從問題入手,怎麼把複雜問題簡單化,從0到1。

相關文章