FastAPI & HTML

\(FastAPI\)

仅用于记录学习进度……

\(HelloWorld\)

创建\(Helloworld.py\)
输入uvicorn HelloWorld:app --reload 启动应用
访问 http://127.0.0.1:8000 (由默认配置和网络基础规则共同决定)
可以看到 {"Hello": "World"} 的输出

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"Hello": "World"}

FastAPI():创建 FastAPI 应用实例。
@app.get("/"):使用 @app.get 装饰器创建一个处理根路径的路由。
def read_root():路由处理函数,返回一个包含 {"Hello": "World"} 的字典。

from typing import Union

from fastapi import FastAPI

# 创建 FastAPI,用于定义和管理应用的各个组件,包括路由。
app = FastAPI()


# 定义根路径 / 的路由操作,通过 HTTP GET 请求访问根路径时,将执行 read_root 函数

@app.get("/")
def read_root():
    return {"Hello": "World"}


# 定义带路径参数和查询参数的路由操作,使用了 @app.get("/items/{item_id}") 装饰器,表示当用户通过 HTTP GET 请求访问 /items/{item_id} 路径时,将执行 read_item 函数

@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
    return {"item_id": item_id, "q": q}

@app.get("/items/{item_id}"):定义了一个路由路径,其中 {item_id} 是路径参数,对应于函数参数 item_id
def read_item(item_id: int, q: str = None):路由处理函数接受一个整数类型的路径参数 item_id 和一个可选的字符串类型查询参数 q

使用 Uvicorn 启动应用:

uvicorn main:app --reload

访问 http://127.0.0.1:8000 查看根路径的响应:
访问 http://127.0.0.1:8000/items/42?q=runoob 查看带路径参数和查询参数的响应:

\(FastAPI\) 交互式 \(API\) 文档

(网不好会打不开。。。)
默认情况下,可以通过访问 http://127.0.0.1:8000/docs 来打开 \(Swagger\) \(UI\) 风格的文档
或者通过 http://127.0.0.1:8000/redoc 来打开 \(ReDoc\) 风格的文档

交互式文档实时更新、自动验证、便于测试

# 直接在前面的代码上修改,不需要重启服务器,会自动重载(因为在 uvicorn 命令添加了 --reload 选项)

from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    price: float
    is_offer: Union[bool, None] = None


@app.get("/")
def read_root():
    return {"Hello": "World"}


@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
    return {"item_id": item_id, "q": q}


@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item):
    return {"item_name": item.name, "item_id": item_id}

\(FastAPT\)基本路由

路由都映射到应用程序中的一个函数
形如:

xxx = FastAPI()   # 创建 FastAPI 应用实例
@xxx.get(……)   # 装饰器创建一个路由
def fun():
  return {……}

\(FastAPI\) 请求和响应

# 接受两个查询参数 skip 和 limit
@app.get("/items/")
def read_item(skip: int = 0, limit: int = 10):
    return {"skip": skip, "limit": limit}
#  http://127.0.0.1:8000/items/?skip=1&limit=5
# 把参数设置在路径上
@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = None):
    return {"item_id": item_id, "q": q}
# http://127.0.0.1:8000/items/5/?q=runoob
# 使用 Pydantic 模型 Item 定义了一个请求体
class Item(BaseModel):
    name: str
    description: str = None
    price: float
    tax: float = None

@app.post("/items/")
def create_item(item: Item):
    return item

使用 HeaderCookie 类型注解获取请求头和 Cookie 数据

from fastapi import Header, Cookie
from fastapi import FastAPI

app = FastAPI()

@app.get("/items/")
def read_item(user_agent: str = Header(None), session_token: str = Cookie(None)):
    return {"User-Agent": user_agent, "Session-Token": session_token}

使用 RedirectResponse 实现重定向,将客户端重定向到 /items/ 路由

from fastapi import Header, Cookie
from fastapi import FastAPI
from fastapi.responses import RedirectResponse

app = FastAPI()

@app.get("/items/")
def read_item(user_agent: str = Header(None), session_token: str = Cookie(None)):
    return {"User-Agent": user_agent, "Session-Token": session_token}

@app.get("/redirect")
def redirect():
    return RedirectResponse(url="/items/")
# 访问 http://127.0.0.1:8000/redirect/ 会自动跳转到 http://127.0.0.1:8000/items/ 页面

