设计背景
Flask 由 Armin Ronacher 于 2010 年创建,最初是作为 Pocoo 项目的一部分(Pocoo 是一个由 Armin 领导的开源 Python 项目集合)。当时,Python 社区已有像 Django 这样功能全面但"大而全"的 Web 框架,但也存在对更轻量、更灵活工具的需求。
Armin Ronacher 受到 Ruby 的 Sinatra 框架启发(一个极简的 Web 框架),希望在 Python 中实现类似的理念:简单、可扩展、不强制开发者遵循特定结构。因此,Flask 应运而生,旨在提供一个"微框架"(microframework)。
项目定位与目标
解决什么问题?
- 过度工程化问题
像 Django 这样的全栈框架虽然功能强大,但对小型项目或原型开发来说显得过于复杂。Flask 提供最小可行的 Web 框架,避免"杀鸡用牛刀"。 - 缺乏灵活性: 一些框架强制使用特定的 ORM、模板引擎或项目结构。Flask 不做这些假设,允许开发者自由选择组件(如 SQLAlchemy、Jinja2、MongoDB 等)。
- 快速原型与教学需求: Flask 语法简洁、学习曲线平缓,非常适合快速构建原型、教学演示或小型 API 服务。
- 模块化与可扩展性: Flask 本身只提供核心功能(路由、请求/响应处理、模板渲染等),其他功能通过扩展(如 Flask-SQLAlchemy、Flask-Login)按需添加,避免"臃肿"。
目标用户是谁?
希望灵活控制结构的开发者、微服务场景、原型开发。
核心设计理念是什么?
Flask 的设计哲学深受 Python "显式优于隐式"和"简单胜于复杂"的影响,其核心理念包括:
- 微框架(Microframework): Flask 仅包含最基础的 Web 开发功能(基于 Werkzeug 处理 WSGI,Jinja2 作为默认模板引擎),其他一切由开发者决定。
- 约定少,自由多(Minimalism & Freedom): Flask 不强制项目结构、数据库选择或表单处理方式。开发者可以完全控制应用架构。
- 可扩展性(Extensibility): 通过官方和社区提供的大量扩展(Flask Extensions),可以轻松集成数据库、身份验证、REST API 等功能。
- 显式优于隐式(Explicit over Implicit): Flask 的行为清晰可见,没有"魔法"。例如,路由通过装饰器明确声明,配置通过字典或类显式设置。
整体架构概览
-
主要组件有哪些?它们如何协作?
- WSGI App:核心应用对象
- 路由系统:URL Rule → View Function 映射
- 请求/应用上下文:
request,g,session的生命周期管理 - Jinja2 模板引擎
- 扩展机制:通过
flask-sqlalchemy等插件集成功能
-
画一张简单的架构图(可用文字描述)
Flask Application 1. HTTP请求 GET/POST/PUT/DELETE 成功 失败 是 否 2. 返回 HTML/JSON/重定向 URL路由匹配? Flask App 视图函数 View Function 返回404 Not Found 创建请求上下文
request / session / g 访问 request: 表单/查询参数等 使用 session: 用户会话数据 使用 g: 临时全局变量 执行业务逻辑 需数据库操作? 数据库操作 SQLAlchemy等 构建响应内容 DB会话 查询/写入 生成HTTP响应 HTTP响应 销毁请求上下文
request / session / g 失效 客户端 Client 客户端 Client
关键设计与实现机制
选择 2--3 个最具代表性的设计点深入分析
🔹 设计点 1:请求上下文(Request Context)
- 问题背景:如何在多线程/协程环境下安全地访问当前请求对象?
- 解决方案 :使用
LocalStack实现线程隔离的上下文栈- request: 线程内请求对象
- g: 线程内的缓存对象, 比如权限缓存, 用户信息缓存, 数据库连接
- session: 线程内会话信息的缓存
- 关键技术 :
Werkzeug中的Local、LocalStack、_app_ctx_stack - 优点:无需显式传递 request,使用方便
- 代价:调试困难,隐式状态可能引发坑
AppContext.pop() 过程 RequestContext.push() 过程 无 有 是 否 _cv_app 重置 (若为最后一个) AppContext.pop() g 对象随 AppContext 销毁 当前是否有 AppContext? 调用 request_context.push() 新建 AppContext(app) 复用现有 AppContext AppContext.init: self.g = _AppCtxGlobals() 跳过 g 初始化 _cv_app.set(self) 创建 Request(environ) → self.request _cv_request.set(self) session = session_interface.open_session(app, request) 用户发起 HTTP 请求 Werkzeug WSGI Server 调用 app(environ, start_response) Flask.wsgi_app(environ, start_response) 创建 RequestContext(app, environ) 执行视图函数
可安全使用: request, session, g 视图返回 Response RequestContext.pop() session_interface.save_session(...) (若需) AppContext 是否由本次请求创建? 保留 AppContext WSGI 返回响应 请求结束
🔹 设计点 2:蓝图(Blueprint)
- 问题背景:如何实现模块化开发?
- 解决方案:将路由、静态文件、模板按功能分组注册
- 关键技术 :
Blueprint对象延迟注册到主应用 - 优点:项目结构清晰,适合大型应用拆分
蓝图对比传统的url拆分优势
- 代码组织和可维护性: 没有蓝图所有的路由都集中在一个文件中变得难以维护, 模板和静态文件都是独立的
- 命名空间和URL生成: 蓝图可以定义公共的url前缀
- 配置和中间件的模块化: 蓝图有自己的
before_request,after_request等 - 代码复用: 比如开发一个通用的管理后台组件
- 延迟注册和条件注册: 可以根据配置决定是否启用哪些功能
🔹 设计点 3:扩展机制(Extensions)
- 问题背景:如何支持第三方功能集成(如数据库、认证)?
- 解决方案 :约定式命名 + 工厂函数初始化(如
db.init_app(app)) - 优点:解耦核心与功能,生态繁荣
- 启示:我在项目中也可以设计类似的插件系统
python
def init_app(self, app: Flask) -> None:
"""初始化Flask应用,在这个扩展实例中使用。这必须在使用应用程序访问数据库引擎或会话之前调用。
设置默认配置值,然后在上配置扩展
应用程序并为每个绑定键创建引擎。因此,这必须是
在配置应用程序之后调用。更改应用程序配置
之后这个呼叫将不会被反映。
The following keys from ``app.config`` are used:
- :data:`.SQLALCHEMY_DATABASE_URI`
- :data:`.SQLALCHEMY_ENGINE_OPTIONS`
- :data:`.SQLALCHEMY_ECHO`
- :data:`.SQLALCHEMY_BINDS`
- :data:`.SQLALCHEMY_RECORD_QUERIES`
- :data:`.SQLALCHEMY_TRACK_MODIFICATIONS`
:param app: The Flask application to initialize.
"""
if "sqlalchemy" in app.extensions:
raise RuntimeError(
"A 'SQLAlchemy' instance has already been registered on this Flask app."
" Import and use that instance instead."
)
app.extensions["sqlalchemy"] = self
# 定义销毁回调, 会清理session
app.teardown_appcontext(self._teardown_session)
# 如果添加了shell, 就从shell解析
if self._add_models_to_shell:
from .cli import add_models_to_shell
app.shell_context_processor(add_models_to_shell)
basic_uri: str | sa.engine.URL | None = app.config.setdefault(
"SQLALCHEMY_DATABASE_URI", None
)
basic_engine_options = self._engine_options.copy()
basic_engine_options.update(
app.config.setdefault("SQLALCHEMY_ENGINE_OPTIONS", {})
)
echo: bool = app.config.setdefault("SQLALCHEMY_ECHO", False)
config_binds: dict[
str | None, str | sa.engine.URL | dict[str, t.Any]
] = app.config.setdefault("SQLALCHEMY_BINDS", {})
engine_options: dict[str | None, dict[str, t.Any]] = {}
# Build the engine config for each bind key.
for key, value in config_binds.items():
engine_options[key] = self._engine_options.copy()
if isinstance(value, (str, sa.engine.URL)):
engine_options[key]["url"] = value
else:
engine_options[key].update(value)
# Build the engine config for the default bind key.
if basic_uri is not None:
basic_engine_options["url"] = basic_uri
if "url" in basic_engine_options:
engine_options.setdefault(None, {}).update(basic_engine_options)
if not engine_options:
raise RuntimeError(
"Either 'SQLALCHEMY_DATABASE_URI' or 'SQLALCHEMY_BINDS' must be set."
)
engines = self._app_engines.setdefault(app, {})
# Dispose existing engines in case init_app is called again.
if engines:
for engine in engines.values():
engine.dispose()
engines.clear()
# Create the metadata and engine for each bind key.
for key, options in engine_options.items():
self._make_metadata(key)
options.setdefault("echo", echo)
options.setdefault("echo_pool", echo)
self._apply_driver_defaults(options, app)
engines[key] = self._make_engine(key, options, app)
if app.config.setdefault("SQLALCHEMY_RECORD_QUERIES", False):
from . import record_queries
for engine in engines.values():
record_queries._listen(engine)
if app.config.setdefault("SQLALCHEMY_TRACK_MODIFICATIONS", False):
from . import track_modifications
track_modifications._listen(self.session)
🔹 设计点 4:信号机制(Signals via Blinker)
- 问题背景:如果感知请求和响应的中间过程?
- 解决方案:引入信号机制, 在需要的地方埋点和信号注册和发送
- 优点:解耦核心与功能,生态繁荣
信号定义
python
from __future__ import annotations
from blinker import Namespace
# This namespace is only for signals provided by Flask itself.
_signals = Namespace()
template_rendered = _signals.signal("template-rendered")
before_render_template = _signals.signal("before-render-template")
request_started = _signals.signal("request-started")
request_finished = _signals.signal("request-finished")
request_tearing_down = _signals.signal("request-tearing-down")
got_request_exception = _signals.signal("got-request-exception")
appcontext_tearing_down = _signals.signal("appcontext-tearing-down")
appcontext_pushed = _signals.signal("appcontext-pushed")
appcontext_popped = _signals.signal("appcontext-popped")
message_flashed = _signals.signal("message-flashed")
信号发送
python
def full_dispatch_request(self, ctx: AppContext) -> Response:
"""Dispatches the request and on top of that performs request
pre and postprocessing as well as HTTP exception catching and
error handling.
.. versionadded:: 0.7
"""
self._got_first_request = True
try:
request_started.send(self, _async_wrapper=self.ensure_sync)
rv = self.preprocess_request(ctx)
if rv is None:
rv = self.dispatch_request(ctx)
except Exception as e:
rv = self.handle_user_exception(ctx, e)
return self.finalize_request(ctx, rv)
信号注册
python
app = flask.Flask(__name__)
def before_request_signal(sender):
print("before-signal")
flask.request_started.connect(before_request_signal, app)
4. 我的收获与启发
把"学到的东西"转化为"我能用的东西"
| 启发 | 我可以怎么应用到实际工作中? |
|---|---|
| 上下文管理机制 | 实现日志追踪时,可以用 contextvars 管理请求 ID |
| 蓝图模块化设计 | 将现有项目按业务域拆分为多个模块 |
| 插件式扩展 | 设计一个可插拔的通知系统(邮件/SMS/钉钉) |
5. 延伸思考(可选)
- 如果让你改进它,你会做什么? 例如:Flask 缺少内置 ORM,是否可以设计一个轻量级默认数据层?
- 它不适合什么场景? 例如:不适合高并发实时服务,缺少原生异步支持(虽然后来有 Flask-Async)
6. 参考资料
- 官方文档链接
- GitHub 仓库
- 推荐阅读文章或视频