如何在 FastAPI 中的一条路径上捕获任意路径?

How to capture arbitrary paths at one route in FastAPI?

我正在通过 FastAPIReact 应用提供服务 安装

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

@app.route('/session')
async def renderReactApp(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})

通过这个 React 应用程序得到服务并且 React 路由在客户端也可以正常工作 但是一旦客户端重新加载到未在服务器上定义但在 React 应用程序 FastAPI 中使用的路由 return not found 来解决此问题,我做了如下操作。


async def renderReactApp(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})

但这对我来说似乎很奇怪而且是错误的,因为我需要在后端和前端添加每条路由。

我确定 FastAPI 中一定有类似 Flask @flask_app.add_url_rule('/<path:path>', 'index', index) 的东西,它将为所有任意路径提供服务

假设您的应用程序结构如下:

├── main.py
└── routers
    └── my_router.py

以及我们在 my_router.py

中创建的路由器
from fastapi import APIRouter

router = APIRouter()

@router.get("/some")
async def some_path():
    pass

@router.get("/path")
async def some_other_path():
    pass

@router.post("/some_post_path")
async def some_post_path():
    pass

让我们深入了解 main.py 首先我们需要导入我们用

声明的路由器
from routers import my_router

然后让我们创建一个app实例

from fastapi import FastAPI
from routers import my_router

app = FastAPI()

那么我们如何添加我们的路由器呢?

from fastapi import FastAPI
from routers import my_router

app = FastAPI()

app.include_router(my_router.router)

您还可以添加前缀、标签等

from fastapi import FastAPI
from routers import my_router

app = FastAPI()


app.include_router(
    my_router.router,
    prefix="/custom_path",
    tags=["We are from router!"],
)

让我们检查文档

由于 FastAPI 基于 Starlette,您可以在路由参数中使用他们所谓的“转换器”,在这种情况下使用类型 path,即“returns 路径的其余部分,包括任何额外的 / 个字符。"

参考https://www.starlette.io/routing/#path-parameters

如果您的 React(或 vue 或...)应用程序正在使用基本路径,您可以这样做,它将 /my-app/ 之后的任何内容分配给 rest_of_path 变量:

@app.get("/my-app/{rest_of_path:path}")
async def serve_my_app(request: Request, rest_of_path: str):
    print("rest_of_path: "+rest_of_path)
    return templates.TemplateResponse("index.html", {"request": request})

如果您没有使用像 /my-app/ 这样的唯一基本路径(这似乎是您的用例),您仍然可以使用 catch-all 路由来完成此操作,该路由应该在任何其他路由之后路线,以便它不会覆盖它们:

@app.route("/{full_path:path}")
async def catch_all(request: Request, full_path: str):
    print("full_path: "+full_path)
    return templates.TemplateResponse("index.html", {"request": request})

(事实上你会想要使用这个 catch-all 无论如何为了捕捉 /my-app//my-app 请求之间的区别)

这是一个使用单个 post url 服务多个路由(或延迟加载函数)的示例。对 url 的请求正文将包含要调用的函数的名称和要传递给该函数的数据(如果有)。 routes/ 目录中的 *.py 个文件包含函数,并且函数与其文件同名。

项目结构

app.py
routes/
  |__helloworld.py
  |_*.py

routes/helloworld.py

def helloworld(data):
    return data

app.py

from os.path import split, realpath
from importlib.machinery import SourceFileLoader as sfl
import uvicorn
from typing import Any
from fastapi import FastAPI
from pydantic import BaseModel

# set app's root directory 
API_DIR = split(realpath(__file__))[0]

class RequestPayload(BaseModel):
  """payload for post requests"""
  # function in `/routes` to call
  route: str = 'function_to_call'
  # data to pass to the function
  data: Any = None

app = FastAPI()

@app.post('/api')
async def api(payload: RequestPayload):
    """post request to call function"""
  # load `.py` file from `/routes`
  route = sfl(payload.route,
    f'{API_DIR}/routes/{payload.route}.py').load_module()
  # load function from `.py` file
  func = getattr(route, payload.route)
  # check if function requires data
  if ('data' not in payload.dict().keys()):
    return func()
  return func(payload.data)

此示例 returns {"hello": "world"} 与下面的 post 请求。

curl -X POST "http://localhost:70/api" -H  "accept: application/json" -H  "Content-Type: application/json" -d "{\"route\":\"helloworld\",\"data\":{\"hello\": \"world\"}}"

此设置的好处是单个 post url 可用于完成任何类型的请求(get、delete、put 等),作为“请求类型”是函数中定义的逻辑。例如,如果 get_network.pydelete_network.py 添加到 routes/ 目录

routes/get_network.py

def get_network(id: str):
  network_name = ''
  # logic to retrieve network by id from db
  return network_name

routes/delete_network.py

def delete_network(id: str):
  network_deleted = False
  # logic to delete network by id from db
  return network_deleted

然后 {"route": "get_network", "data": "network_id"} return 的请求负载是一个网络名称,{"route": "delete_network", "data": "network_id"} 将 return 一个布尔值,指示网络是否已删除。

正如@mecampbellsoup 所指出的:通常还有其他静态文件需要与这样的应用程序一起提供。

希望这对其他人有用:

import os
from typing import Tuple

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

app = FastAPI()


class SinglePageApplication(StaticFiles):
    """Acts similar to the bripkens/connect-history-api-fallback
    NPM package."""

    def __init__(self, directory: os.PathLike, index='index.html') -> None:
        self.index = index

        # set html=True to resolve the index even when no
        # the base path is passed in
        super().__init__(directory=directory, packages=None, html=True, check_dir=True)

    async def lookup_path(self, path: str) -> Tuple[str, os.stat_result]:
        """Returns the index file when no match is found.

        Args:
            path (str): Resource path.

        Returns:
            [tuple[str, os.stat_result]]: Always retuens a full path and stat result.
        """
        full_path, stat_result = await super().lookup_path(path)

        # if a file cannot be found
        if stat_result is None:
            return await super().lookup_path(self.index)

        return (full_path, stat_result)



app.mount(
    path='/',
    app=SinglePageApplication(directory='path/to/dist'),
    name='SPA'
)

这些修改使 StaticFiles 挂载行为类似于 connect-history-api-fallback NPM 包。

react-router

兼容的简单有效的解决方案

我做了一个非常简单的功能,它完全兼容 react-routercreate-react-app 应用程序(大多数用例)

函数

from pathlib import Path
from typing import Union

from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates


def serve_react_app(app: FastAPI, build_dir: Union[Path, str]) -> FastAPI:
    """Serves a React application in the root directory `/`

    Args:
        app: FastAPI application instance
        build_dir: React build directory (generated by `yarn build` or
            `npm run build`)

    Returns:
        FastAPI: instance with the react application added
    """
    if isinstance(build_dir, str):
        build_dir = Path(build_dir)

    app.mount(
        "/static/",
        StaticFiles(directory=build_dir / "static"),
        name="React App static files",
    )
    templates = Jinja2Templates(directory=build_dir.as_posix())

    @app.get("/{full_path:path}")
    async def serve_react_app(request: Request, full_path: str):
        """Serve the react app
        `full_path` variable is necessary to serve each possible endpoint with
        `index.html` file in order to be compatible with `react-router-dom
        """
        return templates.TemplateResponse("index.html", {"request": request})

    return app

用法

import uvicorn
from fastapi import FastAPI


app = FastAPI()

path_to_react_app_build_dir = "./frontend/build"
app = serve_react_app(app, path_to_react_app_build_dir)

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