使用 HTTPException 抛出异常,返回自定义的状态码和详细信息

from fastapi import Header, Cookie
from fastapi import FastAPI
from fastapi.responses import RedirectResponse

app = FastAPI()

@app.get("/items/")
def read_item(user_agent: str = Header(None), session_token: str = Cookie(None)):
    return {"User-Agent": user_agent, "Session-Token": session_token}

@app.get("/redirect")
def redirect():
    return RedirectResponse(url="/items/")
# 在 item_id 为 42 会返回 404 状态码

使用 JSONResponse 自定义响应头

from fastapi import FastAPI
from fastapi.responses import JSONResponse

app = FastAPI()

@app.get("/items/{item_id}")
def read_item(item_id: int):
    content = {"item_id": item_id}
    headers = {"X-Custom-Header": "custom-header-value"}
    return JSONResponse(content=content, headers=headers)

\(FastAPI Pydantic\) 模型

定义 \(Pydantic\) 模型

from pydantic import BaseModel

class Item(BaseModel):
    name: str
    description: str = None
    price: float
    tax: float = None
# `name` 和 `price` 是必需的字段,而 `description` 和 `tax` 是可选的字段,其默认值为 `None`

使用 \(Pydantic\) 模型

  1. 请求体验证
    Pydantic 模型用作请求体(\(Request Body\)),以自动验证和解析客户端发送的数据
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    description: str = None
    price: float
    tax: float = None

@app.post("/items/")
def create_item(item: Item):
    return item

