“农场”技术栈是什么?浅聊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 用户名、密码和连接字符串,稍后您将需要它们。

安装依赖

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

配置环境变量

export DEBUG_MODE=True
export DB_URL="mongodb+srv://<username>:<password>@<url>/<db>?retryWrites=true&w=majority"
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 服务器。

if __name__ == "__main__":
    uvicorn.run(
        "main:app",
        host=settings.HOST,
        reload=settings.DEBUG_MODE,
        port=settings.PORT,
    )

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

@app.on_event("startup")
async def startup_db_client():
    app.mongodb_client = AsyncIOMotorClient(settings.DB_URL)
    app.mongodb = app.mongodb_client[settings.DB_NAME]


@app.on_event("shutdown")
async def shutdown_db_client():
    app.mongodb_client.close()

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

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

定义模型

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

class TaskModel(BaseModel):
    id: str = Field(default_factory=uuid.uuid4, alias="_id")
    name: str = Field(...)
    completed: bool = False

    class Config:
        allow_population_by_field_name = True
        schema_extra = {
            "example": {
                "id": "00010203-0405-0607-0809-0a0b0c0d0e0f",
                "name": "My important task",
                "completed": True,
            }
        }

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

class UpdateTaskModel(BaseModel):
    name: Optional[str]
    completed: Optional[bool]

    class Config:
        schema_extra = {
            "example": {
                "name": "My important task",
                "completed": True,
            }
        }

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

FastAPI 路由器

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

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

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

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

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

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

CREATE

@router.post("/", response_description="Add new task")
async def create_task(request: Request, task: TaskModel = Body(...)):
    task = jsonable_encoder(task)
    new_task = await request.app.mongodb["tasks"].insert_one(task)
    created_task = await request.app.mongodb["tasks"].find_one(
        {"_id": new_task.inserted_id}
    )

    return JSONResponse(status_code=status.HTTP_201_CREATED, content=created_task)

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

READ

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

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

@router.get("/{id}", response_description="Get a single task")
async def show_task(id: str, request: Request):
    if (task := await request.app.mongodb["tasks"].find_one({"_id": id})) is not None:
        return task

    raise HTTPException(status_code=404, detail=f"Task {id} not found")

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

UPDATE

@router.put("/{id}", response_description="Update a task")
async def update_task(id: str, request: Request, task: UpdateTaskModel = Body(...)):
    task = {k: v for k, v in task.dict().items() if v is not None}

    if len(task) >= 1:
        update_result = await request.app.mongodb["tasks"].update_one(
            {"_id": id}, {"$set": task}
        )

        if update_result.modified_count == 1:
            if (
                updated_task := await request.app.mongodb["tasks"].find_one({"_id": id})
            ) is not None:
                return updated_task

    if (
        existing_task := await request.app.mongodb["tasks"].find_one({"_id": id})
    ) is not None:
        return existing_task

    raise HTTPException(status_code=404, detail=f"Task {id} not found")

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

DELETE

@router.delete("/{id}", response_description="Delete Task")
async def delete_task(id: str, request: Request):
    delete_result = await request.app.mongodb["tasks"].delete_one({"_id": id})

    if delete_result.deleted_count == 1:
        return JSONResponse(status_code=status.HTTP_204_NO_CONTENT)

    raise HTTPException(status_code=404, detail=f"Task {id} not found")

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

React 前端

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

npm install
npm start

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

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

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

useEffect(() => {
    const fetchAllTasks = async () => {
        const response = await fetch("/task/")
        const fetchedTasks = await response.json()
        setTasks(fetchedTasks)
    }

    const interval = setInterval(fetchAllTasks, 1000)

    return () => {
        clearInterval(interval)
    }
}, [])

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

useEffect(() => {
    const timelineItems = tasks.reverse().map((task) => {
        return task.completed ? (
            <Timeline.Item
                dot={<CheckCircleOutlined />}
                color="green"
                style={{ textDecoration: "line-through", color: "green" }}
            >
                {task.name} <small>({task._id})</small>
            </Timeline.Item>
        ) : (
            <Timeline.Item
                dot={<MinusCircleOutlined />}
                color="blue"
                style={{ textDecoration: "initial" }}
            >
                {task.name} <small>({task._id})</small>
            </Timeline.Item>
        )
    })

    setTimeline(timelineItems)
}, [tasks])
    const timelineItems = tasks.reverse().map((task) => {
        return task.completed ? (
            <Timeline.Item
                dot={<CheckCircleOutlined />}
                color="green"
                style={{ textDecoration: "line-through", color: "green" }}
            >
                {task.name} <small>({task._id})</small>
            </Timeline.Item>
        ) : (
            <Timeline.Item
                dot={<MinusCircleOutlined />}
                color="blue"
                style={{ textDecoration: "initial" }}
            >
                {task.name} <small>({task._id})</small>
            </Timeline.Item>
        )
    })

    setTimeline(timelineItems)
}, [tasks])
```useEffec
    const timelineItems = tasks.reverse().map((task) => {
        return task.completed ? (
            <Timeline.Item
                dot={<CheckCircleOutlined />}
                color="green"
                style={{ textDecoration: "line-through", color: "green" }}
            >
                {task.name} <small>({task._id})</small>
            </Timeline.Item>
        ) : (
            <Timeline.Item
                dot={<MinusCircleOutlined />}
                color="blue"
                style={{ textDecoration: "initial" }}
            >
                {task.name} <small>({task._id})</small>
            </Timeline.Item>
        )
    })

    setTimeline(timelineItems)
}, [tasks])useEffect(() => {
    const timelineItems = tasks.reverse().map((task) => {
        return task.completed ? (
            <Timeline.Item
                dot={<CheckCircleOutlined />}
                color="green"
                style={{ textDecoration: "line-through", color: "green" }}
            >
                {task.name} <small>({task._id})</small>
            </Timeline.Item>
        ) : (
            <Timeline.Item
                dot={<MinusCircleOutlined />}
                color="blue"
                style={{ textDecoration: "initial" }}
            >
                {task.name} <small>({task._id})</small>
            </Timeline.Item>
        )
    })

    setTimeline(timelineItems)
}, [tasks])

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

<>
    <Row style={{ marginTop: 50 }}>
        <Col span={14} offset={5}>
            <Timeline mode="alternate">{timeline}</Timeline>
        </Col>
    </Row>
</>

最后一部分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学研大本营》,加入读者群,分享更多精彩

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值