- 后端
- Web框架
【免费下载链接】sanic
Accelerate your web app development | Build fast. Run fast.
导读
Blueprint(蓝图)是 Sanic 框架中用于子路由组织的核心对象,它把路由、中间件、异常处理器、静态文件与监听器按业务域拆分到独立模块,再以灵活、可插拔的方式注册到应用实例。本文以官方指南为主体,结合仓库源码(sanic/blueprints.py、sanic/app.py)与测试用例(tests/test_blueprints.py)逐层讲解:读完你将掌握 Blueprint 的创建与注册、复制、分组(Blueprint.group)、分组级中间件与版本化、copy()组合复用,以及基于url_for()的端点 URL 生成,能够把大型 Sanic 应用组织得清晰、可维护、可复用。
概述:什么是 Blueprint
Blueprint 是用于在应用内部做"子路由"的对象。与直接把路由添加到应用实例不同,Blueprint 提供了与Sanic()应用实例非常相似的 API(大量同名装饰器),定义好之后以灵活、可插拔的方式注册到应用中:
- 它允许开发者将路由、异常处理器、中间件及其他 Web 功能组织进独立、模块化的分组(见 Blueprint 类源码 的 docstring);
- 尤其适合大型应用:应用逻辑可以被拆分为若干个分组或职责区域,互不干扰、便于维护;
- 从源码结构看,
Blueprint继承自BaseSanic(sanic/blueprints.py),并通过lazy(...)机制复用了route、middleware、exception、listener、signal、static等装饰器(sanic/blueprints.py),这就是它与应用实例 API 高度一致的底层原因。
创建与注册
创建 Blueprint
首先创建 Blueprint 实例,它的 API 与Sanic()应用实例非常相似,很多装饰器可直接复用:
# ./my_blueprint.py from sanic.response import json from sanic import Blueprint bp = Blueprint("my_blueprint") @bp.route("/") async def bp_root(request): return json({"my": "blueprint"})Blueprint构造函数支持以下参数(见 sanic/blueprints.py):
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name | str | 必填 | Blueprint 的名称,注册后用于端点命名与去重 |
url_prefix | str \| None | None | 该 Blueprint 下所有路由的 URL 前缀(构造时若以/结尾会被自动去除末尾斜杠) |
host | list[str] \| str \| None | None | 该 Blueprint 响应请求的域名或域名列表 |
version | int \| str \| float \| None | None | 该 Blueprint 实现的 API 版本号 |
strict_slashes | bool \| None | None | 是否强制 URL 以斜杠结尾 |
version_prefix | str | "/v" | 版本号在 URL 中显示时的前缀 |
注册到应用
接着把 Blueprint 注册到应用实例上:
from sanic import Sanic from my_blueprint import bp app = Sanic(__name__) app.blueprint(bp)app.blueprint()的完整签名如下(sanic/app.py):
def blueprint( self, blueprint: Blueprint | Iterable[Blueprint] | BlueprintGroup, *, url_prefix: str | None = None, version: int | float | str | None = None, strict_slashes: bool | None = None, version_prefix: str | None = None, name_prefix: str | None = None, ) -> None: ...也就是说,除了 Blueprint 对象本身,还可以在注册时传入url_prefix、version、strict_slashes、version_prefix、name_prefix等关键字参数,对注册行为做一次性覆盖。注册期间,Blueprint.register()会读取这些 options 并应用到其内部的_future_routes、_future_statics、_future_middleware、_future_exceptions、_future_listeners、_future_signals上(见 sanic/blueprints.py)。
另外,Blueprint 同样提供websocket()装饰器和add_websocket_route方法来实现 WebSocket 路由。
v21.12 起:注册前后均可继续添加
从 v21.12 开始,Blueprint 可以先注册、后添加内容。此前只有注册时已附着在 Blueprint 上的对象才会被加载进应用实例;现在则可以这样写:
app.blueprint(bp) @bp.route("/") async def bp_root(request): ...其底层机制是:Blueprint.register()在注册后若发现自身已被注册(self.registered为真),会调用register_futures()将后续新增的 future(路由、中间件、异常、监听器、信号)同步注册到所有已关联的应用上(sanic/blueprints.py)。这一点在测试 tests/test_blueprints.py 中有基本验证。
提示:注册前访问
bp.apps会抛出SanicException(消息形如<Blueprint test> has not yet been registered to an app),可用bp.registered属性判断是否已注册(sanic/blueprints.py),对应测试见 tests/test_blueprints.py。
复制 Blueprint:copy()
Blueprint 连同附着在其上的所有内容(路由、WebSocket 路由、中间件、异常、监听器、静态文件)都可以通过copy()方法复制到新实例。唯一必填参数是新的name;同时你也可以用它覆盖旧 Blueprint 的任意属性值:
v1 = Blueprint("Version1", version=1) @v1.route("/something") def something(request): pass v2 = v1.copy("Version2", version=2) app.blueprint(v1) app.blueprint(v2)此时可用路由为:
/v1/something /v2/somethingcopy()的完整参数(见 sanic/blueprints.py):
| 参数 | 默认值 | 说明 |
|---|---|---|
name | 必填 | 新 Blueprint 的名称,原名称会记录到copied_from |
url_prefix | 沿用旧值 | 新实例的 URL 前缀 |
version | 沿用旧值 | 新实例的版本号 |
version_prefix | 沿用旧值 | 版本前缀 |
allow_route_overwrite | 沿用旧值 | 是否允许路由覆盖 |
strict_slashes | 沿用旧值 | 是否强制尾部斜杠 |
with_registration | True | 是否把新实例同时注册到旧实例已关联的 Sanic 应用 |
with_ctx | False | 是否复制旧实例的ctx(默认为否,新实例获得空ctx) |
从源码实现看,copy()会深拷贝旧实例(deepcopy)、写入新名称与copied_from,再按传入参数覆盖相应属性;若with_registration=True且旧实例已注册,还会自动对每个已关联应用调用app.blueprint(new_bp),让新 Blueprint 一并生效(sanic/blueprints.py)。复制时若旧实例已注册静态文件路由,会抛出SanicException提示无法重复注册。相关断言见测试 tests/test_blueprints.py。
该功能在 v21.9 中加入。
Blueprint 分组(Blueprint groups)
Blueprint 也可以作为列表或元组的一部分注册,注册器会递归遍历其中的任何子序列并逐个注册。官方提供了Blueprint.group()方法来简化这一过程,可以"模拟"一套与前端目录结构对应的后端目录结构。考虑下面这个示例:
api/ ├──content/ │ ├──authors.py │ ├──static.py │ └──__init__.py ├──info.py └──__init__.py app.py第一个 Blueprint
# api/content/authors.py from sanic import Blueprint authors = Blueprint("content_authors", url_prefix="/authors")第二个 Blueprint
# api/content/static.py from sanic import Blueprint static = Blueprint("content_static", url_prefix="/static")Blueprint 分组
# api/content/__init__.py from sanic import Blueprint from .static import static from .authors import authors content = Blueprint.group(static, authors, url_prefix="/content")第三个 Blueprint
# api/info.py from sanic import Blueprint info = Blueprint("info", url_prefix="/info")另一个 Blueprint 分组
# api/__init__.py from sanic import Blueprint from .content import content from .info import info api = Blueprint.group(content, info, url_prefix="/api")主服务器(注册所有 Blueprint)
# app.py from sanic import Sanic from .api import api app = Sanic(__name__) app.blueprint(api)最终挂载的 URL 前缀依次叠加为:/api/content/authors、/api/content/static、/api/info。
Blueprint.group()的完整签名(sanic/blueprints.py):
@staticmethod def group( *blueprints: Blueprint | BlueprintGroup, url_prefix: str | None = None, version: int | str | float | None = None, strict_slashes: bool | None = None, version_prefix: str = "/v", name_prefix: str | None = "", ) -> BlueprintGroup: ...从实现看,group()内部会chain递归展开嵌套的 list/tuple,最终构造一个BlueprintGroup对象(sanic/blueprints.py)。BlueprintGroup实现了MutableSequence[Blueprint](sanic/blueprints.py),因此支持索引、切片、append()、insert()、__len__等类似 list 的操作,同时暴露url_prefix、version、strict_slashes、version_prefix、name_prefix等属性(sanic/blueprints.py)。尽管可以直接实例化BlueprintGroup,官方推荐始终使用Blueprint.group()来创建。
分组前缀与可组合性(url_prefix与name_prefix)
如上所示,创建分组时传入url_prefix可以为组内所有 Blueprint 统一扩展 URL 前缀,非常适合搭建模拟目录结构的 API。
此外,group()还支持name_prefix参数,用于让 Blueprint 可复用、可组合。当同一个 Blueprint 被应用到多个分组时,name_prefix会让该 Blueprint 在每个分组中以唯一名称注册,从而允许同一 Blueprint 多次注册,且其每条路由都被赋予带唯一标识的名字。考虑这个例子,生成的路由名如下:
TestApp.group-a_bp1.route1TestApp.group-a_bp2.route2TestApp.group-b_bp1.route1TestApp.group-b_bp2.route2
bp1 = Blueprint("bp1", url_prefix="/bp1") bp2 = Blueprint("bp2", url_prefix="/bp2") bp1.add_route(lambda _: ..., "/", name="route1") bp2.add_route(lambda _: ..., "/", name="route2") group_a = Blueprint.group( bp1, bp2, url_prefix="/group-a", name_prefix="group-a" ) group_b = Blueprint.group( bp1, bp2, url_prefix="/group-b", name_prefix="group-b" ) app = Sanic("TestApp") app.blueprint(group_a) app.blueprint(group_b)从源码看,name_prefix在Blueprint.register()中会被拼接到路由名前面:name = f"{opt_name_prefix}_{future.name}",随后再交给app.generate_name()生成最终名称(sanic/blueprints.py)。同时app.blueprint()在递归注册分组时会合并各级url_prefix,并将组级version、strict_slashes等向下继承(sanic/app.py)。
name_prefix功能在 v23.6 中加入。
中间件
Blueprint 可以挂载仅作用于其自身端点的中间件:
@bp.middleware async def print_on_request(request): print("I am a spy") @bp.middleware("request") async def halt_request(request): return text("I halted the request") @bp.middleware("response") async def halt_response(request, response): return text("I halted the response")类似地,通过 Blueprint 分组,可以把中间件应用到整组嵌套 Blueprint:
bp1 = Blueprint("bp1", url_prefix="/bp1") bp2 = Blueprint("bp2", url_prefix="/bp2") @bp1.middleware("request") async def bp1_only_middleware(request): print("applied on Blueprint : bp1 Only") @bp1.route("/") async def bp1_route(request): return text("bp1") @bp2.route("/<param>") async def bp2_route(request, param): return text(param) group = Blueprint.group(bp1, bp2) @group.middleware("request") async def group_middleware(request): print("common middleware applied for both bp1 and bp2") # Register Blueprint group under the app app.blueprint(group)分组级中间件的实现方式是:BlueprintGroup.middleware()把装饰器递归应用到组内每个 Blueprint 上(嵌套分组同样会逐层展开),并额外提供on_request/on_response便捷方法(sanic/blueprints.py)。注册时,Blueprint.register()会把中间件与组内实际挂载的路由名关联起来,从而保证中间件只作用于本组端点(sanic/blueprints.py)。
异常处理
与常规异常处理一样,可以为 Blueprint 定义专属的异常处理器:
@bp.exception(NotFound) def ignore_404s(request, exception): return text("Yep, I totally found the page: {}".format(request.url))分组同样支持整组异常处理:BlueprintGroup.exception()会把异常处理器递归注册到组内每个 Blueprint(sanic/blueprints.py)。注册到应用时,异常处理器同样只与组内路由名关联(sanic/blueprints.py),因此不会影响其他 Blueprint 或应用级路由。
静态文件
Blueprint 也可以拥有自己的静态文件处理器:
bp = Blueprint("bp", url_prefix="/bp") bp.static("/web/path", "/folder/to/serve") bp.static("/web/path", "/folder/to/server", name="uploads")随后可以用url_for()获取对应 URL,路由相关细节可参考路由指南:
>>> print(app.url_for("static", name="bp.uploads", filename="file.txt")) '/bp/web/path/file.txt'注意这里的端点名遵循{blueprint_name}.{handler_name}规则:静态资源命名空间是static,配合 Blueprint 名称后即bp.uploads。测试中也有类似用法,例如app.url_for("static", name="static.testing")(tests/test_blueprints.py)。
监听器(Listeners)
Blueprint 同样可以实现监听器:
@bp.listener("before_server_start") async def before_server_start(app, loop): ... @bp.listener("after_server_stop") async def after_server_stop(app, loop): ...Blueprint.register()在注册阶段会把_future_listeners中每个监听器按事件名(如before_server_start、after_server_stop)应用到应用上(sanic/blueprints.py),因此 Blueprint 级监听器与应用级监听器在触发时机上保持一致。
版本化(Versioning)
正如版本化专题所讨论的,Blueprint 可用于实现 Web API 的多个版本。version会被作为/v1、/v2等前缀拼接到路由前:
auth1 = Blueprint("auth", url_prefix="/auth", version=1) auth2 = Blueprint("auth", url_prefix="/auth", version=2)注册后,/v1/auth与/v2/auth会分别指向两个 Blueprint,从而实现每个 API 版本的子站点:
from auth_blueprints import auth1, auth2 app = Sanic(__name__) app.blueprint(auth1) app.blueprint(auth2)也可以把多个 Blueprint 放入一个BlueprintGroup,一次性为它们统一设置版本:
auth = Blueprint("auth", url_prefix="/auth") metrics = Blueprint("metrics", url_prefix="/metrics") group = Blueprint.group(auth, metrics, version="v1") # 这将提供以下 URL 前缀的 API # /v1/auth/ 和 /v1/metrics关于版本化的补充要点(源自版本化专题):
version可以是int、float或str,如1、2.25、"v1.1";- 组级版本会被组内 Blueprint 继承,除非 Blueprint 或路由自身显式覆盖;优先级从高到低为:路由级 > Blueprint 级 > Blueprint Group 级;
version_prefix默认是/v,可在Blueprint构造、Blueprint.group()、app.blueprint()等多个位置定义,更具体的定义覆盖更宽泛的定义;- 一条路由的完整 URI 构成规则为:
version_prefix + version + url_prefix + URI 定义; - 与
url_prefix类似,version_prefix中也可以定义路径参数(如version_prefix="/<foo:str>/v")。
可组合性(Composable)
一个Blueprint可以注册到多个分组,而每个BlueprintGroup本身又可以被继续嵌套注册,这带来了近乎无限的 Blueprint 组合可能。看下面的例子:两个 handler 实际被挂载成了五(5)条不同的路由。
app = Sanic(__name__) blueprint_1 = Blueprint("blueprint_1", url_prefix="/bp1") blueprint_2 = Blueprint("blueprint_2", url_prefix="/bp2") group = Blueprint.group( blueprint_1, blueprint_2, version=1, version_prefix="/api/v", url_prefix="/grouped", strict_slashes=True, ) primary = Blueprint.group(group, url_prefix="/primary") @blueprint_1.route("/") def blueprint_1_default_route(request): return text("BP1_OK") @blueprint_2.route("/") def blueprint_2_default_route(request): return text("BP2_OK") app.blueprint(group) app.blueprint(primary) app.blueprint(blueprint_1) # 挂载的路径: # /api/v1/grouped/bp1/ # /api/v1/grouped/bp2/ # /api/v1/primary/grouped/bp1 # /api/v1/primary/grouped/bp2 # /bp1这个例子同时展示了多个能力的叠加:分组级version与version_prefix定制(/api/v1)、分组级url_prefix叠加(/grouped、/primary/grouped)、strict_slashes统一控制,以及同一 Blueprint(blueprint_1)既被包含在分组中又单独注册。测试文件 tests/test_blueprint_group.py 与 tests/test_blueprints.py 中的test_blueprint_group_versioning、test_blueprint_group_strict_slashes、test_blueprint_registered_multiple_apps等用例分别验证了分组版本化、严格斜杠与多应用注册行为(tests/test_blueprints.py、tests/test_blueprints.py、tests/test_blueprints.py)。
可组合性在 v21.6 中加入。
生成 URL(url_for)
使用url_for()生成 URL 时,端点名采用如下形式:
{blueprint_name}.{handler_name}即"Blueprint 名称 +.+ 处理器名称"。例如前面name_prefix示例中生成的路由名TestApp.group-a_bp1.route1就是"应用名 + Blueprint 名 + handler 名"的完整拼接;而静态资源的命名空间固定为static,因此bp.uploads表示名为uploads的静态路由属于名为bp的 Blueprint。
结合url_for()的通用能力(如_anchor、_external、_host、_server、_scheme等特殊关键字参数,见 sanic/app.py),你可以为任何带name的 Blueprint 路由生成稳定的内部或外部 URL,从而避免在代码中硬编码路径。
小结:何时使用 Blueprint
综合官方指南与源码设计,建议在以下场景使用 Blueprint:
- 应用按业务域拆分(如
users、orders、payments),每个域一个或多个 Blueprint; - 需要为同一组接口提供多版本(
version/version_prefix),尤其适合用Blueprint.group()统一管理; - 需要把同一套 Blueprint 注册到多个分组或应用,利用
name_prefix避免命名冲突; - 需要为某组端点单独附加中间件、异常处理器、监听器与静态文件,而不影响应用其他部分。
进阶阅读:完整的端点命名与 URL 生成规则见路由指南,版本化的更多细节见版本化专题,Blueprint 级异常处理与异常处理最佳实践一脉相承,监听器机制可参考监听器指南。
- 后端
- Web框架
【免费下载链接】sanic
Accelerate your web app development | Build fast. Run fast.
相关推荐
Nuxeo未来路线图:2024年最值得期待的5大功能升级
Nuxeo未来路线图:2024年最值得期待的5大功能升级 Nuxeo作为领先的内容管理平台,2024年将迎来一系列重大功能升级,为企业用户带来更强大的内容管理能
后端CMS企业应用告别窗口混乱:Windows 11文件资源管理器的标签页效率革命
告别窗口混乱:Windows 11文件资源管理器的标签页效率革命 你是否曾在处理多个项目文件时,被满屏的文件资源管理器窗口搞得眼花缭乱?每次打开新文件夹就多一个
开发工具Lint代码质量PInvoke项目架构解析:多目标框架支持与Windows Store兼容性
PInvoke项目架构解析:多目标框架支持与Windows Store兼容性 PInvoke是一个包含所有P/Invoke代码的类库,让开发者无需重复导入系统调
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考