Flask最佳實踐
1. 怎麼用擴展
在Application Factories提到了如何用擴展,也就是在create_app內才用init_app初始化對應的擴展。但是我推薦如下方式來組織擴展。首先 創建一個ext.py(叫extensions.py或者其他我也不反對)文件,用來管理全部的擴展:
from flask_mongoengine import MongoEnginenfrom flask_mako import MakoTemplates, render_template # noqanndb = MongoEngine()nmako = MakoTemplates()n
同樣在create_app裡面初始化(https://github.com/dongweiming/commentbox/blob/master/app.py#L18):
from ext import db, makonnndef create_app():n app = Flask(__name__, template_folder=templates,n static_folder=static)n app.config.from_object(config)n mako.init_app(app)n db.init_app(app)nn ...nn return appn
舉個例子,注意其中的db,我沒有用 「from yourapplication.model import db」,而是使用了第三方的ext中import進來的。
而在model裡面怎麼用呢(commentbox/models.py at master · dongweiming/commentbox · GitHub):
from ext import dbnnnclass BaseModel(db.Document):n ...n
這樣就解耦了擴展的使用,也就是不會有相互依賴的問題了,這就是init_app存在的意義。
2. 自定義RESTAPI的處理
現存的框架比較知名的有django-rest-framework和flask-restapi,但是這些框架我都不太滿意,而對於我這個項目用它們還太重了。好吧,手動寫一個實現。首先是借用 DispatcherMiddleware 實現對/j這樣的路徑特殊處理(commentbox/app.py at master · dongweiming/commentbox · GitHub):
from werkzeug.wsgi import DispatcherMiddleware nnnapp.wsgi_app = DispatcherMiddleware(app.wsgi_app, OrderedDict(( n (/j, json_api), n))) n
我希望/j開頭的返回的響應都是json格式的內容:
from flask import Flasknnnclass ApiFlask(Flask): n def make_response(self, rv): n if isinstance(rv, dict): n if r not in rv: n rv[r] = 1 n rv = ApiResult(rv) n if isinstance(rv, ApiResult): n return rv.to_response() n return Flask.make_response(self, rv) n n njson_api = ApiFlask(__name__) n
其中返回了一個額外的欄位r, 如果是0表示響應的結果是正確的,為1表示響應的內容有問題。
接著我們自定義錯誤處理的方式,比如404返回這樣:
{n message: "Not Found"n}n
怎麼實現呢:
from flask import json nfrom werkzeug.wrappers import Response n n nclass ApiResult(object): n def __init__(self, value, status=200): n self.value = value n self.status = status n def to_response(self): n return Response(json.dumps(self.value), n status=self.status, n mimetype=application/json) n n nclass ApiException(Exception): n def __init__(self, message, status=400): n self.message = message n self.status = status n def to_result(self): n return ApiResult({message: self.message, r: 1}, n status=self.status)nnn@json_api.errorhandler(ApiException) ndef api_error_handler(error): n return error.to_result() n n n@json_api.errorhandler(403) n@json_api.errorhandler(404) n@json_api.errorhandler(500) ndef error_handler(error): n if hasattr(error, name): n msg = error.name n code = error.code n else: n msg = error.message n code = 500 n return ApiResult({message: msg}, status=code) n
而且響應也被封裝了:
def success(res=None, status_code=200): n res = res or {} n n dct = { n r: 1 n } n n if res and isinstance(res, dict): n dct.update(res) n n return ApiResult(dct, status_code) n n ndef failure(message, status_code): n dct = { n r: 0, n status: status_code, n message: message n } n return dct n n ndef updated(res=None): n return success(res=res, status_code=204) n n ndef bad_request(message, res=None): n return failure(message, 400)n
使用的時候可以讓返回的正確和錯誤結果的格式都保持統一。
3. Redis序列化
我使用了mongoengine處理model,但是為了給後端減少壓力,所以使用Redis緩存結果。大家知道Redis支持了很多數據結構,對我來說,其實是可以滿足的,但是為了演示如何存儲複雜對象,單個文檔對象緩存的是序列化之後的結果,也就是一個字元串(https://github.com/dongweiming/commentbox/blob/master/models.py#L42):
cache.set(key, rs.to_json())n
取的時候這樣(commentbox/models.py at master · dongweiming/commentbox · GitHub):
rs = cache.get(key)nif rs:n return cls.from_json(rs)n
其中from_json和to_json都是mongoengine自帶的,希望對大家在業務中的使用有啟發。
4. local_settings.py
local_settings.py在豆瓣被廣泛的使用,一般的Flask應用都會有一個config.py文件,包含一些配置,它會被放進版本庫。但是線上和測試環境其中的一些設置是不一樣的,比如DEBUG在線上一定是False,但是在測試環境就是True。 那麼可以在config.py這麼用(commentbox/config.py at master · dongweiming/commentbox · GitHub):
DEBUG = Falsenntry:n from local_settings import * # noqanexcept ImportError:n passn
假如存在local_settings.py,那麼配置就被會覆蓋了,而local_settings.py就是在特定環境下才存在的。
5. 使用Mako
豆瓣在我印象裡面好像都是沒人使用Jinja2的。Mako是另一個知名模板語言,它有如下優點:
1. 性能和Jinja2相近,這一點[Jinja2也承認](Frequently Asked Questions)。
2. 有大型網站在使用,有質量保證。Reddit在2011年的月PV就達到10億,豆瓣幾乎全部用戶產品都使用Mako模板,所以不需要擔心沒有大公司使用的案例。
3. 有知名Web框架支持。Pylons和Pyramid這兩個Web框架內置Mako,而且把它作為默認模板。
4. 支持在模板中寫幾乎原生的Python語法的代碼,對Python工程師友好,我已經見過多個人來了豆瓣愛上Mako而拋棄Jinja2的例子了。
5. 自帶完整的緩存系統。Mako提供非常好的擴展介面,很容易切換成其他的緩存系統。
Jinja2和Mako的設計哲學有一點不同:Jinja2認為應該儘可能把邏輯從模板中移除,界限清晰,不允許在模板內寫Python代碼,也不支持全部的Python內置函數(只提供了很有限、最常用的一部分);而Mako正好相反,它最後會編譯成Python代碼以達到性能最優,在模板裡面可以自由寫後端邏輯,不需要傳遞就可以使用Python自帶的數據結構和內置類。Jinja2帶來的好處是模板引擎易於維護,並且模板有更好的可讀性;而Mako是一個對Python工程師非常友好的語言,限制很少,完成模板開發工作時更有效率,整個項目的代碼可維護性更好。
6. 合理使用Flask提供的資源。
這並不是這個項目中用到的實踐。我在豆瓣東西的一個後台項目看到過這樣用藍圖:
import views.story.user_storynimport views.story.rec_poolnimport views.story.exportnimport views.story.similarn... 省略了剩下的幾十個importnnndef create_app():n ...n app.register_blueprint(n views.story.user_story.bp, url_prefix="/story/user_story")n app.register_blueprint(n views.story.rec_pool.bp, url_prefix="/story/rec_pool")n app.register_blueprint(n views.story.export.bp, url_prefix="/story/export")n app.register_blueprint(n views.story.similar.bp, url_prefix="/story/similar")n ... 繼續省略剩下的那幾十個register_blueprintn
其實好的做法是什麼呢:
from werkzeug.utils import find_modules, import_stringnnndef register_blueprints(root, app):n for name in find_modules(root, recursive=True):n mod = import_string(name)n if hasattr(mod, bp):n urls = name.split(.)n prefix = /{}/{}.format(urls[-2], urls[-1])n app.register_blueprint(n mod.bp, url_prefix=prefix)nnndef create_app():n ...n register_blueprints(views, app)n ...n
是否懂了呢?
無恥的廣告:《Python Web開發實戰》上市了!
歡迎關注本人的微信公眾號獲取更多Python相關的內容(也可以直接搜索「Python之美」):
推薦閱讀:
※flask 角色驗證中位操作求解?
※上雲連載5:使用 Nginx + uWSGI 部署 Flask 應用
※第一期 · 如何理解Flask中的藍圖?
※GitHub 上有什麼使用 Flask 建站的項目嗎?
※Flask文件上傳系列目錄索引