Sanic Blueprints 权威实战指南:模块化路由、Blueprint 分组、版本化与组合复用
2026/9/21 15:30:35 网站建设 项目流程
  • 后端
  • Web框架

【免费下载链接】sanic

Accelerate your web app development | Build fast. Run fast.

项目地址:https://gitcode.com/gh_mirrors/sa/sanic
点击查看免费下载

导读

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(...)机制复用了routemiddlewareexceptionlistenersignalstatic等装饰器(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):

参数类型默认值说明
namestr必填Blueprint 的名称,注册后用于端点命名与去重
url_prefixstr \| NoneNone该 Blueprint 下所有路由的 URL 前缀(构造时若以/结尾会被自动去除末尾斜杠)
hostlist[str] \| str \| NoneNone该 Blueprint 响应请求的域名或域名列表
versionint \| str \| float \| NoneNone该 Blueprint 实现的 API 版本号
strict_slashesbool \| NoneNone是否强制 URL 以斜杠结尾
version_prefixstr"/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_prefixversionstrict_slashesversion_prefixname_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/something

copy()的完整参数(见 sanic/blueprints.py):

参数默认值说明
name必填新 Blueprint 的名称,原名称会记录到copied_from
url_prefix沿用旧值新实例的 URL 前缀
version沿用旧值新实例的版本号
version_prefix沿用旧值版本前缀
allow_route_overwrite沿用旧值是否允许路由覆盖
strict_slashes沿用旧值是否强制尾部斜杠
with_registrationTrue是否把新实例同时注册到旧实例已关联的 Sanic 应用
with_ctxFalse是否复制旧实例的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_prefixversionstrict_slashesversion_prefixname_prefix等属性(sanic/blueprints.py)。尽管可以直接实例化BlueprintGroup,官方推荐始终使用Blueprint.group()来创建。

分组前缀与可组合性(url_prefixname_prefix

如上所示,创建分组时传入url_prefix可以为组内所有 Blueprint 统一扩展 URL 前缀,非常适合搭建模拟目录结构的 API。

此外,group()还支持name_prefix参数,用于让 Blueprint 可复用、可组合。当同一个 Blueprint 被应用到多个分组时,name_prefix会让该 Blueprint 在每个分组中以唯一名称注册,从而允许同一 Blueprint 多次注册,且其每条路由都被赋予带唯一标识的名字。考虑这个例子,生成的路由名如下:

  • TestApp.group-a_bp1.route1
  • TestApp.group-a_bp2.route2
  • TestApp.group-b_bp1.route1
  • TestApp.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_prefixBlueprint.register()中会被拼接到路由名前面:name = f"{opt_name_prefix}_{future.name}",随后再交给app.generate_name()生成最终名称(sanic/blueprints.py)。同时app.blueprint()在递归注册分组时会合并各级url_prefix,并将组级versionstrict_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_startafter_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可以是intfloatstr,如12.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

这个例子同时展示了多个能力的叠加:分组级versionversion_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_versioningtest_blueprint_group_strict_slashestest_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:

  • 应用按业务域拆分(如usersorderspayments),每个域一个或多个 Blueprint;
  • 需要为同一组接口提供多版本(version/version_prefix),尤其适合用Blueprint.group()统一管理;
  • 需要把同一套 Blueprint 注册到多个分组或应用,利用name_prefix避免命名冲突;
  • 需要为某组端点单独附加中间件、异常处理器、监听器与静态文件,而不影响应用其他部分。

进阶阅读:完整的端点命名与 URL 生成规则见路由指南,版本化的更多细节见版本化专题,Blueprint 级异常处理与异常处理最佳实践一脉相承,监听器机制可参考监听器指南。

  • 后端
  • Web框架

【免费下载链接】sanic

Accelerate your web app development | Build fast. Run fast.

项目地址:https://gitcode.com/gh_mirrors/sa/sanic
点击查看免费下载

相关推荐

上一篇:TiDB 非并行 HashAgg 内存溢写(Spill to Disk)设计与实现全解析
下一篇:Eel框架深度解析:如何用Python轻松构建桌面GUI应用

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询