可以使用requests发送数据(pip install requests

import requests

url = "http://127.0.0.1:8000/items/"

data = {
    "name": ……,
    "description": ……,
    "price": ……,
    "tax": ……
}

response = requests.post(url, json=data)
print(response.json())

查询参数验证

Pydantic 模型还可以用于验证查询参数、路径参数等。

from fastapi import FastAPI, Query
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    description: str = None
    price: float
    tax: float = None

@app.get("/items/")
def read_item(item: Item, q: str = Query(..., max_length=10)):
    return {"item": item, "q": q}

\(FastAPI\) 路径操作依赖项

  1. 预处理
    在路由执行前预处理输入数据的功能
from fastapi import Depends, FastAPI

app = FastAPI()

# 依赖项函数
def common_parameters(q: str = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}

@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    return commons

# 用到 await,就必须加 async,现在加不加 async,运行起来完全没区别
  1. 后处理(没看懂\(qwq\)
    在路由执行后执行一些逻辑,不过例子好像有点问题。。。
from fastapi import Depends, FastAPI, HTTPException

app = FastAPI()

# 依赖项函数
def common_parameters(q: str = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}

# 路由操作函数
@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    return commons

# 后处理函数
async def after_request():
    # 这里可以执行一些后处理逻辑,比如记录日志
    pass

# 后处理依赖项
@app.get("/items/", response_model=dict)
async def read_items_after(request: dict = Depends(after_request)):
    return {"message": "Items returned successfully"}
  1. 多个依赖项的组合
from fastapi import Depends, FastAPI, HTTPException

app = FastAPI()

# 依赖项函数1
def common_parameters(q: str = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}

# 依赖项函数2
def verify_token(token: str = Depends(common_parameters)):
    if token is None:
        raise HTTPException(status_code=400, detail="Token required")
    return token

# 路由操作函数
@app.get("/items/")
async def read_items(token: dict = Depends(verify_token)):
    return token
  1. 异步依赖项
    依赖项函数和后处理函数可以是异步的(看不懂orz)
from fastapi import Depends, FastAPI, HTTPException
from typing import Optional
import asyncio

app = FastAPI()

# 异步依赖项函数
async def get_token():
    # 模拟异步操作
    await asyncio.sleep(2)
    return "fake-token"

# 异步路由操作函数
@app.get("/items/")
async def read_items(token: Optional[str] = Depends(get_token)):
    return {"token": token}

\(FastAPI\) 表单数据

  1. 声明表单数据模型
    需要pip install python-multipart
from fastapi import FastAPI, Form

app = FastAPI()


@app.post("/login/")
async def login(username: str = Form(), password: str = Form()):
    return {"username": username}
  1. 在路由中接收表单数据
from fastapi import FastAPI, Form

app = FastAPI()

# 路由操作函数
@app.post("/items/")
async def create_item(
    name: str = Form(...),
    description: str = Form(None),
    price: float = Form(..., gt=0),
):
    return {"name": name, "description": description, "price": price}
  1. 表单数据的验证和文档生成
    使用 Pydantic 模型和 Form 类型,表单数据的验证和文档生成都是自动的。

\(FastAPI\) 将根据模型中的字段信息生成交互式 \(API\) 文档,并根据验证规则进行数据验证。

  1. 处理文件上传
    如果表单包含文件上传,可以使用 UploadFile 类型处理。
from fastapi import FastAPI, File, UploadFile

app = FastAPI()

# 路由操作函数
@app.post("/files/")
async def create_file(file: UploadFile = File(...)):
    return {"filename": file.filename}

可以写以下代码进行测试

import requests

# 接口地址
url = "http://127.0.0.1:8000/files/"

# 要上传的文件(改成你电脑上的文件)
files = {
    "file": open("t.html", "r")   # file 必须和接口参数名一样
}

# 发送请求
response = requests.post(url, files=files)

print(response.json())

\(ASGI\) 与异步编程

\(WSGI vs ASGI\) 对比:

# WSGI 应用(同步)- 传统 Flask 风格
def wsgi_app(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return [b'Hello World']

# ASGI 应用(异步)- FastAPI 风格
async def asgi_app(scope, receive, send):
    await send({
        'type': 'http.response.start',
        'status': 200,
        'headers': [(b'content-type', b'text/plain')],
    })
    await send({
        'type': 'http.response.body',
        'body': b'Hello World',
    })

同步 \(vs\) 异步执行:

import asyncio
import time
import httpx

# 同步方式 - 阻塞执行
def sync_fetch_data():
    start_time = time.time()
    # 模拟三个网络请求
    time.sleep(1)  # 第一个请求
    time.sleep(1)  # 第二个请求
    time.sleep(1)  # 第三个请求
    print(f"同步执行耗时: {time.time() - start_time:.2f}秒")  # 约3秒

# 异步方式 - 并发执行
async def async_fetch_data():
    start_time = time.time()
    # 三个请求并发执行
    await asyncio.gather(
        asyncio.sleep(1),  # 第一个请求
        asyncio.sleep(1),  # 第二个请求
        asyncio.sleep(1),  # 第三个请求
    )
    print(f"异步执行耗时: {time.time() - start_time:.2f}秒")  # 约1秒

# 运行示例
sync_fetch_data()  # 输出: 同步执行耗时: 3.00秒
asyncio.run(async_fetch_data())  # 输出: 异步执行耗时: 1.00秒

\(FastAPI\) 中的异步路径操作:

from fastapi import FastAPI
import asyncio

app = FastAPI()

# 同步路径操作
@app.get("/sync")
def sync_endpoint():
    # 同步操作会阻塞整个应用
    time.sleep(2)
    return {"message": "同步响应"}

# 异步路径操作(推荐)
@app.get("/async")
async def async_endpoint():
    # 异步操作不会阻塞其他请求
    await asyncio.sleep(2)
    return {"message": "异步响应"}

# 异步数据库操作示例  
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    # 异步数据库查询
    user = await database.fetch_one(
        "SELECT * FROM users WHERE id = :user_id", 
        {"user_id": user_id}
    )
    return user

适合异步的场景:

  • 数据库操作
  • 网络请求(\(API\) 调用)
  • 文件 \(I/O\) 操作
  • 长时间等待的操作

不适合异步的场景:

  • \(CPU\) 密集型计算
  • 简单的数据处理
  • 没有 \(I/O\) 等待的操作

\(HTTP\) 方法与状态码

\(HTTP\) 方法的语义

from fastapi import FastAPI, status, HTTPException
from fastapi.responses import JSONResponse

app = FastAPI()

# GET - 安全且幂等
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    """获取资源,不修改服务器状态"""
    user = find_user(user_id)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="User not found"
        )
    return user

# POST - 不安全且非幂等
@app.post("/users", status_code=status.HTTP_201_CREATED)
async def create_user(user: UserCreate):
    """创建新资源"""
    if user_exists(user.email):
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="User already exists"
        )
    new_user = create_new_user(user)
    return new_user

# PUT - 不安全但幂等
@app.put("/users/{user_id}")
async def replace_user(user_id: int, user: UserUpdate):
    """完整替换资源"""
    if not user_exists(user_id):
        # PUT 可以创建资源
        return create_user_with_id(user_id, user)
    return replace_existing_user(user_id, user)

# PATCH - 不安全且通常非幂等
@app.patch("/users/{user_id}")
async def update_user(user_id: int, user: UserPatch):
    """部分更新资源"""
    existing_user = find_user(user_id)
    if not existing_user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="User not found"
        )
    return patch_user_fields(existing_user, user)

