FARM堆栈是一种现代的Web开发技术组合,它将三种强大的技术相结合:FastAPI、React和MongoDB。这个全栈解决方案为开发者提供了一套强大的工具,用于构建可扩展、高效和高性能的Web应用程序。
在本文中,我将向您介绍每个关键技术,然后我们将使用FARM堆栈和Docker构建一个项目,以便您可以了解所有内容是如何协同工作的。
这篇文章是基于我在freeCodeCamp.org YouTube频道上创建的课程。您可以在这里观看:
FARM堆栈介绍
FARM堆栈中的FARM代表:
-
F: FastAPI(后端)
-
R: React(前端)
-
M: MongoDB(数据库)
FARM堆栈旨在利用每个组件的优势,使开发者能够创建具有流畅开发体验的丰富功能应用程序。
FARM堆栈组件
-
FastAPI: FastAPI是一个现代、高性能的Python Web框架,用于构建API。它旨在易于使用,编码快速,并适用于生产环境。FastAPI建立在Starlette的Web部分和Pydantic的数据部分之上,使其成为构建健壮后端服务的强大选择。
-
React:React是一个流行的JavaScript库,用于构建用户界面。由Facebook开发和维护,React允许开发者创建可重用的UI组件,这些组件可以有效地更新和渲染数据变化。其基于组件的架构和虚拟DOM使其成为构建动态和响应式前端应用的绝佳选择。
-
MongoDB: MongoDB是一个面向文档的NoSQL数据库。它以灵活的、类似JSON的文档形式存储数据,意味着每个文档的字段可以不同,并且数据结构可以随时间变化。这种灵活性使得MongoDB成为需要快速发展和处理多种数据类型的应用的理想选择。
使用FARM栈的优势
-
高性能:FastAPI是可用最快的Python框架之一,而React的虚拟DOM确保了高效的UI更新。MongoDB的文档模型允许快速读写。
-
可扩展性:FARM栈的所有组件都旨在扩展。FastAPI可以高效地处理并发请求,React应用程序可以管理复杂的UI,而MongoDB可以在多个服务器上分布数据。
-
社区和生态系统:这三种技术都有庞大的活跃社区和丰富的库和工具的生态系统。
-
灵活性:FARM 栈足够灵活,可以适应各种类型的网络应用程序,从简单的 CRUD 应用程序到复杂的、数据密集型的系统。
通过结合这些技术,FARM 栈为构建现代网络应用程序提供了全面的解决方案。它允许开发者使用 FastAPI 创建快速、可扩展的后端,使用 React 创建直观、响应式的用户界面,使用 MongoDB 创建灵活、高效的数据存储。这个栈特别适合需要实时更新、复杂数据模型和高性能的应用程序。
项目概述:待办事项应用程序
在视频课程中,我详细介绍了 FARM 栈中的每种技术。但在本文中,我们将直接进入一个项目,将所有内容整合在一起。
我们将创建一个待办事项应用程序来帮助我们了解 FARM 栈。在开始创建应用程序之前,让我们更多地讨论一下功能和软件架构。
待办事项应用程序的功能
我们的 FARM 栈待办事项应用程序将包括以下功能:
-
多个待办事项列表:
-
用户可以创建、查看、更新和删除多个待办事项列表。
-
每个列表都有一个名称,并包含多个待办事项项目。
-
-
待办项目:
-
在每 个列表中,用户可以添加、查看、更新和删除待办项目。
-
每个项目都有一个标签,一个已完成/未完成的状 态,并属于特定的列表。
-
-
实时更新:
- 当列表或项目发生变化时,UI会实时更新。
-
响应式设计:
- 该应用将具有响应性,在桌面和移动设备上都能良好工作。
系统架构
我们的待办事项应用将遵循典型的FARM堆栈架构:
-
前端(React):
-
提供与待办事项列表和项目交互的用户界面。
-
通过RESTful API调用与后端通信。
-
-
后端(FastAPI):
-
处理来自前端的应用程序请求。
-
实现管理待办事项列表和项目的业务逻辑。
-
与MongoDB数据库交互以实现数据持久化。
-
-
数据库(MongoDB):
-
存储待办事项列表和项目。
-
提供高效查询和更新待办事项数据。
-
-
Docker:
- 将每个组件(前端、后端、数据库)容器化,以便于开发和部署。
数据模型设计
我们的MongoDB数据模型将包含两个主要结构:
- 待办事项列表:
{
"_id": ObjectId,
"name": String,
"items": [
{
"id": String,
"label": String,
"checked": Boolean
}
]
}
- 列表摘要(显示在所有待办事项列表中):
{
"_id": ObjectId,
"name": String,
"item_count": Integer
}
API端点设计
我们的FastAPI后端将暴露以下RESTful端点:
-
待办事项列表:
-
GET /api/lists: 检索所有待办事项列表(摘要视图)
-
POST /api/lists: 创建一个新的待办事项列表
-
GET /api/lists/{list_id}: 检索具有所有项目的特定待办事项列表
-
DELETE /api/lists/{list_id}: 删除特定的待办事项列表
-
-
待办项目:
-
POST /api/lists/{list_id}/items:向特定列表添加新项目
-
PATCH /api/lists/{list_id}/checked_state:更新项目的选中状态
-
DELETE /api/lists/{list_id}/items/{item_id}:从列表中删除特定项目
-
这个项目将为您提供一个坚实的FARM堆栈开发和Docker容器化的基础,您可以在未来在此基础上扩展更复杂的应用程序。
那么让我们开始这个项目吧。
项目教程
项目设置和后端开发
步骤1:设置项目结构
为您的项目创建一个新的目录:
mkdir farm-stack-todo
cd farm-stack-todo
为后端和前端创建子目录:
mkdir backend frontend
步骤2:设置后端环境
导航到后端目录:
cd backend
创建一个虚拟环境并激活它:
python -m venv venv
source venv/bin/activate # On Windows, use: venv\Scripts\activate
在后台目录中创建以下文件:
-
-
Dockerfile
- pyproject.toml
-
在你的终端中安装所需的包:
pip install "fastapi[all]" "motor[srv]" beanie aiostream
生成requirements.txt文件:
pip freeze > requirements.txt
创建requirements.txt文件(通过pip-compile或手动创建)后,可以使用以下命令安装依赖项:
pip install -r requirements.txt
将以下内容添加到Dockerfile中:
FROM python:3
WORKDIR /usr/src/app
COPY requirements.txt ./
RUN pip install --no-cache-dir --upgrade -r ./requirements.txt
EXPOSE 3001
CMD [ "python", "./src/server.py" ]
将以下内容添加到pyproject.toml中:
[tool.pytest.ini_options]
pythonpath = "src"
第4步:设置后端结构
在后台目录中创建一个src目录:
mkdir src
在src目录中创建以下文件:
第5步:实现数据访问层(DAL)
打开 src/dal.py 并添加以下内容:
from bson import ObjectId
from motor.motor_asyncio import AsyncIOMotorCollection
from pymongo import ReturnDocument
from pydantic import BaseModel
from uuid import uuid4
class ListSummary(BaseModel):
id: str
name: str
item_count: int
@staticmethod
def from_doc(doc) -> "ListSummary":
return ListSummary(
id=str(doc["_id"]),
name=doc["name"],
item_count=doc["item_count"],
)
class ToDoListItem(BaseModel):
id: str
label: str
checked: bool
@staticmethod
def from_doc(item) -> "ToDoListItem":
return ToDoListItem(
id=item["id"],
label=item["label"],
checked=item["checked"],
)
class ToDoList(BaseModel):
id: str
name: str
items: list[ToDoListItem]
@staticmethod
def from_doc(doc) -> "ToDoList":
return ToDoList(
id=str(doc["_id"]),
name=doc["name"],
items=[ToDoListItem.from_doc(item) for item in doc["items"]],
)
class ToDoDAL:
def __init__(self, todo_collection: AsyncIOMotorCollection):
self._todo_collection = todo_collection
async def list_todo_lists(self, session=None):
async for doc in self._todo_collection.find(
{},
projection={
"name": 1,
"item_count": {"$size": "$items"},
},
sort={"name": 1},
session=session,
):
yield ListSummary.from_doc(doc)
async def create_todo_list(self, name: str, session=None) -> str:
response = await self._todo_collection.insert_one(
{"name": name, "items": []},
session=session,
)
return str(response.inserted_id)
async def get_todo_list(self, id: str | ObjectId, session=None) -> ToDoList:
doc = await self._todo_collection.find_one(
{"_id": ObjectId(id)},
session=session,
)
return ToDoList.from_doc(doc)
async def delete_todo_list(self, id: str | ObjectId, session=None) -> bool:
response = await self._todo_collection.delete_one(
{"_id": ObjectId(id)},
session=session,
)
return response.deleted_count == 1
async def create_item(
self,
id: str | ObjectId,
label: str,
session=None,
) -> ToDoList | None:
result = await self._todo_collection.find_one_and_update(
{"_id": ObjectId(id)},
{
"$push": {
"items": {
"id": uuid4().hex,
"label": label,
"checked": False,
}
}
},
session=session,
return_document=ReturnDocument.AFTER,
)
if result:
return ToDoList.from_doc(result)
async def set_checked_state(
self,
doc_id: str | ObjectId,
item_id: str,
checked_state: bool,
session=None,
) -> ToDoList | None:
result = await self._todo_collection.find_one_and_update(
{"_id": ObjectId(doc_id), "items.id": item_id},
{"$set": {"items.$.checked": checked_state}},
session=session,
return_document=ReturnDocument.AFTER,
)
if result:
return ToDoList.from_doc(result)
async def delete_item(
self,
doc_id: str | ObjectId,
item_id: str,
session=None,
) -> ToDoList | None:
result = await self._todo_collection.find_one_and_update(
{"_id": ObjectId(doc_id)},
{"$pull": {"items": {"id": item_id}}},
session=session,
return_document=ReturnDocument.AFTER,
)
if result:
return ToDoList.from_doc(result)
这样,我们完成了教程的第1部分,在这里我们设置了项目结构并为我们的FARM堆栈待办事项应用程序实现了数据访问层。在下一部分中,我们将实现FastAPI服务器并创建API端点。
实现FastAPI服务器
第6步:实现FastAPI服务器
打开 src/server.py 并添加以下内容:
from contextlib import asynccontextmanager
from datetime import datetime
import os
import sys
from bson import ObjectId
from fastapi import FastAPI, status
from motor.motor_asyncio import AsyncIOMotorClient
from pydantic import BaseModel
import uvicorn
from dal import ToDoDAL, ListSummary, ToDoList
COLLECTION_NAME = "todo_lists"
MONGODB_URI = os.environ["MONGODB_URI"]
DEBUG = os.environ.get("DEBUG", "").strip().lower() in {"1", "true", "on", "yes"}
@asynccontextmanager
async def lifespan(app: FastAPI):
# 启动:
client = AsyncIOMotorClient(MONGODB_URI)
database = client.get_default_database()
# 确保数据库可用:
pong = await database.command("ping")
if int(pong["ok"]) != 1:
raise Exception("Cluster connection is not okay!")
todo_lists = database.get_collection(COLLECTION_NAME)
app.todo_dal = ToDoDAL(todo_lists)
# 返回FastAPI应用程序:
yield
# 关闭:
client.close()
app = FastAPI(lifespan=lifespan, debug=DEBUG)
@app.get("/api/lists")
async def get_all_lists() -> list[ListSummary]:
return [i async for i in app.todo_dal.list_todo_lists()]
class NewList(BaseModel):
name: str
class NewListResponse(BaseModel):
id: str
name: str
@app.post("/api/lists", status_code=status.HTTP_201_CREATED)
async def create_todo_list(new_list: NewList) -> NewListResponse:
return NewListResponse(
id=await app.todo_dal.create_todo_list(new_list.name),
name=new_list.name,
)
@app.get("/api/lists/{list_id}")
async def get_list(list_id: str) -> ToDoList:
"""Get a single to-do list"""
return await app.todo_dal.get_todo_list(list_id)
@app.delete("/api/lists/{list_id}")
async def delete_list(list_id: str) -> bool:
return await app.todo_dal.delete_todo_list(list_id)
class NewItem(BaseModel):
label: str
class NewItemResponse(BaseModel):
id: str
label: str
@app.post(
"/api/lists/{list_id}/items/",
status_code=status.HTTP_201_CREATED,
)
async def create_item(list_id: str, new_item: NewItem) -> ToDoList:
return await app.todo_dal.create_item(list_id, new_item.label)
@app.delete("/api/lists/{list_id}/items/{item_id}")
async def delete_item(list_id: str, item_id: str) -> ToDoList:
return await app.todo_dal.delete_item(list_id, item_id)
class ToDoItemUpdate(BaseModel):
item_id: str
checked_state: bool
@app.patch("/api/lists/{list_id}/checked_state")
async def set_checked_state(list_id: str, update: ToDoItemUpdate) -> ToDoList:
return await app.todo_dal.set_checked_state(
list_id, update.item_id, update.checked_state
)
class DummyResponse(BaseModel):
id: str
when: datetime
@app.get("/api/dummy")
async def get_dummy() -> DummyResponse:
return DummyResponse(
id=str(ObjectId()),
when=datetime.now(),
)
def main(argv=sys.argv[1:]):
try:
uvicorn.run("server:app", host="0.0.0.0", port=3001, reload=DEBUG)
except KeyboardInterrupt:
pass
if __name__ == "__main__":
main()
这个实现设置了带有CORS中间件的FastAPI服务器,连接到MongoDB,并为我们的待办事项应用程序定义了API端点。
第7步:设置环境变量
在根目录中创建一个名为.env的文件,内容如下。请确保在”.mongodb.net/”的最后添加数据库名称(“todo”)。
MONGODB_URI='mongodb+srv://beau:codecamp@cluster0.ji7hu.mongodb.net/todo?retryWrites=true&w=majority&appName=Cluster0'
第8步:创建一个docker-compose文件
在项目的根目录(farm-stack-todo)中创建一个名为compose.yml的文件,内容如下:
name: todo-app
services:
nginx:
image: nginx:1.17
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
ports:
- 8000:80
depends_on:
- backend
- frontend
frontend:
image: "node:22"
user: "node"
working_dir: /home/node/app
environment:
- NODE_ENV=development
- WDS_SOCKET_PORT=0
volumes:
- ./frontend/:/home/node/app
expose:
- "3000"
ports:
- "3000:3000"
command: "npm start"
backend:
image: todo-app/backend
build: ./backend
volumes:
- ./backend/:/usr/src/app
expose:
- "3001"
ports:
- "8001:3001"
command: "python src/server.py"
environment:
- DEBUG=true
env_file:
- path: ./.env
required: true
第9步:设置Nginx配置
在项目的根目录下创建一个名为nginx的目录:
mkdir nginx
在 nginx 目录下创建一个名为 nginx.conf 的文件,内容如下:
server {
listen 80;
server_name farm_intro;
location / {
proxy_pass http://frontend:3000;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /api {
proxy_pass http://backend:3001/api;
}
}
本教程第二部分到此结束,在这一部分中,我们实现了 FastAPI 服务器,设置了环境变量,创建了 docker-compose 文件,并配置了 Nginx。在下一部分,我们将重点介绍为我们的 FARM 堆栈待办事项应用程序设置 React 前端。
设置 React 前端
第 10 步:创建 React 应用程序
导航到 frontend 目录:
cd ../frontend
使用 Create React App 创建一个新的 React 应用程序:
npx create-react-app .
安装其他依赖项:
npm install axios react-icons
第 11 步:设置主 App 组件
将 src/App.js 的内容替换为以下内容:
import { useEffect, useState } from "react";
import axios from "axios";
import "./App.css";
import ListToDoLists from "./ListTodoLists";
import ToDoList from "./ToDoList";
function App() {
const [listSummaries, setListSummaries] = useState(null);
const [selectedItem, setSelectedItem] = useState(null);
useEffect(() => {
reloadData().catch(console.error);
}, []);
async function reloadData() {
const response = await axios.get("/api/lists");
const data = await response.data;
setListSummaries(data);
}
function handleNewToDoList(newName) {
const updateData = async () => {
const newListData = {
name: newName,
};
await axios.post(`/api/lists`, newListData);
reloadData().catch(console.error);
};
updateData();
}
function handleDeleteToDoList(id) {
const updateData = async () => {
await axios.delete(`/api/lists/${id}`);
reloadData().catch(console.error);
};
updateData();
}
function handleSelectList(id) {
console.log("Selecting item", id);
setSelectedItem(id);
}
function backToList() {
setSelectedItem(null);
reloadData().catch(console.error);
}
if (selectedItem === null) {
return (
<div className="App">
<ListToDoLists
listSummaries={listSummaries}
handleSelectList={handleSelectList}
handleNewToDoList={handleNewToDoList}
handleDeleteToDoList={handleDeleteToDoList}
/>
</div>
);
} else {
return (
<div className="App">
<ToDoList listId={selectedItem} handleBackButton={backToList} />
</div>
);
}
}
export default App;
第 12 步:创建 ListTodoLists 组件
创建一个新文件 src/ListTodoLists.js,内容如下:
import "./ListTodoLists.css";
import { useRef } from "react";
import { BiSolidTrash } from "react-icons/bi";
function ListToDoLists({
listSummaries,
handleSelectList,
handleNewToDoList,
handleDeleteToDoList,
}) {
const labelRef = useRef();
if (listSummaries === null) {
return <div className="ListToDoLists loading">Loading to-do lists ...</div>;
} else if (listSummaries.length === 0) {
return (
<div className="ListToDoLists">
<div className="box">
<label>
New To-Do List:
<input id={labelRef} type="text" />
</label>
<button
onClick={() =>
handleNewToDoList(document.getElementById(labelRef).value)
}
>
New
</button>
</div>
<p>There are no to-do lists!</p>
</div>
);
}
return (
<div className="ListToDoLists">
<h1>All To-Do Lists</h1>
<div className="box">
<label>
New To-Do List:
<input id={labelRef} type="text" />
</label>
<button
onClick={() =>
handleNewToDoList(document.getElementById(labelRef).value)
}
>
New
</button>
</div>
{listSummaries.map((summary) => {
return (
<div
key={summary.id}
className="summary"
onClick={() => handleSelectList(summary.id)}
>
<span className="name">{summary.name} </span>
<span className="count">({summary.item_count} items)</span>
<span className="flex"></span>
<span
className="trash"
onClick={(evt) => {
evt.stopPropagation();
handleDeleteToDoList(summary.id);
}}
>
<BiSolidTrash />
</span>
</div>
);
})}
</div>
);
}
export default ListToDoLists;
创建一个新文件 src/ListTodoLists.css,内容如下:
.ListToDoLists .summary {
border: 1px solid lightgray;
padding: 1em;
margin: 1em;
cursor: pointer;
display: flex;
}
.ListToDoLists .count {
padding-left: 1ex;
color: blueviolet;
font-size: 92%;
}
第 13 步:创建 ToDoList 组件
创建一个新文件 src/ToDoList.js,内容如下:
import "./ToDoList.css";
import { useEffect, useState, useRef } from "react";
import axios from "axios";
import { BiSolidTrash } from "react-icons/bi";
function ToDoList({ listId, handleBackButton }) {
let labelRef = useRef();
const [listData, setListData] = useState(null);
useEffect(() => {
const fetchData = async () => {
const response = await axios.get(`/api/lists/${listId}`);
const newData = await response.data;
setListData(newData);
};
fetchData();
}, [listId]);
function handleCreateItem(label) {
const updateData = async () => {
const response = await axios.post(`/api/lists/${listData.id}/items/`, {
label: label,
});
setListData(await response.data);
};
updateData();
}
function handleDeleteItem(id) {
const updateData = async () => {
const response = await axios.delete(
`/api/lists/${listData.id}/items/${id}`
);
setListData(await response.data);
};
updateData();
}
function handleCheckToggle(itemId, newState) {
const updateData = async () => {
const response = await axios.patch(
`/api/lists/${listData.id}/checked_state`,
{
item_id: itemId,
checked_state: newState,
}
);
setListData(await response.data);
};
updateData();
}
if (listData === null) {
return (
<div className="ToDoList loading">
<button className="back" onClick={handleBackButton}>
Back
</button>
Loading to-do list ...
</div>
);
}
return (
<div className="ToDoList">
<button className="back" onClick={handleBackButton}>
Back
</button>
<h1>List: {listData.name}</h1>
<div className="box">
<label>
New Item:
<input id={labelRef} type="text" />
</label>
<button
onClick={() =>
handleCreateItem(document.getElementById(labelRef).value)
}
>
New
</button>
</div>
{listData.items.length > 0 ? (
listData.items.map((item) => {
return (
<div
key={item.id}
className={item.checked ? "item checked" : "item"}
onClick={() => handleCheckToggle(item.id, !item.checked)}
>
<span>{item.checked ? "✅" : "⬜️"} </span>
<span className="label">{item.label} </span>
<span className="flex"></span>
<span
className="trash"
onClick={(evt) => {
evt.stopPropagation();
handleDeleteItem(item.id);
}}
>
<BiSolidTrash />
</span>
</div>
);
})
) : (
<div className="box">There are currently no items.</div>
)}
</div>
);
}
export default ToDoList;
创建一个新文件 src/ToDoList.css,内容如下:
.ToDoList .back {
margin: 0 1em;
padding: 1em;
float: left;
}
.ToDoList .item {
border: 1px solid lightgray;
padding: 1em;
margin: 1em;
cursor: pointer;
display: flex;
}
.ToDoList .label {
margin-left: 1ex;
}
.ToDoList .checked .label {
text-decoration: line-through;
color: lightgray;
}
第 14 步:更新主 CSS 文件
将 src/index.css 的内容替换为以下内容:
html, body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-size: 12pt;
}
input, button {
font-size: 1em;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
.box {
border: 1px solid lightgray;
padding: 1em;
margin: 1em;
}
.flex {
flex: 1;
}
本教程第三部分到此结束,我们在其中为我们的 FARM 堆栈待办事项应用程序设置了 React 前端。我们创建了主 App 组件、用于显示所有待办事项列表的 ListTodoLists 组件以及用于单个待办事项列表的 ToDoList 组件。在下一部分,我们将重点介绍运行和测试应用程序。
运行和测试应用程序
第18步:使用Docker Compose运行应用程序
-
确保您已经在系统上安装了Docker和Docker Compose
-
在您项目的根目录(farm-stack-todo)中打开一个终端
-
构建并启动容器:
docker-compose up --build
- 容器启动并运行后,打开您的网页浏览器,访问http://localhost:8000
第19步:停止应用程序
-
如果您不使用Docker运行应用程序:
-
在React开发服务器的终端中按Ctrl+C停止它
-
在FastAPI服务器的终端中按Ctrl+C停止它
-
在MongoDB服务器的终端中按Ctrl+C停止它
-
-
如果你使用Docker Compose运行应用程序:
-
在你运行docker-compose up的终端按Ctrl+C
-
运行以下命令来停止并删除容器:
-
docker-compose down
“`
恭喜你!你已经成功构建并测试了一个FARM堆栈待办事项应用程序。这个应用程序展示了FastAPI、React和MongoDB在完整栈web应用程序中的集成。
以下是一些可以增强你应用程序的潜在下一步:
-
添加用户认证和授权
-
实现数据验证和错误处理
-
为待办事项添加更多功能,如到期日期、优先级或类别
-
用更精致的设计改善UI/UX
-
编写前端和后端的单元测试和集成测试
-
为应用程序设置持续集成和部署(CI/CD)
记得保持依赖项更新,并遵循安全性和性能的最佳实践,以便在开发应用程序的过程中持续发展。
结论和下一步骤
恭喜您完成了这个全面的FARM堆栈教程!通过构建这个待办事项应用程序,您已经获得了对现代Web开发中一些最强大和流行的技术的实践经验。您学会了如何使用FastAPI创建一个强大的后端API,使用React构建一个动态和响应式的前端,使用MongoDB持久化数据,并使用Docker将整个应用程序容器化。该项目展示了这些技术如何无缝地协同工作,创建一个功能齐全、可扩展的Web应用程序。
Source:
https://www.freecodecamp.org/news/use-the-farm-stack-to-develop-full-stack-apps/