逐步理解 Flask 的 Blueprint(蓝本)

Python 的 Flask 框架能让我们快速的建立一个轻量级的 Web 或 REST API。对于小应用由一个 @app 装饰一撸到底就行,当项目稍具规模或要更清晰就要考虑模块化,于是来到了我们今天的话题,首先是

为什么需要 Blueprint?

比如说我们一定超级简单的 Flask 应用 main.py 的代码如下:
1from flask import Flask
2
3app = Flask(__name__)
4
5
6@app.route('/', methods=['GET'])
7def hello():
8    return "hello world!"

现在需要加上一个后端管理 admin 模块,可以继续在 main.py 中加代码,并且依然用 @app 来装饰
 1from flask import Flask
 2
 3app = Flask(__name__)
 4
 5
 6@app.route('/admin/user', methods=['POST'])
 7def add_user():
 8    return 'add user'
 9
10
11@app.route('/', methods=['GET'])
12def hello():
13    return "hello world!"

这只是用 URL 前缀来区分了功能,分别是
  1. GET /
  2. POST /admin/users

再进一步, 我们可在同一个模块中使用 Blueprint,main.py 的代码变为
 1from flask import Flask, Blueprint
 2
 3app = Flask(__name__)
 4admin = Blueprint('admin_blueprint', __name__)
 5
 6
 7@admin.route('/user', methods=['POST'])
 8def add_user():
 9    return 'add user'
10
11
12@app.route('/', methods=['GET'])
13def hello():
14    return "hello world!"
15
16
17app.register_blueprint(admin, url_prefix='/admin')

实现的功能与前面的 main.py 是一样的。从这里高亮行可以看出实现一个 Blueprint 需要基本的三步
  1. 声明一个 Blueprint
  2. 用 Blueprint 实例去装饰函数
  3. 注册 Blueprint 到 Flask 应用实例上

由 Blueprint 装饰的函数,访问时必须加上它的 url_prefix 前缀。如
@admin.route('/user', methods=['POST'])
访问时要加上 /admin 前缀,就是
POST /admin/users
注:有两种方式指定 Blueprint 的 url_prefix
  1. 声明 Blueprint 时可以指定 url_prefix: 相当于是默认的 url_prefix
  2. 向 app 注册 Blueprint 时也可以指定 url_prefix:注册时不指定就用声明时的 url_prefix, 否则会覆盖声明时的 url_prefix

Blueprint 的 url_prefix 就像是 Java 的 Spring MVC 中 Controller 类的 @RequestMapping 定义一样,定义在类级别的 @RequestMapping path 会作为 Controller 方法的 URL 前缀。

使用 Blueprint 模块化应用

上一节中我们体验了在同一个 Python 使用 Blueprint, 虽然 Blueprint 是通了,但那样不能算作真正的模块化。要实现模块化至少得把功能分到不同的 Python 文件中去,最好是有各自的目录。也就产生了两种使用 Blueprint 模块化的方式,使用模块和包

Blueprint 定义在不同的模块中

首先进行文件分家,我们要把 /admin 下的实现挪到  admin.py  文件中去,于是 main.pyadmin.py 的内容就变成了

admin.py
1from flask import Blueprint
2
3admin = Blueprint('admin_blueprint', __name__)
4
5
6@admin.route('/users', methods=['POST'])
7def add_user():
8    return 'add user'

在 admin 中像使用 @app 一样用 @admin 装饰,并且不需要知道哪个 Flask 应用去注册它

main.py
 1from flask import Flask
 2from admin import admin
 3
 4app = Flask(__name__)
 5
 6
 7@app.route('/', methods=['GET'])
 8def hello():
 9    return "hello world!"
10
11
12app.register_blueprint(admin, url_prefix='/admin')

注册第二行的引用是从模块 admin 中引用同名的  admin 变量

Blueprint 定义在不同的包(目录) 中

对于更复杂的应用,把不同功能分布到不同的模块文件中还不够,用包(目录)来组织不同的功能就显得十分的必要了。