# DELETE - 不安全但幂等
@app.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(user_id: int):
    """删除资源"""
    if not user_exists(user_id):
        # 幂等性:删除不存在的资源仍返回成功
        return JSONResponse(status_code=status.HTTP_204_NO_CONTENT)
    delete_existing_user(user_id)
    return JSONResponse(status_code=status.HTTP_204_NO_CONTENT)

\(HTTP\) 状态码的使用

from fastapi import status

# 2xx 成功响应
@app.post("/users", status_code=status.HTTP_201_CREATED)  # 201 Created
@app.get("/users")  # 200 OK(默认)
@app.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)  # 204 No Content

# 4xx 客户端错误
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    if user_id < 1:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,  # 400 Bad Request
            detail="User ID must be positive"
        )
    
    user = find_user(user_id)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,  # 404 Not Found
            detail="User not found"
        )
    
    return user

# 业务逻辑错误
@app.post("/users/{user_id}/follow")
async def follow_user(user_id: int, current_user_id: int):
    if user_id == current_user_id:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,  # 422 Unprocessable Entity
            detail="Cannot follow yourself"
        )
    
    if is_already_following(current_user_id, user_id):
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,  # 409 Conflict
            detail="Already following this user"
        )

# 5xx 服务器错误(通常由异常处理器处理)
@app.exception_handler(Exception)
async def general_exception_handler(request, exc):
    return JSONResponse(
        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,  # 500 Internal Server Error
        content={"detail": "Internal server error"}
    )

常见状态码:

# 成功状态码
200 OK                    # 请求成功
201 Created              # 资源创建成功
204 No Content           # 成功但无内容返回
206 Partial Content      # 部分内容(分页、断点续传)

# 重定向
301 Moved Permanently    # 永久重定向
302 Found               # 临时重定向
304 Not Modified        # 资源未修改(缓存)

# 客户端错误
400 Bad Request         # 请求格式错误
401 Unauthorized        # 未认证
403 Forbidden          # 已认证但无权限
404 Not Found          # 资源不存在
405 Method Not Allowed  # HTTP 方法不允许
409 Conflict           # 资源冲突
422 Unprocessable Entity # 数据验证失败
429 Too Many Requests   # 请求过于频繁

# 服务器错误
500 Internal Server Error # 服务器内部错误
502 Bad Gateway          # 网关错误
503 Service Unavailable  # 服务不可用

\(SON\) 数据格式

\(JSON\) 基础与最佳实践

from datetime import datetime
from decimal import Decimal
from typing import Optional, List
from pydantic import BaseModel, Field
import json

# JSON 数据格式规范
class UserResponse(BaseModel):
    id: int
    username: str
    email: str
    full_name: Optional[str] = None
    is_active: bool = True
    created_at: datetime
    updated_at: Optional[datetime] = None
    
    # JSON 序列化配置
    class Config:
        # 允许使用字段别名
        allow_population_by_field_name = True
        # JSON 编码器
        json_encoders = {
            datetime: lambda v: v.isoformat(),
            Decimal: lambda v: float(v)
        }

# 响应格式标准化
class APIResponse(BaseModel):
    """标准 API 响应格式"""
    success: bool = True
    message: str = "操作成功"
    data: Optional[dict] = None
    errors: Optional[List[str]] = None
    timestamp: datetime = Field(default_factory=datetime.now)

# 分页响应格式
class PaginatedResponse(BaseModel):
    items: List[dict]
    total: int
    page: int
    size: int
    pages: int

@app.get("/users", response_model=PaginatedResponse)
async def get_users(page: int = 1, size: int = 10):
    """返回分页的用户列表"""
    users = get_users_paginated(page, size)
    total = count_users()
    
    return PaginatedResponse(
        items=users,
        total=total,
        page=page,
        size=size,
        pages=(total + size - 1) // size
    )

\(JSON Schema\) 与数据验证

from pydantic import BaseModel, validator, Field
from typing import Optional, List
from enum import Enum

