• “农场”技术栈是什么?浅聊FARM Stack


    介绍 FARM 堆栈 - FastAPI、React 和 MongoDB。

    长按关注《Python学研大本营》,加入读者群,分享更多精彩 扫码关注《Python学研大本营》,加入读者群,分享更多精彩

    当我获得第一份编程工作时,LAMP(Linux、Apache、MySQL、PHP)堆栈非常主流。我在工作中使用 WAMP,在家里使用 DAMP,并将我们的客户部署到 SAMP。

    但是现在所有带有令人难忘的首字母缩略词的堆栈似乎都非常适合 JavaScript。MEAN(MongoDB、Express、Angular、Node.js)、MERN(MongoDB、Express、React、Node.js)、MEVN(MongoDB、Express、Vue、Node.js)、JAM(JavaScript、API、Markup)等。

    尽管我很喜欢使用 React 和 Vue,但 Python 仍然是我最喜欢的用于构建后端 Web 服务的语言。我想要从 MERN 获得的相同好处——MongoDB、速度、灵活性、最少的样板——但使用 Python 而不是 Node.js。考虑到这一点,我想介绍FARM stack;FastAPI、React 和 MongoDB。

    什么是 FastAPI?

    FARM 堆栈在许多方面与 MERN 非常相似。我们保留了 MongoDB 和 React,但我们用 Python 和 FastAPI 替换了 Node.js 和 Express 后端。

    FastAPI 是一个现代、高性能的 Python 3.6+ Web 框架 . 就 Web 框架而言,它是令人难以置信的新事物。我能找到的最早的 git commit 是从 2018 年 12 月 5 日开始的,但它是 Python 社区的一颗冉冉升起的新星。它已经被以下公司用于生产 微软、优步和 Netflix。

    它速度迅速。基准测试表明它不如 golang 的 chi 或 fasthttp 快,但它比所有其他测试过的 Python 框架都快,并且也击败了大多数 Node.js 框架。

    入门

    如果您想尝试一下 FARM 堆栈,我已经创建了可以从 GitHub 克隆的示例 TODO 应用程序。

    git clone git@github.com:mongodb-developer/FARM-Intro.git
    

    代码被组织到两个目录中:后端和前端。后端代码是我们的 FastAPI 服务器。此目录中的代码与我们的 MongoDB 数据库交互,创建我们的 API 端点,并且感谢 OAS3(OpenAPI 规范 3)。它还生成我们的交互式文档。

    运行 FastAPI 服务器

    在我浏览代码之前,请尝试自己运行 FastAPI 服务器。您将需要 Python 3.8+ 和 MongoDB 数据库。一个 免费的 Atlas 集群会绰绰有余。 记下您的 MongoDB 用户名、密码和连接字符串,稍后您将需要它们。

    安装依赖

    1. cd FARM-Intro/backend
    2. pip install -r requirements.txt

    配置环境变量

    1. export DEBUG_MODE=True
    2. export DB_URL="mongodb+srv://:@/?retryWrites=true&w=majority"
    3. export DB_NAME="farmstack"

    安装和配置完所有内容后,您可以运行服务器python main.py并在您的浏览器中访问http://localhost:8000/docs。

    这个交互式文档是由 FastAPI 自动为我们生成的,是在开发过程中尝试 API 的好方法。您可以看到我们涵盖了 CRUD 的主要元素。尝试添加、更新和删除一些任务,并探索从 FastAPI 服务器返回的响应。

    创建 FastAPI 服务器

    我们初始化服务器main.py;这是我们创建应用程序的地方。

    app = FastAPI()
    

    附加我们的路由或 API 端点。

    app.include_router(todo_router, tags=["tasks"], prefix="/task")
    

    启动异步事件循环和 ASGI 服务器。

    1. if __name__ == "__main__":
    2.     uvicorn.run(
    3.         "main:app",
    4.         host=settings.HOST,
    5.         reload=settings.DEBUG_MODE,
    6.         port=settings.PORT,
    7.     )

    它也是我们打开和关闭与 MongoDB 服务器的连接的地方。

    1. @app.on_event("startup")
    2. async def startup_db_client():
    3.     app.mongodb_client = AsyncIOMotorClient(settings.DB_URL)
    4.     app.mongodb = app.mongodb_client[settings.DB_NAME]
    5. @app.on_event("shutdown")
    6. async def shutdown_db_client():
    7.     app.mongodb_client.close()

    因为 FastAPI 是一个异步框架,所以我们使用 Motor 连接到我们的 MongoDB 服务器。Motor 是官方维护的 MongoDB 异步 Python 驱动程序 。

    当应用程序启动事件被触发时,我打开一个到 MongoDB 的连接,并确保它可以通过应用程序对象访问,这样我以后可以在我的不同路由器中访问它。

    定义模型

    许多人认为 MongoDB 是无模式的,这是错误的。MongoDB 具有灵活的模式。也就是说,默认情况下集合不强制执行文档结构,因此您可以灵活地选择最符合您的应用程序及其性能要求的任何数据建模选择。因此,在使用 MongoDB 数据库时创建模型并不少见。 TODO 应用程序的模型在 中backend/apps/todo/models.py,正是这些模型帮助 FastAPI 创建交互式文档。

    1. class TaskModel(BaseModel):
    2.     id: str = Field(default_factory=uuid.uuid4, alias="_id")
    3.     name: str = Field(...)
    4.     completed: bool = False
    5.     class Config:
    6.         allow_population_by_field_name = True
    7.         schema_extra = {
    8.             "example": {
    9.                 "id""00010203-0405-0607-0809-0a0b0c0d0e0f",
    10.                 "name""My important task",
    11.                 "completed"True,
    12.             }
    13.         }

    我想提请注意id这个模型的领域。MongoDB 使用_id,但在 Python 中,属性开头的下划线具有特殊含义。如果您的模型上有以下划线开头的属性,pydantic— FastAPI 使用的数据验证框架— 将假定它是一个私有变量,这意味着您将无法为其分配值!为了解决这个问题,我们为该字段命名,但id给它一个alias. _id您还需要在模型的类中设置allow_population_by_field_name为。True Config 你可能会注意到我没有使用 MongoDB 的 对象 ID . 你可以 将 ObjectIds 与 FastAPI 一起使用 ; 在序列化和反序列化期间需要做更多的工作。尽管如此,对于这个例子,我发现自己生成 UUID 更容易,所以它们总是字符串。

    1. class UpdateTaskModel(BaseModel):
    2.     name: Optional[str]
    3.     completed: Optional[bool]
    4.     class Config:
    5.         schema_extra = {
    6.             "example": {
    7.                 "name""My important task",
    8.                 "completed"True,
    9.             }
    10.         }

    当用户更新任务时,我们不希望他们改变 id,所以UpdateTaskModel只包括名称和完成的字段。我还将这两个字段设为可选,以便您可以独立更新它们中的任何一个。将它们都设为可选确实意味着所有字段都是可选的,这导致我花了太长时间来决定如何处理PUT用户未发送任何要更改的字段的请求(更新)。我们将在接下来查看路由器时看到这一点。

    FastAPI 路由器

    任务路由器位于backend/apps/todo/routers.py. 为了涵盖不同的 CRUD(创建、读取、更新和删除)操作,我需要以下端点:

    • POST /task/ - 创建一个新任务。

    • GET /task/ - 查看所有现有任务。

    • GET /task/{id}/ - 查看单个任务。

    • PUT /task/{id}/ - 更新一个任务。

    • DELETE /task/{id}/ - 删除一个任务。

    CREATE

    1. @router.post("/", response_description="Add new task")
    2. async def create_task(request: Request, task: TaskModel = Body(...)):
    3.     task = jsonable_encoder(task)
    4.     new_task = await request.app.mongodb["tasks"].insert_one(task)
    5.     created_task = await request.app.mongodb["tasks"].find_one(
    6.         {"_id": new_task.inserted_id}
    7.     )
    8.     return JSONResponse(status_code=status.HTTP_201_CREATED, content=created_task)

    create_task 路由器接受请求正文中的新任务数据作为 JSON 字符串。我们将此数据写入 MongoDB,然后以 HTTP 201 状态和新创建的任务进行响应。

    READ

    1. @router.get("/", response_description="List all tasks")
    2. async def list_tasks(request: Request):
    3.     tasks = []
    4.     for doc in await request.app.mongodb["tasks"].find().to_list(length=100):
    5.         tasks.append(doc)
    6.     return tasks

    list_tasks 路由器过于简单。在实际应用程序中,您至少需要包含分页。值得庆幸的是,有 FastAPI 的包可以简化这个过程

    1. @router.get("/{id}", response_description="Get a single task")
    2. async def show_task(idstr, request: Request):
    3.     if (task := await request.app.mongodb["tasks"].find_one({"_id"id})) is not None:
    4.         return task
    5.     raise HTTPException(status_code=404, detail=f"Task {id} not found")

    虽然 FastAPI 支持 Python 3.6+,但我在像这样的路由器中使用了赋值表达式,这就是为什么这个示例应用程序需要 Python 3.8+。 在这里,如果我们找不到具有正确 ID 的任务,我将提出异常。

    UPDATE

    1. @router.put("/{id}", response_description="Update a task")
    2. async def update_task(idstr, request: Request, task: UpdateTaskModel = Body(...)):
    3.     task = {k: v for k, v in task.dict().items() if v is not None}
    4.     if len(task) >= 1:
    5.         update_result = await request.app.mongodb["tasks"].update_one(
    6.             {"_id"id}, {"$set": task}
    7.         )
    8.         if update_result.modified_count == 1:
    9.             if (
    10.                 updated_task := await request.app.mongodb["tasks"].find_one({"_id"id})
    11.             ) is not None:
    12.                 return updated_task
    13.     if (
    14.         existing_task := await request.app.mongodb["tasks"].find_one({"_id"id})
    15.     ) is not None:
    16.         return existing_task
    17.     raise HTTPException(status_code=404, detail=f"Task {id} not found")

    我们不想将任何字段更新为空值,因此首先,我们从更新文档中删除这些字段。如上所述,由于所有值都是可选的,因此带有空负载的更新请求仍然有效。经过深思熟虑,我决定在这种情况下,API 要做的正确事情是返回未修改的任务和 HTTP 200 状态。 如果用户提供了一个或多个要更新的字段,我们会在返回修改后的文档之前尝试$set使用新值update_one。但是,如果我们找不到具有指定 id 的文档,我们的路由器将引发 404。

    DELETE

    1. @router.delete("/{id}", response_description="Delete Task")
    2. async def delete_task(id: str, request: Request):
    3.     delete_result = await request.app.mongodb["tasks"].delete_one({"_id": id})
    4.     if delete_result.deleted_count == 1:
    5.         return JSONResponse(status_code=status.HTTP_204_NO_CONTENT)
    6.     raise HTTPException(status_code=404detail=f"Task {id} not found")

    最终路由器不会在成功时返回响应正文,因为请求的文档不再存在,因为我们刚刚删除了它。相反,它返回 204 的 HTTP 状态,这意味着请求已成功完成,但服务器没有任何数据可提供给您。

    React 前端

    React 前端没有改变,因为它只使用 API,因此在某种程度上与后端无关。它主要是由create-react-app. 因此,要启动我们的 React 前端,请打开一个新的终端窗口——让您的 FastAPI 服务器在现有终端中运行——然后在前端目录中输入以下命令。

    1. npm install
    2. npm start

    这些命令可能需要一点时间才能完成,但之后,它应该会打开一个新的浏览器窗口http://localhost:3000.

    React 前端只是我们任务列表的一个视图,但您可以通过 FastAPI 文档更新您的任务,并查看 React 中出现的更改!

    我们的大部分前端代码都在frontend/src/App.js

    1. useEffect(() => {
    2.     const fetchAllTasks = async () => {
    3.         const response = await fetch("/task/")
    4.         const fetchedTasks = await response.json()
    5.         setTasks(fetchedTasks)
    6.     }
    7.     const interval = setInterval(fetchAllTasks, 1000)
    8.     return () => {
    9.         clearInterval(interval)
    10.     }
    11. }, [])

    当我们的组件挂载时,我们开始一个间隔,它每秒运行一次,并在将它们存储到我们的状态之前获取最新的任务列表。每当组件卸载时,挂钩末尾返回的函数将运行,清理我们的间隔。

    1. useEffect(() => {
    2.     const timelineItems = tasks.reverse().map((task) => {
    3.         return task.completed ? (
    4.             <Timeline.Item
    5.                 dot={<CheckCircleOutlined />}
    6.                 color="green"
    7.                 style={{ textDecoration: "line-through", color: "green" }}
    8.             >
    9.                 {task.name} <small>({task._id})</small>
    10.             </Timeline.Item>
    11.         ) : (
    12.             <Timeline.Item
    13.                 dot={<MinusCircleOutlined />}
    14.                 color="blue"
    15.                 style={{ textDecoration: "initial" }}
    16.             >
    17.                 {task.name} <small>({task._id})</small>
    18.             </Timeline.Item>
    19.         )
    20.     })
    21.     setTimeline(timelineItems)
    22. }, [tasks])
    23.     const timelineItems = tasks.reverse().map((task) => {
    24.         return task.completed ? (
    25.             <Timeline.Item
    26.                 dot={<CheckCircleOutlined />}
    27.                 color="green"
    28.                 style={{ textDecoration: "line-through", color: "green" }}
    29.             >
    30.                 {task.name} <small>({task._id})</small>
    31.             </Timeline.Item>
    32.         ) : (
    33.             <Timeline.Item
    34.                 dot={<MinusCircleOutlined />}
    35.                 color="blue"
    36.                 style={{ textDecoration: "initial" }}
    37.             >
    38.                 {task.name} <small>({task._id})</small>
    39.             </Timeline.Item>
    40.         )
    41.     })
    42.     setTimeline(timelineItems)
    43. }, [tasks])
    44. ```useEffec
    45.     const timelineItems = tasks.reverse().map((task) => {
    46.         return task.completed ? (
    47.             <Timeline.Item
    48.                 dot={<CheckCircleOutlined />}
    49.                 color="green"
    50.                 style={{ textDecoration: "line-through", color: "green" }}
    51.             >
    52.                 {task.name} <small>({task._id})</small>
    53.             </Timeline.Item>
    54.         ) : (
    55.             <Timeline.Item
    56.                 dot={<MinusCircleOutlined />}
    57.                 color="blue"
    58.                 style={{ textDecoration: "initial" }}
    59.             >
    60.                 {task.name} <small>({task._id})</small>
    61.             </Timeline.Item>
    62.         )
    63.     })
    64.     setTimeline(timelineItems)
    65. }, [tasks])useEffect(() => {
    66.     const timelineItems = tasks.reverse().map((task) => {
    67.         return task.completed ? (
    68.             <Timeline.Item
    69.                 dot={<CheckCircleOutlined />}
    70.                 color="green"
    71.                 style={{ textDecoration: "line-through", color: "green" }}
    72.             >
    73.                 {task.name} <small>({task._id})</small>
    74.             </Timeline.Item>
    75.         ) : (
    76.             <Timeline.Item
    77.                 dot={<MinusCircleOutlined />}
    78.                 color="blue"
    79.                 style={{ textDecoration: "initial" }}
    80.             >
    81.                 {task.name} <small>({task._id})</small>
    82.             </Timeline.Item>
    83.         )
    84.     })
    85.     setTimeline(timelineItems)
    86. }, [tasks])

    每当我们状态中的任务列表发生变化时,都会触发第二个钩子。这个钩子为我们列表中的每个任务创建一个Timeline Item组件。

    1. <>
    2.     <Row style={{ marginTop: 50 }}>
    3.         <Col span={14} offset={5}>
    4.             <Timeline mode="alternate">{timeline}</Timeline>
    5.         </Col>
    6.     </Row>
    7. </>

    最后一部分App.js是将任务呈现到页面的标记。如果您以前使用过 MERN 或其他 React 堆栈,这可能看起来非常熟悉。

    总结

    希望大家能够使用本文介绍的技术构建高性能、异步的 Web 应用程序!

    推荐书单

    《Pandas1.x实例精解》

    本书详细阐述了与Pandas相关的基本解决方案,主要包括Pandas基础,DataFrame基本操作,创建和保留DataFrame,开始数据分析,探索性数据分析,选择数据子集,过滤行,对齐索引,分组以进行聚合、过滤和转换,将数据重组为规整形式,组合Pandas对象,时间序列分析,使用Matplotlib、Pandas和Seaborn进行可视化,调试和测试等内容。此外,本书还提供了相应的示例、代码,以帮助读者进一步理解相关方案的实现过程。 本书适合作为高等院校计算机及相关专业的教材和教学参考书,也可作为相关开发人员的自学用书和参考手册。

    链接:https://u.jd.com/UKjx4et

    精彩回顾

    《Pandas1.x实例精解》新书抢先看!

    【第1篇】利用Pandas操作DataFrame的列与行

    【第2篇】Pandas如何对DataFrame排序和统计

    【第3篇】Pandas如何使用DataFrame方法链

    【第4篇】Pandas如何比较缺失值以及转置方向?

    【第5篇】DataFrame如何玩转多样性数据

    【第6篇】如何进行探索性数据分析?

    【第7篇】使用Pandas处理分类数据

    【第8篇】使用Pandas处理连续数据

    【第9篇】使用Pandas比较连续值和连续列

    【第10篇】如何比较分类值以及使用Pandas分析库

    长按关注《Python学研大本营》

    长按二维码,加入Python读者群

    扫码关注《Python学研大本营》,加入读者群,分享更多精彩

  • 相关阅读:
    数据结构 —— 二叉树(超详细图解 & 接口函数实现)
    Java实现扫雷小游戏【优化版】
    【云备份|| 日志 day1】项目认识 && 环境准备
    Linux-性能分析常用工具
    基于FPGA的自动白平衡算法实现
    《C++设计模式》——创建型
    使用工业DTU打造scada燃气系统,以极短的时间完成智慧升级
    力扣(LeetCode)1710. 卡车上的最大单元数(C++)
    C++位图和布隆过滤器
    爬虫基本库的使用(urllib库的详细解析)
  • 原文地址:https://blog.csdn.net/weixin_39915649/article/details/127944960