漳州做网站建设的公司,给公司做一个网站流程,记事本做网站的流程,广告游戏1. 从第一行代码到应用实例#xff1a;启动流程全景图 很多刚开始用FastAPI的朋友#xff0c;包括我自己刚上手那会儿#xff0c;都会有一个疑问#xff1a;我敲下 uvicorn main:app --reload 这行命令后#xff0c;我的代码到底是怎么一步步跑起来的#xff1f;是直接跳…1. 从第一行代码到应用实例启动流程全景图很多刚开始用FastAPI的朋友包括我自己刚上手那会儿都会有一个疑问我敲下uvicorn main:app --reload这行命令后我的代码到底是怎么一步步跑起来的是直接跳到main函数吗app.get那些路由是什么时候注册的我写在配置文件里的初始化代码又是在哪个环节被执行的呢这感觉就像买了一台精密的仪器你按下了启动按钮听到里面一阵响动最后灯亮了但中间那些齿轮是怎么咬合、电路是怎么接通的你并不完全清楚。今天我就想带你一起把这台叫“FastAPI应用”的仪器拆开从插上电源执行启动命令到进入待机状态服务监听端口完整地走一遍它的内部流程。理解了这套流程以后你优化启动速度、排查启动时的诡异Bug、或者设计一些高级的初始化逻辑时心里会特别有底。整个过程我们可以把它想象成一场精心编排的三幕剧。第一幕是“准备舞台”也就是Python解释器上场它不关心你是不是Web框架它只按自己的规矩来加载模块执行所有顶层的、不在函数里的代码。你的import语句、你的app FastAPI()都在这一刻发生。第二幕是“导演入场”如果你写了if __name__ __main__: main()那么main函数就像导演一样可以在这里安排一些自定义的“开机仪式”比如检查舞台灯光环境变量、预热道具预加载数据。但请注意如果用uvicorn main:app直接启动这位“导演”可能根本不会出场。第三幕是“演出开始”也就是lifespan生命周期事件被触发这是FastAPI框架正式接管在拉开大幕开始接收请求前和落下大幕关闭服务后给你留下的两个黄金钩子让你有机会连接数据库、初始化缓存或者在结束时优雅地告别。我见过不少项目把初始化数据库连接的代码随手写在main函数里结果换了一种部署方式服务就挂了也见过有人试图在模块最顶层用await去异步读取配置直接导致项目启动失败。这些问题根源都在于没搞清楚这场“三幕剧”的演员表和出场顺序。接下来我们就一幕一幕结合代码看个究竟。2. 第一步模块导入与顶层代码执行让我们从一个最简单的项目结构开始。假设你的项目目录长这样my_fastapi_app/ ├── config.py └── main.py你的config.py文件里可能放着一些配置# config.py print( 1. 开始加载 config.py 模块) # 假设这里有一个需要一点时间计算的静态配置 DATABASE_URL postgresql://user:passlocalhost/dbname # 或者是一个需要读取文件的操作 import json with open(settings.json, r) as f: APP_SETTINGS json.load(f) print( 2. 在config.py中同步读取了settings.json文件) print( 3. config.py 模块加载完毕)然后是你的main.py应用的入口# main.py print( 4. 开始执行 main.py 的第一行代码) # 导入语句这是所有事情的起点 import config from fastapi import FastAPI from some_middleware import SomeMiddleware print( 5. 导入语句执行完毕) # 初始化FastAPI应用实例这是核心对象的创建 app FastAPI(title我的API) print( 6. FastAPI app 实例创建完毕) # 添加一些全局的依赖或中间件这依然是顶层代码 app.add_middleware(SomeMiddleware) # 定义路由 app.get(/) async def root(): return {message: Hello World} print( 7. 路由装饰器执行完毕路由已注册到app实例)现在我们在终端执行uvicorn main:app。请你先在脑海里猜一下上面那些print语句的输出顺序会是什么实际输出会是 1. 开始加载 config.py 模块 2. 在config.py中同步读取了settings.json文件 3. config.py 模块加载完毕 4. 开始执行 main.py 的第一行代码 5. 导入语句执行完毕 6. FastAPI app 实例创建完毕 7. 路由装饰器执行完毕路由已注册到app实例这个顺序揭示了一个最关键、也最容易被忽略的事实在uvicorn甚至还没看到app对象之前Python解释器已经忙活半天了。当你执行uvicorn main:appuvicorn做的第一件事是定位到main.py这个模块然后交给Python解释器去加载它。Python加载模块的标准流程就是从上到下、逐行执行模块顶层的代码。所以import config这行代码实际上是一个“加载并执行”的命令。它会立即跳转到config.py把里面的代码从头到尾跑一遍。这就是为什么config.py里的打印和文件读取最先发生。之后main.py自己的顶层代码继续执行创建app对象执行app.get装饰器注意执行装饰器只是把函数注册到app.router中并不是运行路由处理函数本身。这里有个非常重要的实践建议把那些纯同步的、服务启动所必须的、且只需要执行一次的初始化工作放在配置文件或初始化模块的顶层代码中。比如读取环境变量、解析静态的YAML/JSON配置文件、初始化一些全局的单例对象。这样做的好处是时机最早所有后续模块一导入就能用而且逻辑清晰。但切记这里只能做同步操作不能await。2.1 理解“顶层代码”与“函数定义”的区别你可能会有疑问app.get不也是顶层代码吗它和app FastAPI()有什么不同是的它们都是顶层代码都会在模块加载时执行。但它们的“执行”产生的结果不同。app FastAPI()执行结果是创建了一个FastAPI类的实例这是一个对象赋值操作。app.get(/)这是一个装饰器语法糖。它的执行相当于root app.get(/)(root)。也就是说Python会立即调用app.get(/)这个方法这个方法返回一个装饰器函数这个装饰器函数再应用到下面的async def root(): ...函数上。这个调用过程就把路径/和函数root的映射关系注册到了app.router这个路由器中。所以路由的注册发生在模块导入阶段而不是服务启动之后。这就是为什么你的应用一启动所有路由表就已经是准备好的了。2.2 导入的陷阱与循环依赖由于导入阶段会执行代码一个常见的坑就是“循环导入”。比如在main.py里from routers import user而在routers/user.py里又from main import app以便使用app来添加中间件。这会导致Python解释器陷入死循环最终导入失败。解决循环依赖通常需要重新设计代码结构。FastAPI推荐的也是我个人实践下来最好的方式是使用APIRouter。在子模块中创建router APIRouter()然后在main.py的导入阶段之后、顶层代码中再用app.include_router(router)将其挂载到主应用上。这样就打破了直接的导入循环。3. 第二步main函数的角色与误区接下来我们聊聊main函数。在很多Python脚本中if __name__ __main__:是标准入口。但在FastAPI配合ASGI服务器如uvicorn、hypercorn的语境下它的角色变得有些微妙。首先我们明确一点uvicorn main:app这个命令并不会去调用你main.py里定义的main函数。它只是告诉uvicorn“去main.py这个模块里找一个叫app的变量把它作为ASGI应用跑起来。” 你的main函数如果只是静静地躺在那里是不会被执行的。那么main函数什么时候有用有两种情况。情况一你使用python main.py来启动服务。这时Python解释器会运行main.py脚本如果脚本末尾有def main(): # 一些自定义逻辑 uvicorn.run(main:app, host0.0.0.0, port8000, reloadTrue) if __name__ __main__: main()那么main()函数就会被调用。在这个函数里你可以进行一些启动前的自定义操作比如验证环境变量是否齐全。根据命令行参数动态修改配置。启动前检查数据库连通性。甚至启动多个worker进程虽然生产环境更推荐用外部进程管理器。情况二你的应用需要通过复杂的逻辑来构建app实例。比如你的应用配置需要从远程配置中心拉取或者需要根据运行环境动态组装中间件和路由。这时你可以把app的创建封装在一个函数里比如叫create_application()然后在main函数中调用它并启动服务器。# main.py from fastapi import FastAPI import uvicorn def create_application() - FastAPI: app FastAPI() # 动态配置过程... # if condition: # app.add_middleware(...) # app.include_router(...) return app def main(): app create_application() # 启动前的最后检查 print(应用配置检查完毕开始启动服务器...) uvicorn.run(app, host0.0.0.0, port8000) if __name__ __main__: main()这样当你运行python main.py时一切尽在掌控。但如果你用uvicorn main:app它导入的app变量是None因为app只在main()里创建就会报错。为了解决这个问题一个常见的模式是# main.py def create_application() - FastAPI: ... return app # 将app作为模块级变量导出供uvicorn直接使用 app create_application() def main(): uvicorn.run(app, ...) if __name__ __main__: main()这样无论是uvicorn main:app还是python main.py都能正常工作。最大的误区把应该在导入阶段或生命周期中执行的初始化逻辑错误地放在了main函数里并且只依赖python main.py启动。一旦团队其他人或部署脚本使用uvicorn main:app启动这些初始化逻辑就被跳过了导致运行时错误。所以请务必审视你的初始化代码问问自己“这段代码如果main函数不被执行我的应用还能正常工作吗” 如果不能那就应该把它移到更合适的位置。4. 第三步lifespan生命周期事件详解终于到了FastAPI启动流程中最具框架特色的一环——lifespan。这是FastAPI 2.0以及Starlette提供的异步上下文管理器用于管理应用级别的启动和关闭逻辑。它执行得比导入和顶层代码晚但比第一个用户请求早。为什么需要lifespan想象一下你的Web服务需要连接数据库。这个连接应该在什么时候建立如果在模块导入时建立比如在config.py里那么无论你的应用实例是否被真正用来服务请求连接都已经创建了这可能在运行脚本或测试时造成不必要的开销。如果放在每个请求处理函数里临时连接那性能开销又太大。lifespan的startup事件就是在服务“已经准备好接收请求但还没接收第一个请求”这个完美时机让你进行这类初始化工作。4.1 基本用法与执行时机lifespan的基本结构是一个异步上下文管理器from contextlib import asynccontextmanager from fastapi import FastAPI asynccontextmanager async def lifespan(app: FastAPI): # 启动逻辑在yield之前 print(Lifespan: 服务启动正在初始化资源...) app.state.db_connection await connect_to_database() app.state.cache {} yield # 这里服务正式运行接收请求 # 关闭逻辑在yield之后 print(Lifespan: 服务关闭正在清理资源...) await app.state.db_connection.close() del app.state.cache app FastAPI(lifespanlifespan)当你启动服务控制台会看到这样的顺序... (之前导入和app创建的打印信息) Lifespan: 服务启动正在初始化资源... INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRLC to quit)当你按下CtrlC停止服务时会看到INFO: Shutting down Lifespan: 服务关闭正在清理资源...这个顺序清晰地展示了lifespan的定位它夹在ASGI服务器启动完毕和开始监听请求之间以及服务器停止监听和完全退出之间。4.2 与全局变量及main函数的对比为了更直观我画了一个表格来对比三者的区别特性维度模块顶层代码 (全局变量)main函数lifespan事件执行时机Python模块导入时最早。仅当脚本作为主程序运行 (python main.py) 时。ASGI服务器启动后、接收请求前关闭前、完全退出前。执行次数一次模块单例。一次如果被调用。一次每个应用实例生命周期。异步支持不支持。只能同步操作。支持但需在异步环境中调用。原生支持异步操作。主要用途初始化静态配置、常量、轻量同步资源。提供自定义命令行启动逻辑、参数解析。管理应用级有状态资源DB连接池、Redis客户端、外部API会话。资源清理无。变量常驻内存直到进程结束。可在函数末尾同步清理但时机不一定与服务器关闭对齐。有。在yield后编写关闭逻辑由框架保证执行。依赖注入无法直接使用FastAPI的依赖注入系统。无法直接使用。可以通过app.state存储对象供依赖项和路由使用。从这个表格可以得出一些清晰的实践准则放配置文件、读环境变量、算静态常量- 用全局变量模块顶层代码。想用python main.py --port 9000这种自定义命令启动- 用main函数。要连数据库、开Redis连接池、初始化需要await的重资源- 用lifespan。4.3 实战在lifespan中管理数据库连接池让我们看一个更贴近实战的例子使用asyncpg创建PostgreSQL连接池并在整个应用生命周期中共享。# database.py import asyncpg from typing import Optional class Database: def __init__(self): self.pool: Optional[asyncpg.Pool] None async def connect(self, dsn: str): 创建连接池 self.pool await asyncpg.create_pool(dsn, min_size1, max_size10) print(数据库连接池已创建) async def disconnect(self): 关闭连接池 if self.pool: await self.pool.close() print(数据库连接池已关闭) # 创建全局的数据库管理器实例 db Database()# main.py from contextlib import asynccontextmanager from fastapi import FastAPI, Depends from database import db import os asynccontextmanager async def lifespan(app: FastAPI): # 启动时连接 DATABASE_URL os.getenv(DATABASE_URL) if not DATABASE_URL: raise RuntimeError(DATABASE_URL环境变量未设置) await db.connect(DATABASE_URL) # 将连接池挂载到app.state方便其他地方获取 app.state.db_pool db.pool yield # 关闭时断开连接 await db.disconnect() app FastAPI(lifespanlifespan) # 一个依赖项用于在路由中获取数据库连接 async def get_db_connection(): # 从app.state中获取连接池 async with app.state.db_pool.acquire() as connection: yield connection app.get(/items) async def read_items(conn Depends(get_db_connection)): # 使用连接执行查询 items await conn.fetch(SELECT * FROM items) return {items: items}在这个设计里database.py中的db实例在导入时被创建但连接池为空。真正的连接和销毁发生在lifespan的startup和shutdown事件中。这样我们既享受了模块级对象易于导入的便利又拥有了异步初始化和资源清理的能力。5. 完整流程串联与最佳实践现在让我们把所有的碎片拼起来还原一个从终端命令到服务就绪的完整时间线。假设我们有一个稍复杂的项目使用了配置文件、自定义main函数和lifespan。时间线推演开发者输入命令uvicorn main:app --host 0.0.0.0 --port 8000Uvicorn 解析命令找到main.py模块准备加载app对象。Python 解释器加载main.py执行import config- 跳转到config.py执行其全部顶层代码读取静态文件设置全局变量。执行from fastapi import FastAPI等其余导入。回到main.py继续执行顶层代码app FastAPI(...)创建应用实例执行所有app.get、app.post装饰器完成路由注册执行app.include_router。注意此时if __name__ __main__:块内的代码不会执行因为模块是被导入的不是作为主脚本运行。Uvicorn 获取app对象从main模块中成功拿到app实例。Uvicorn 启动 ASGI 服务器初始化事件循环、协议等底层组件。触发lifespanstartup 事件Uvicorn 调用app的lifespan上下文管理器的__aenter__部分执行所有startup逻辑连接数据库、初始化缓存等。yield语句暂停。Uvicorn 开始监听端口打印Uvicorn running on...日志。此时服务正式进入可接收请求的状态。处理用户请求HTTP请求进来匹配路由执行依赖注入运行路径操作函数返回响应。开发者按下 CtrlCUvicorn 开始关闭流程。触发lifespanshutdown 事件Uvicorn 恢复lifespan上下文管理器yield之后的代码执行所有shutdown逻辑关闭数据库连接、清理临时文件等。Uvicorn 关闭服务器释放端口结束进程。基于此流程的最佳实践清单职责分离严格按阶段分配代码。静态配置归config.py动态资源管理归lifespan可选的自定义启动命令行归main函数。异步优先凡是涉及 I/O 的操作网络、磁盘尤其是启动时必须的尽量设计成异步并放在lifespan中。这能避免阻塞事件循环提升服务启动速度和响应能力。状态存储需要在全局或至少应用范围内共享的有状态对象如数据库连接池、Redis客户端、配置客户端在lifespan中初始化后可以存储在app.state这个“百宝袋”里。这是一个简单的命名空间专门用于存放应用状态。错误处理lifespan中的初始化代码必须有健壮的错误处理。如果数据库连不上服务应该启动失败而不是带着一个None的连接池运行。可以在lifespan里用try...except捕获异常记录日志并直接raise让Uvicorn启动失败。测试考虑由于lifespan和导入阶段的代码在测试时也会执行要确保它们不会对测试环境造成副作用。例如可以通过环境变量区分生产环境和测试环境在测试时不真正连接外部数据库而是使用模拟对象。理解FastAPI的启动流程就像拿到了项目的“开机自检报告”。哪里慢了哪里可能出错你都能心中有数。下次当你看到服务启动日志时眼前浮现的将不再是冰冷的文字而是一幅清晰的代码执行地图。这份地图能让你在构建更健壮、更高效的应用时走得更加从容。