class UserRole(str, Enum):
    ADMIN = "admin"
    USER = "user"
    MODERATOR = "moderator"

class UserCreate(BaseModel):
    username: str = Field(..., min_length=3, max_length=50, description="用户名")
    email: str = Field(..., regex=r'^[\w\.-]+@[\w\.-]+\.\w+$', description="邮箱地址")
    password: str = Field(..., min_length=8, description="密码")
    full_name: Optional[str] = Field(None, max_length=100, description="全名")
    role: UserRole = Field(UserRole.USER, description="用户角色")
    age: Optional[int] = Field(None, ge=0, le=150, description="年龄")
    
    @validator('username')
    def username_must_be_alphanumeric(cls, v):
        if not v.isalnum():
            raise ValueError('用户名只能包含字母和数字')
        return v
    
    @validator('password')
    def password_strength(cls, v):
        if not any(c.isupper() for c in v):
            raise ValueError('密码必须包含至少一个大写字母')
        if not any(c.islower() for c in v):
            raise ValueError('密码必须包含至少一个小写字母')
        if not any(c.isdigit() for c in v):
            raise ValueError('密码必须包含至少一个数字')
        return v

    class Config:
        # 生成 JSON Schema 示例
        schema_extra = {
            "example": {
                "username": "johndoe",
                "email": "john@example.com",
                "password": "SecurePass123",
                "full_name": "John Doe",
                "role": "user",
                "age": 30
            }
        }

# 使用自定义验证器
@app.post("/users", response_model=UserResponse)
async def create_user(user: UserCreate):
    """创建新用户,包含数据验证"""
    # Pydantic 自动验证输入数据
    return await create_new_user(user)

\(FastAPI\) 特有

自动文档生成

from fastapi import FastAPI, Query, Path, Body
from fastapi.openapi.utils import get_openapi

app = FastAPI(
    title="我的 API",
    description="这是一个示例 API,展示 FastAPI 的功能",
    version="1.0.0",
    terms_of_service="http://example.com/terms/",
    contact={
        "name": "开发者",
        "url": "http://example.com/contact/",
        "email": "developer@example.com",
    },
    license_info={
        "name": "MIT",
        "url": "https://opensource.org/licenses/MIT",
    },
)

@app.get(
    "/users/{user_id}",
    summary="获取用户信息",
    description="根据用户 ID 获取用户的详细信息",
    response_description="用户信息对象",
    tags=["用户管理"]
)
async def get_user(
    user_id: int = Path(..., title="用户ID", description="要获取的用户ID", ge=1),
    include_posts: bool = Query(False, title="包含文章", description="是否包含用户的文章列表")
):
    """
    获取用户信息:
    
    - **user_id**: 用户的唯一标识符
    - **include_posts**: 是否在响应中包含用户的文章列表
    
    返回用户的基本信息,如果 include_posts 为 True,还会包含文章列表。
    """
    user = find_user(user_id)
    if include_posts:
        user.posts = get_user_posts(user_id)
    return user

# 自定义 OpenAPI schema
def custom_openapi():
    if app.openapi_schema:
        return app.openapi_schema
    
    openapi_schema = get_openapi(
        title="自定义 API 文档",
        version="2.5.0",
        description="这是自定义的 OpenAPI schema",
        routes=app.routes,
    )
    
    # 添加自定义信息
    openapi_schema["info"]["x-logo"] = {
        "url": "https://example.com/logo.png"
    }
    
    app.openapi_schema = openapi_schema
    return app.openapi_schema

app.openapi = custom_openapi

依赖注入系统预览

from fastapi import Depends

# 简单依赖
def get_current_user_id() -> int:
    # 从 token 中解析用户 ID
    return 123

def get_database_session():
    # 创建数据库会话
    db = DatabaseSession()
    try:
        yield db
    finally:
        db.close()

# 使用依赖
@app.get("/profile")
async def get_profile(
    user_id: int = Depends(get_current_user_id),
    db = Depends(get_database_session)
):
    return get_user_profile(db, user_id)

# 依赖链
def get_current_user(
    user_id: int = Depends(get_current_user_id),
    db = Depends(get_database_session)
):
    return db.query(User).filter(User.id == user_id).first()

@app.get("/dashboard")
async def get_dashboard(
    current_user: User = Depends(get_current_user)  # 依赖于其他依赖
):
    return generate_dashboard(current_user)