如果在 admin 中只有一个 user 模块使用了 Blueprint,我们可以用下面的目录结构
my-project
├── admin
│   └── user.py
└── main.py
在 user.py 中
1from flask import Blueprint
2
3admin = Blueprint('admin', __name__)
4
5
6@admin.route('/users', methods=['POST'])
7def add_user():
8    return 'add user'

在 main.py 中就是
 1from flask import Flask
 2from admin.user import admin
 3
 4app = Flask(__name__)
 5
 6
 7@app.route('/', methods=['GET'])
 8def hello():
 9    return "hello world!"
10
11
12app.register_blueprint(admin, url_prefix='/admin')

现在两个 API
  1. GET /
  2. POST /admin/users

再往 admin 包中添加一个  order 模块,情形就变得稍微复杂了,由 order 和  user 都必须使用同一个 Blueprint 实例,所以需要在 __init__.py 中初始化它。同时为了在 Flask 启动时加载 admin/user 和 admin/order 模块,也就要在 __init__.py 中导入它们(main 导入 admin 包时还要立即导入其下的  user 和  order 模块)。所以,重新组织后的目录结构如下
my-project
├── admin
│   ├── __init__.py
│   ├── order.py
│   └── user.py
└── main.py
每个文件的内容如下

admin/order.py
1from admin import admin
2
3
4@admin.route('/orders', methods=['GET'])
5def list_orders():
6    return 'all orders'

admin/user.py
1from admin import admin
2
3
4@admin.route('/users', methods=['POST'])
5def add_user():
6    return 'add user'

admin/__init__.py
1from flask import Blueprint
2
3admin = Blueprint('admin', __name__)
4
5from . import user
6from . import order

这个文件的内容必须在声明 Blueprint 后把使用了它的模块导入到命包空间来,否则不能注册那些 endpoints。其目的就是当我们在 main.py 中用
from admin import admin
导入 admin 包时能立即导入 admin 包中的 user 和 order 模块

如果不想在 admin/__init__.py 中逐个导入子模块,而需要把所有子模块全部自动引入的话,就在 __init__.py 中加入下方的代码
1import pkgutil
2for loader, module_name, is_pkg in pkgutil.walk_packages(__path__):
3    _module = loader.find_module(module_name).load_module(module_name)
4    globals()[module_name] = _module

或者
1import importlib
2import pkgutil
3
4for loader, module_name, is_pkg in pkgutil.walk_packages(__path__, __name__ + '.'):
5    importlib.import_module(module_name)

IDE 可能会提示后面的  import 语句要提到前面去,这时候不能听它的,否则出现错误
ImportError: cannot import name 'admin' from partially initialized module 'admin' (most likely due to a circular import)
main.py
 1from flask import Flask
 2from admin import admin
 3
 4app = Flask(__name__)
 5
 6
 7@app.route('/', methods=['GET'])
 8def hello():
 9    return "hello world!"
10
11
12app.register_blueprint(admin, url_prefix='/admin')

main.py 文件没什么变化,关键的部分应该是在 admin/__init__.py 中

现在运行  Flask,就有以下三个 API
  1. GET /
  2. GET /admin/orders
  3. POST /admin/users

关于  Flask Blueprint 的其他知识

更多的关于 Blueprint 的用法可参考源码中的 Bluepint 初始函数和 Flask.register_blueprint 注册函数。

由于 Blueprint 相当于一个 Flask 应用的子站点,所以它可以定义自己的静态文件映射,子域名。Blueprint 可以嵌套,也就是一个 Blueprint 还能注册另一个 Blueprint
1blueprint_aa = Blueprint("aa", __name__)
2blueprint_bb = Blueprint("aa", __name__)
3blueprint_aa.register_blueprint(bb, url_prefix="/bb")
4
5app.register_blueprint(blueprint_aa, url_prefix="/aa")

那么 blueprint_bb 中的 /user 的 API 完整路径就是
/aa/bb/user
我们或许还可以考虑把 Flask app 和 Blueprint 实例作为参数传递给别的模块使用。

链接:
  1. Modular Applications with Blueprints
  2. Use a Flask Blueprint to Architect Your Applications
永久链接 https://yanbin.blog/understand-flask-blueprint/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。