flask项目大名鼎鼎,不需要多做介绍。我把它称之为python服务开发的TOP2项目,另外一个就是django。这两个项目各有千秋,各自有不同的应用场景,都需要深入理解,熟练掌握。本次源码选择的版本是 1.1.2
,我会采用慢读法,尽自己最大努力把它讲透。本篇是第3篇-自助餐,主要包括:
- view 解析
- blueprint 解析
- 小结
view 解析
flask一个简单的监听函数如下:
@app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': return do_the_login() else: return show_the_login_form() 复制代码
如果URL多了,就需要实现多个监听函数,代码会比较乱。同时对于一个URL,在监听函数中区分http的method,进行不同的业务逻辑处理。一个函数中处理两种逻辑,也不太符合单一职责,会让代码难以维护。
这种情况下,就需要使用视图。下面是一个视图示例:
class CounterAPI(MethodView): def get(self): return session.get('counter', 0) def post(self): session['counter'] = session.get('counter', 0) + 1 return 'OK' app.add_url_rule('/counter', view_func=CounterAPI.as_view('counter')) 复制代码
CounterAPI可以把一个类的实例方法注册到一个URL上,自动将get和post方法区分开。我们一起看看View的实现,先是所有View的基类:
class View(object): @classmethod def as_view(cls, name, *class_args, **class_kwargs): def view(*args, **kwargs): self = view.view_class(*class_args, **class_kwargs) return self.dispatch_request(*args, **kwargs) ... view.view_class = cls view.__name__ = name ... return view 复制代码
as_view函数返回一个视图函数,在视图函数里可以派发和处理request。
MethodViewType是一个元类,定义了视图支持的所有HTTP方法的集合:
http_method_funcs = frozenset( ["get", "post", "head", "options", "delete", "put", "trace", "patch"] ) class MethodViewType(type): def __init__(cls, name, bases, d): super(MethodViewType, cls).__init__(name, bases, d) if "methods" not in d: methods = set() ... for key in http_method_funcs: if hasattr(cls, key): methods.add(key.upper()) if methods: cls.methods = methods 复制代码
MethodView是使用MethodViewType和View创建的新类:
class MethodView(with_metaclass(MethodViewType, View)): def dispatch_request(self, *args, **kwargs): meth = getattr(self, request.method.lower(), None) ... return meth(*args, **kwargs) 复制代码
with_metaclass 是为了兼容python2的语法,可以简单的理解为继承自MethodViewType和View
dispatch_request中根据请求的http-method找到对应的方法,进行执行。
view的处理函数还可以增加装饰器,示例如下:
# 使用示例 class SecretView(View): methods = ['GET'] decorators = [login_required] class View(object): @classmethod def as_view(cls, name, *class_args, **class_kwargs): ... if cls.decorators: view.__name__ = name view.__module__ = cls.__module__ # 包装上装饰器 for decorator in cls.decorators: view = decorator(view) ... return view # 装饰器 def login_required(view): @functools.wraps(view) def wrapped_view(**kwargs): if g.user is None: return redirect(url_for('auth.login')) return view(**kwargs) return wrapped_view 复制代码
blueprint 解析
View相对还是比较单薄,大型的项目都会分模块进行开发,所以flask还有blueprint的概念。下面是示例项目flaskr中的 auth.py
:
import functools from flask import ( Blueprint, flash, g, redirect, render_template, request, session, url_for ) ... bp = Blueprint('auth', __name__, url_prefix='/auth') @bp.route('/register', methods=('GET', 'POST')) def register(): ... @bp.route('/login', methods=('GET', 'POST')) def login(): ... @bp.route('/logout') def logout(): ... 复制代码
这里定义了一个名称叫做auth的蓝图,里面定义了3个方法: register , login 和 logout 。蓝图在app的__init__.py
中注册:
def create_app(): app = ... # existing code omitted from . import auth app.register_blueprint(auth.bp) return app 复制代码
在flask项目中还有名为blog的蓝图,提供博客文章的增删改查方法:
bp = Blueprint("blog", __name__) @bp.route("/") def index(): ... @bp.route("/create", methods=("GET", "POST")) @login_required def create(): ... 复制代码
采用这种方式,就可以很方便的分模块进行程序开发。
了解了bluerpint的使用方法后,我们再看看其实现原理。
class Blueprint(_PackageBoundObject): def __init__( self, name, import_name, static_folder=None, static_url_path=None, template_folder=None, url_prefix=None, subdomain=None, url_defaults=None, root_path=None, cli_group=_sentinel, ): _PackageBoundObject.__init__( self, import_name, template_folder, root_path=root_path ) self.name = name self.url_prefix = url_prefix self.subdomain = subdomain self.static_folder = static_folder self.static_url_path = static_url_path self.deferred_functions = [] if url_defaults is None: url_defaults = {} self.url_values_defaults = url_defaults self.cli_group = cli_group 复制代码
上面Blueprint的构造函数中显示:
- 继承自_PackageBoundObject。_PackageBoundObject上一篇介绍过,主要实现本地目录的动态加载,因为蓝图也有一些模版需求,所以继承了_PackageBoundObject。
- deferred_functions数组是蓝图的所有视图的集合
- url_prefix,subdomain, static_folder等是蓝图模块化的功能参数
蓝图的route装饰器:
def route(self, rule, **options): def decorator(f): endpoint = options.pop("endpoint", f.__name__) self.add_url_rule(rule, endpoint, f, **options) return f return decorator def add_url_rule(self, rule, endpoint=None, view_func=None, **options): ... self.record(lambda s: s.add_url_rule(rule, endpoint, view_func, **options)) def record(self, func): self.deferred_functions.append(func) 复制代码
这里主要的疑问在添加视图函数时候的lambda函数的参数 s
是什么?继续看看蓝图的注册:
# app的方法 def register_blueprint(self, blueprint, **options): self.blueprints[blueprint.name] = blueprint self._blueprint_order.append(blueprint) first_registration = True blueprint.register(self, options, first_registration) # blueprint的方法 def register(self, app, options, first_registration=False): self._got_registered_once = True state = self.make_setup_state(app, options, first_registration) for deferred in self.deferred_functions: deferred(state) ... 复制代码
make_setup_stat创建BlueprintSetupState对象, 然后执行蓝图route添加到deferred_functions的方法。这个方法就是前面的lambda函数,前面的 s
就是state对象.
class BlueprintSetupState(object): def __init__(self, blueprint, app, options, first_registration): #: a reference to the current application self.app = app self.blueprint = blueprint def add_url_rule(self, rule, endpoint=None, view_func=None, **options): """A helper method to register a rule (and optionally a view function) to the application. The endpoint is automatically prefixed with the blueprint's name. """ if self.url_prefix is not None: if rule: rule = "/".join((self.url_prefix.rstrip("/"), rule.lstrip("/"))) else: rule = self.url_prefix options.setdefault("subdomain", self.subdomain) if endpoint is None: endpoint = _endpoint_from_view_func(view_func) defaults = self.url_defaults if "defaults" in options: defaults = dict(defaults, **options.pop("defaults")) self.app.add_url_rule( rule, "%s.%s" % (self.blueprint.name, endpoint), view_func, defaults=defaults, **options ) 复制代码
BlueprintSetupState中建立了app和blueprint的关联,并且使用app的add_url_rule方法,把blueprint的视图函数注册进入app。
小结
flask是一个 micro
框架,但是也(至少)可以支持中型项目。我们可以利用Blueprint和View功能进行模块化: View可以很好的区分URL上的http-method;Blueprint可以很好的定义子域名和URL前缀等。