示例
from fastapi import FastAPI, HTTPException, status, Depends, Query
from pydantic import BaseModel, Field
from typing import List, Optional
from datetime import datetime
import asyncio

# 综合示例:博客 API
app = FastAPI(
    title="博客 API",
    description="展示 FastAPI 核心概念的博客系统",
    version="1.0.0"
)

# 数据模型
class PostBase(BaseModel):
    title: str = Field(..., min_length=1, max_length=200)
    content: str = Field(..., min_length=1)
    published: bool = Field(True)

class PostCreate(PostBase):
    pass

class PostResponse(PostBase):
    id: int
    author_id: int
    created_at: datetime
    updated_at: Optional[datetime] = None
    
    class Config:
        orm_mode = True

# 模拟数据库
posts_db = []
next_id = 1

# 依赖:获取当前用户(简化版)
async def get_current_user() -> int:
    # 实际应用中会从 JWT token 解析
    return 1

# 异步路径操作
@app.get("/posts", response_model=List[PostResponse], tags=["文章"])
async def list_posts(
    skip: int = Query(0, ge=0, description="跳过的文章数"),
    limit: int = Query(10, ge=1, le=100, description="返回的文章数"),
    published_only: bool = Query(True, description="只返回已发布的文章")
):
    """
    获取文章列表
    
    支持分页和筛选功能
    """
    # 模拟异步数据库查询
    await asyncio.sleep(0.1)
    
    filtered_posts = posts_db
    if published_only:
        filtered_posts = [p for p in posts_db if p["published"]]
    
    return filtered_posts[skip:skip + limit]

@app.post("/posts", response_model=PostResponse, status_code=status.HTTP_201_CREATED, tags=["文章"])
async def create_post(
    post: PostCreate,
    current_user_id: int = Depends(get_current_user)
):
    """
    创建新文章
    
    需要用户认证
    """
    global next_id
    
    # 模拟异步数据库操作
    await asyncio.sleep(0.1)
    
    new_post = {
        "id": next_id,
        "title": post.title,
        "content": post.content,
        "published": post.published,
        "author_id": current_user_id,
        "created_at": datetime.now(),
        "updated_at": None
    }
    
    posts_db.append(new_post)
    next_id += 1
    
    return new_post

@app.get("/posts/{post_id}", response_model=PostResponse, tags=["文章"])
async def get_post(post_id: int):
    """
    获取特定文章
    
    根据文章 ID 返回文章详情
    """
    # 模拟异步数据库查询
    await asyncio.sleep(0.1)
    
    post = next((p for p in posts_db if p["id"] == post_id), None)
    if not post:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Post with id {post_id} not found"
        )
    
    return post

# 健康检查端点
@app.get("/health", tags=["系统"])
async def health_check():
    """系统健康检查"""
    return {
        "status": "healthy",
        "timestamp": datetime.now(),
        "posts_count": len(posts_db)
    }

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

\(HTML\)

\(HTML\) 是用来描述网页的一种标记语言(感觉有点像\(markdown\)\(latex\))。

<!DOCTYPE html> 声明为 \(HTML5\) 文档

<html> 元素是 \(HTML\) 页面的根元素

<head> 元素包含了文档的元(\(meta\))数据,如 <meta charset="utf-8"> 定义网页编码格式为 utf-8

<title> 元素描述了文档的标题

<body> 元素包含了可见的页面内容

<h1> 元素定义一个大标题

<p> 元素定义一个段落

例如

<h1>这是一个标题</h1>
<h2>这是一个标题</h2>
<h3>这是一个标题</h3>

<p>这是一个段落。</p>
<p>这是另外一个段落。</p>

<a href="https://www.xxxx.com">这是一个链接</a>

<img src="/images.png" width="258" height="39" />

感觉自行查表即可 \(\rightarrow\)

\(JavaScript\)

写在 <script> 标签中,可以在 \(web\) 浏览器中执行

显示

  1. 弹出弹窗
window.alert(5+6)
window.alert("5+6")
  1. 操作 HTML 元素
    id 属性来标识 \(HTML\) 元素,并 innerHTML 来获取或插入元素内容
<p id="demo">段落内容</p>
<script>
document.getElementById("demo").innerHTML = "段落已修改。";
</script>

如果在文档已完成加载后执行 document.write,整个 \(HTML\) 页面将被覆盖,以下两个代码的效果不同

