Estoy sirviendo la aplicación React desde FastAPI montando
app.mount("/static", StaticFiles(directory="static"), name="static") @app.route('/session') async def renderReactApp(request: Request): return templates.TemplateResponse("index.html", {"request": request})
por esta aplicación React se sirve y el enrutamiento React también funciona bien en el lado del cliente, pero tan pronto como el cliente recarga en una ruta que no está definida en el servidor pero se usa en la aplicación React FastAPI not found
la devolución para solucionar esto, hice algo como a continuación.
@app.route('/network')
@app.route('/gat')
@app.route('/session')
async def renderReactApp(request: Request): return templates.TemplateResponse("index.html", {"request": request})
pero me parece extraño y erróneo, ya que necesito agregar todas las rutas tanto en el back-end como en el front-end.
Estoy seguro de que debe haber algo como Flask @flask_app.add_url_rule('/<path:path>', 'index', index)
en FastAPI que servidor todas las rutas arbitrarias
Como señaló @mecampbellsoup: generalmente hay otros archivos estáticos que deben servirse con una aplicación como esta.
Esperemos que esto sea útil para alguien más:
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' )
Estas modificaciones hacen que el montaje de StaticFiles actúe de manera similar al paquete NPM connect-history-api-fallback .
react-router
Hice una función muy simple que es totalmente compatible con las aplicaciones react-router
y create-react-app
(la mayoría de los casos de uso)
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)
Aquí hay un ejemplo de servir múltiples rutas (o funciones de carga diferida) usando una sola URL de publicación. El cuerpo de una solicitud a la URL contendría el nombre de una función para llamar y los datos para pasar a la función, si corresponde. Los archivos *.py
en el directorio route routes/
contienen las funciones, y las funciones comparten el mismo nombre que sus archivos.
estructura del proyecto
app.py routes/ |__helloworld.py |_*.py
rutas/holamundo.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)
Este ejemplo devuelve {"hello": "world"}
con la solicitud de publicación a continuación.
curl -X POST "http://localhost:70/api" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"route\":\"helloworld\",\"data\":{\"hello\": \"world\"}}"
El beneficio de esta configuración es que se puede usar una única URL de publicación para completar cualquier tipo de solicitud (obtener, eliminar, colocar, etc.), ya que el "tipo de solicitud" es la lógica definida en la función. Por ejemplo, si se get_network.py
y delete_network.py
al directorio route routes/
rutas/get_network.py
def get_network(id: str): network_name = '' # logic to retrieve network by id from db return network_name
rutas/eliminar_red.py
def delete_network(id: str): network_deleted = False # logic to delete network by id from db return network_deleted
luego, una carga útil de solicitud de {"route": "get_network", "data": "network_id"}
devuelve un nombre de red, y {"route": "delete_network", "data": "network_id"}
devolvería un valor booleano que indica si la red fue eliminada o no.
Dado que FastAPI se basa en Starlette, puede usar lo que ellos llaman "convertidores" con sus parámetros de ruta, usando el tipo de path
en este caso, que "devuelve el resto de la ruta, incluidos los /
caracteres adicionales".
Consulte https://www.starlette.io/routing/#path-parameters como referencia.
Si su aplicación de reacción (o vue o ...) está usando una ruta base, puede hacer algo como esto, que asigna cualquier cosa después /my-app/
a la variable 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})
Si no está utilizando una ruta base única como /my-app/
(que parece ser su caso de uso), aún puede lograr esto con una ruta general, que debe ir después de cualquier otra ruta para que no lo haga. sobrescribirlos:
@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})
(De hecho, le gustaría usar este catch-all independientemente para detectar la diferencia entre las solicitudes de /my-app/
y /my-app
)
Supongamos que tiene una estructura de aplicación como esta:
├── main.py └── routers └── my_router.py
Y los enrutadores que creamos en 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
Sumerjámonos en main.py
primero, necesitamos importar nuestro enrutador que declaramos con
from routers import my_router
Entonces vamos a crear una instancia de aplicación
from fastapi import FastAPI from routers import my_router app = FastAPI()
Entonces, ¿cómo agregamos nuestros enrutadores?
from fastapi import FastAPI from routers import my_router app = FastAPI() app.include_router(my_router.router)
También puede agregar prefijo, etiqueta, etc.
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!"], )