<button onclick="Function()">点我</button>

<script>
function Function() {
    document.write(Date());
}
</script>
<p id="demo">这是一个段落</p>

<button type="button" onclick="displayDate()">显示日期</button>

<script>
function displayDate(){
	document.getElementById("demo").innerHTML=Date();
}
</script>
  1. 写入控制台
    在浏览器中使用 $ F12 $ 来启用调试模式,可以在控制台看见输出
<script>
a = 5;
b = 6;
c = a + b;
console.log(c);
console.log(5);
console.log("c");
</script>

语法

  1. 字面量
    固定值称为字面量
  • 数字( $ Number $ )字面量: 可以是整数或者是小数,或者是科学计数( $ e $ )

  • 字符串( $ String $ )字面量: 可以使用单引号或双引号

可以在字符串中使用引号,只要不匹配包围字符串的引号即可

可以用内置属性 length 来计算字符串的长度

反斜杠是一个转义字符,将特殊字符转换为字符串字符

一些相关函数模板

  • 表达式字面量: 用于计算

  • 数组( $ Array $ )字面量: 定义一个数组,如 [40, 100, 1, 5, 25, 10]

可以用以下方式创建数组

var arr=new Array();
cars[0]="num 0";
cars[1]="num 1";
cars[2]="num 2";
var cars = new Array('num 1','num 2','num 3');
var cars = ['num 1','num 2','num 3'];
  • 对象( $ Object $ )字面量: 定义一个对象,如var person={firstname:"John", lastname:"Doe", id:5566};,声明可横跨多行

两种寻址方式name=person.lastname;name=person["lastname"];

对象的方法定义了一个函数,并作为对象的属性存储

//创建
methodName : function() {
    // 代码 
}
//访问
objectName.methodName()
  • 函数( $ Function $ )字面量: 定义一个函数,如 function myFunction(a, b) { return a * b;}
  1. 变量
    使用关键字 varletconst 来定义变量, 使用等号来为变量赋值,和其他任何编程语言一样, $ JavaScript $ 保留了一些关键字为自己所用
  • var: $ ES5 $ 引入的变量声明方式,具有函数作用域。

  • let: $ ES6 $ 引入的变量声明方式,具有块级作用域。

  • const: $ ES6 $ 引入的常量声明方式,具有块级作用域,且值不可变。

重新声明 $ JavaScript $ 变量,该变量的值不会丢失(也不会报错

  1. 数据类型
  • 值类型(基本类型):字符串( $ String $ )、数字( $ Number $ )、布尔( $ Boolean $ )、空( $ Null $ )、未定义( $ Undefined $ )、 $ Symbol $ 。

  • 引用数据类型(对象类型):对象( $ Object $ )、数组( $ Array $ )、函数( $ Function $ ),还有两个特殊的对象:正则( $ RegExp $ )和日期( $ Date $ )

$ JavaScript $ 拥有动态类型,相同的变量可用作不同的类型,即以下代码合法

var x;               
	// x 为 undefinedvar x = 5;           
	// 现在 x 为数字
var x = "John";      // 现在 x 为字符串

变量的数据类型可以使用 typeof 操作符来查看,

typeof "John"                // 返回 string
typeof 3.14                  // 返回 number
typeof false                 // 返回 boolean
typeof {name:'John', age:34} // 返回 object

typeof [1,2,3,4]             // 返回 object
// 正确检测数组的方法
Array.isArray([1,2,3]); // true
[1,2,3] instanceof Array; // true

Undefined 这个值表示变量不含有值。

可以通过将变量的值设置为 null 来清空变量。

声明新变量时,可以使用关键词 new 来声明其类型

JavaScript 变量均为对象,当声明一个变量时,就创建了一个新的对象

运算符逻辑运算符

  1. 其他
  • 分号用于分隔 $ JavaScript $ 语句
  • 浏览器按照编写顺序依次执行每条语句
  • 用花括号组合代码块
  • 语句标识符
  • 忽略多余的空格
  • 文本字符串 中使用反斜杠对代码行进行换行
  • \(html\)事件
  • if-else switch-case for/while break/continue等和其他语言类似
  • ……

\(markdown\)实时预览服务代码

code.py
# main.py
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse, Response  
import markdown

app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")

@app.get("/favicon.ico", include_in_schema=False)
async def favicon():
    return Response(status_code=204)

@app.post("/api/convert")
async def convert(request: Request):
    data = await request.json()
    md_text = data.get("markdown", "")
    html = markdown.markdown(
        md_text,
        extensions=["fenced_code", "codehilite", "tables", "sane_lists"]
    )
    return {"html": html}

@app.get("/", response_class=HTMLResponse)
async def index():
    with open("static/index.html", "r", encoding="utf-8") as f:
        return f.read()

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)
code.html
# static/index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8"/>
    <title>Markdown 实时预览</title>
    <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/highlight.js/11.7.0/styles/github.min.css"/>
    <script src="https://cdn.bootcdn.net/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
    <script>
        window.MathJax = {
            tex: {
                inlineMath: [['$', '$'], ['\\(', '\\)']],  // 支持 $公式$ 和 \(公式\)
                displayMath: [['$$', '$$'], ['\\[', '\\]']],  // 支持 $$公式$$ 和 \[公式\]
                processEscapes: true  // 处理转义字符
            },
            svg: {
                fontCache: 'global'
            },
            startup: {
                typeset: false  // 禁用自动渲染,手动控制
            }
        };
    </script>
    <script src="https://cdn.bootcdn.net/ajax/libs/mathjax/3.2.2/es5/tex-mml-chtml.min.js"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { display: flex; height: 100vh; font-family: system-ui; }
        #editor, #preview { flex: 1; height: 100%; padding: 20px; overflow-y: auto; }
        #editor { border-right: 1px solid #eee; font-size: 16px; outline: none; font-family: monospace; }
        #preview { background: #fafafa; }
        .MathJax { font-size: 1.1em !important; }
    </style>
</head>
<body>
<textarea id="editor" placeholder="输入 Markdown...
示例公式:
行内公式:$E=mc^2$
块级公式:$$\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}$$"></textarea>
<div id="preview"></div>

<script>
const editor = document.getElementById('editor');
const preview = document.getElementById('preview');

// 防抖
function debounce(fn, delay = 200) {
    let t;
    return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), delay); };
}

const update = debounce(async () => {
    // 保存当前编辑器滚动位置
    const editorScrollTop = editor.scrollTop;
    
    const res = await fetch('/api/convert', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ markdown: editor.value })
    });
    const data = await res.json();
    preview.innerHTML = data.html;
    
    // 1. 代码高亮
    document.querySelectorAll('pre code').forEach(b => hljs.highlightElement(b));
    
    // 2. 手动触发 MathJax 渲染公式
    if (window.MathJax) {
        MathJax.typesetClear();  // 清空之前的渲染
        MathJax.typeset([preview]);  // 只渲染预览区域的公式
    }
    
    // 3. 恢复滚动同步(输入后保持预览区滚动位置与编辑器一致)
    syncScroll(editor, preview, editorScrollTop);
});

// 滚动同步函数(增加边界判断 + 仅主动滚动时同步)
let isScrolling = false;
function syncScroll(fromEl, toEl, fixedScrollTop = null) {
    // 防止重复触发
    if (isScrolling) return;
    isScrolling = true;
    
    try {
        // 计算滚动比例(处理边界值,避免除以 0)
        const fromHeight = fromEl.scrollHeight - fromEl.clientHeight;
        const toHeight = toEl.scrollHeight - toEl.clientHeight;
        
        // 如果内容高度小于可视高度,无需滚动
        if (fromHeight <= 0 || toHeight <= 0) {
            toEl.scrollTop = 0;
            return;
        }
        
        // 使用固定值或实时计算滚动位置
        const scrollTop = fixedScrollTop !== null ? fixedScrollTop : fromEl.scrollTop;
        const scrollRatio = scrollTop / fromHeight;
        toEl.scrollTop = scrollRatio * toHeight;
    } finally {
        // 释放锁
        setTimeout(() => { isScrolling = false; }, 50);
    }
}

// 绑定事件:仅在主动滚动时同步
editor.addEventListener('scroll', () => syncScroll(editor, preview));
preview.addEventListener('scroll', () => syncScroll(preview, editor));

// 输入事件:更新内容 + 同步滚动(保持预览区跟随编辑器)
editor.addEventListener('input', update);

// 初始化触发一次渲染
update();
</script>
</body>
</html>

edit

posted @ 2026-03-24 01:03  雨夜风月  阅读(12)  评论(0)    收藏  举报