使用 Django、Django REST 和 Next.js 构建全栈应用程序
Django 和 Nextjs 是后端和前端开发最常用的 Web 框架。Django 拥有强大的功能、安全性和灵活性,允许任何开发人员在短时间内构建简单或复杂的应用程序。另一方面,在使用 React 开发响应式前端时,Nextjs 绝对是首选框架。
Django 是一个基于 Python 的框架,以其“内置电池”的设计理念而闻名。它简化了后端开发,让您可以专注于编写应用程序,而无需重新设计轮子。另一方面,Next.js 则提升了基于 React 的前端,提供服务器端渲染等功能,以加快加载速度并提升 SEO。两者强强联手,共同打造全栈开发。
在本文中,我们将学习如何使用 Django 作为后端来构建全栈应用程序以构建 REST API,然后使用 Nextjs 创建一个简单漂亮的前端来使用数据。
该应用程序是一个简单的 CRUD 应用,用于管理餐厅菜单。在前端,用户应该能够:
-
列出所有菜单
-
检索菜单
-
创建菜单
-
更新菜单
-
删除菜单
在本文结束时,您将了解如何连接 Django 应用程序和使用 Nextjs 构建的前端应用程序。
设置
首先,让我们使用必要的工具和技术来设置我们的项目。我们将使用 Python 3.11 和 Django 4.2 作为应用程序的后端。这些最新版本将有助于确保我们的后端平稳安全地运行。
对于前端,我们将使用 Next.js 13 和 Node 19。
在样式方面,我们将使用纯 CSS。
构建 Django API
仅使用 Django,构建健壮的 API 几乎是不可能的。你确实可以从应用程序的视图函数或类返回 JSON 数据,但如何处理权限、身份验证、解析、限制、数据序列化等等?
这就是 Django REST 框架发挥作用的地方。它是一个基于 Django 架构开发的框架,旨在帮助开发人员构建强大而健壮的 REST API。
不用太多犹豫,让我们创建 Django 应用程序。
创建应用程序
确保已安装 Python 3.11。在计算机的终端中,运行以下命令来创建工作目录、虚拟环境,然后创建项目。
mkdir menu-api && cd menu-api
python3.11 -m venv venv
source venv/bin/activate
pip install django djangorestframework
django-admin startproject RestaurantCore .
上面,我们刚刚创建了一个名为 的新 Django 项目RestaurantCore
。你会注意到当前工作目录中有一个新的目录和文件。
包含RestaurantCore
如下文件:
-
settings.py
其中包含 Django 项目的所有配置。我们将在这里添加 Django rest-framework 包和其他包的配置。 -
urls.py
包含项目的所有 URL。 -
wsgi
这对于在开发模式下运行 Django 应用程序以及部署很有用。
为了允许管理员对菜单对象进行 CRUD 操作,我们需要添加一个应用程序,其中包含处理请求、序列化或反序列化数据以及最终将其保存在数据库中所需的所有逻辑。
我们正在重用 MVT 架构(模型 - 视图 - 模板),但我们用序列化器和视图集替换视图和模板层。
那么让我们从添加应用程序开始。
添加菜单应用程序
在当前工作目录中,键入以下命令来创建一个新的 Django 应用程序。
django-admin startapp menu
执行此命令后,确保目录结构与下图相同:
创建 Django 应用程序后,我们需要在Django 项目的文件中注册这个新创建的应用程序。我们还需要注册该INSTALLED_APPS
应用程序,以便 rest-framework 包能够实际运行。settings.py
rest_framework
# RestaurantCore/settings.py
...
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
#third apps
"rest_framework",
# installed apps
"menu"
]
将应用程序添加到INSTALLED_APPS
列表中后,我们现在可以开始编写menu
Django 应用程序逻辑。
让我们从models.py
文件开始。大多数情况下,该文件包含一个模型,它是数据库中表的表示。使用 Django ORM,我们无需编写任何 SQL 语句即可创建表并添加字段。
# menu/models.py
from django.db import models
class Menu(models.Model):
name = models.CharField(max_length=255)
price = models.FloatField()
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
该Menu
模型包含以下字段:
-
name
菜单名称 -
price
价格 -
created
对象的创建日期。auto_now_add
设置为 True 时,数据会在创建时自动添加。 -
最后
updated
是对象更新或修改的日期。auto_now
每次保存对象时,该日期都会更新。
下一步是添加序列化器。这将帮助 Django 将 JSON 数据无缝转换为 Python 原生对象,从而更轻松地处理。
在应用程序中menu
,在 serializers.py 文件中,添加以下内容
from rest_framework import serializers
from menu.models import Menu
class MenuSerializer(serializers.ModelSerializer):
class Meta:
model = Menu
fields = ['name', 'price', 'created', 'updated', 'id']
read_only_fields = ['created', 'updated', 'id']
上面几行代码中,我们使用 类创建了一个序列化器ModelSerializer
。该类ModelSerializer
是为模型添加序列化器的快捷方式,可以无缝处理查询集和字段验证,因此无需添加验证逻辑和错误处理。
在这个序列化器中,我们定义了一个Meta
类,并设置了模型、字段以及只读字段。例如,这些字段不应该被修改。
太棒了!现在我们有了模型序列化器,接下来让我们添加视图集来定义处理请求的接口(控制器)。在menu
应用程序文件夹中创建一个名为 的文件,viewsets
并添加以下内容。
from rest_framework import viewsets
from rest_framework.permissions import AllowAny
from menu.models import Menu
from menu.serializers import MenuSerializer
class MenuViewSet(viewsets.ModelViewSet):
queryset = Menu.objects.all()
serializer_class = MenuSerializer
permission_classes = [AllowAny]
在上面的代码中,我们用该类创建了一个新的视图集类ModelViewSet
。为什么要使用视图集而不是 API 视图呢?因为视图集本身就包含了 CRUD 操作所需的所有逻辑,例如列出、检索、更新、创建和删除。这有助于我们让开发过程更快、更简洁。
对于此端点,我们希望允许任何用户执行这些 CRUD 操作。(我们将在下一篇文章中讨论身份验证和权限问题😁)
我们有可以帮助 CRUD 操作的视图集,我们需要注册视图集并公开 API 端点来管理菜单。
添加菜单端点
在 Django 项目的根目录中,创建一个名为 的文件routers.py
。此文件将包含 API 的所有端点,在本例中/menu/
为 端点。然后,我们将在urls.py
Django 应用程序的文件中注册这些端点。
让我们从编写文件的代码开始routers.py
。
# ./routers.py
from rest_framework import routers
from menu.viewsets import MenuViewSet
router = routers.SimpleRouter()
router.register(r'menu', MenuViewSet, basename="menu")
urlpatterns = router.urls
在上面的代码中,发生了以下事情:
-
routers.SimpleRouter()
用于创建一个简单的默认路由器,该路由器自动为 DRF 生成URLViewSet
。 -
MenuViewSet
frommenu.viewsets
已向路由器注册。 -
basename
中的参数设置router.register
为"menu"
。此基本名称用于构造 的 URL 名称MenuViewSet
。 -
最后,
urlpatterns = router.urls
将应用程序此部分的 urlpatterns 设置为路由器为 生成的 urlpatternsMenuViewSet
。
我们现在可以在Django 项目的文件router
中注册定义的URL。urls.py
# RestaurantCore/urls.py
from django.contrib import admin
from django.urls import path, include
from routers import router
urlpatterns = [
path("admin/", admin.site.urls),
path('api/', include((router.urls, 'core_api'), namespace='core_api')),
]
在上面的代码中,我们url
在 Django 应用程序中注册了 new 。该行path('api/', include((router.urls, 'core_api'), namespace='core_api'))
定义了一个路径,其中包含来自 的所有 URL router
,并以 为前缀'api/'
。它嵌套在 命名空间 中'core_api'
。
定义好 URL 和端点后,我们应该可以开始创建前端并开始从 API 中获取数据了😍。不过,等等,我们还需要配置一些东西,这是 Web 开发人员最大的挑战:CORS。
重要配置:CORS
在前端可以使用 API 之前,我们需要配置 CORS。那么什么是 CORS?跨域资源共享(CORS) 是 Web 浏览器中实现的一项安全功能,用于控制一个域中的网页如何从另一个域请求资源。它是 Web 安全的重要组成部分,因为它通过限制不同域之间资源的共享方式,帮助防止恶意攻击,例如跨站脚本 (XSS) 和数据盗窃。
在我们的例子中,从浏览器向 API URL 发出请求将返回前端错误,这是一个非常丑陋且有时令人沮丧的错误。
让我们通过在创建的 API 上配置 CORS 来解决此错误。首先,让我们安装该django-cors-headers
包。
python -m pip install django-cors-headers
安装完成后,打开settings.py
文件并确保添加以下配置。
INSTALLED_APPS = [
...,
"corsheaders",
...,
]
MIDDLEWARE = [
...,
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
...,
]
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
"http://127.0.0.1:3000",
]
上面的代码CORS_ALLOWED_ORIGINS
帮助我们告诉 Django 应该接受哪个域名来源。由于我们计划在前端使用 Next.js,并且这些应用默认在该3000
端口上运行,因此我们添加了两个地址,以便 Django 允许来自这些地址的请求。
太棒了!我们已经成功构建了一个 Django REST API,可以用于提供数据了。通过该api/menu
端点,我们可以列出菜单、创建菜单;使用详情端点api/menu/<menu_id>
,我们可以更新或删除菜单。
在下一部分中,我们将使用 App 路由器架构构建一个 Next.js 前端,它将使用我们创建的 Django 后端的数据。
使用 Next.js 构建前端
在本文的上一节中,我们使用 Django 构建了全栈应用程序的后端。在本节中,我们将使用 Next.js 构建前端。Next.js 是一个 React 框架,旨在使 React 应用程序的开发和部署比直接使用库更加简单。
我们将仅使用 CSS 构建前端应用程序的 UI。我们将从列表、创建页面和编辑文章页面开始。事不宜迟,让我们先创建 Next.js 项目。
设置 Next.js 项目
Next.js 团队让创建 Next.js 项目变得非常简单。运行以下命令即可创建新项目。
npx create-next-app@latest
您将看到一些用于配置项目的选项。如果您想按照本文的说明配置项目,请遵循以下选项。
What is your project named? next-js-front
Would you like to use TypeScript? No
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? No
Would you like to use `src/` directory? Yes
Would you like to use App Router? (recommended) Yes
Would you like to customize the default import alias (@/*)? No
完成选择项目创建选项后,next-js-front
将创建一个名为 的新目录,其中包含开发、启动和构建 Next.js 项目所需的所有资源。
cd next-js-front && npm run dev
这将在http://localhost:3000上启动项目。
安装项目后,我们现在可以构建应用程序的第一个块。
构建文章列表页面
在本节中,我们将构建一个文章列表页面。在该页面上,用户应该能够查看文章列表,并有一个添加新菜单的按钮,并且对于列表中显示的每个项目(菜单),都应该有编辑和删除的操作。
文章结束时,它应该如下图所示。👇
现在我们已经知道界面应该是什么样子了,让我们开始编码吧。
在 Next.js 项目中,您将找到src/app
Next.js 项目的内容。该app
文件夹应包含page.js
、layout.js
和等文件style.css
。以下是每个文件及其用途的简要说明。
-
page.js
Next.js 13 版引入了 AppRouter 架构模式,这与之前版本中将文件组织在目录中的方式截然不同pages
。新模式通过根据文件和目录的组织方式确定前端页面渲染的结构,增强了路由的清晰度和简洁性。AppRouter 带来了显著的改进。它不仅速度更快,还默认启用了服务器端渲染,方便使用服务器组件。此外,它还扩展了诸如 之类的功能
layout.js
,我将在稍后进行讲解。它还包含用于各种功能的特定文件,例如error.js
用于错误处理和loading.js
默认加载行为的文件。menu/supplements
在此框架中,像在 Next.js 应用程序中的客户端上创建路由一样,需要将page.js
文件放置在相应的menu/supplements
目录中。这种专注于page.js
文件的方式简化了结构,因为目录中的其他文件不参与路由。这种方法使开发人员在组织应用程序架构方面拥有更大的灵活性,允许将特定页面上使用的组件放置在与页面声明相邻的位置。 -
layout.js
在 Next.js 中,尤其是从包含 AppRouter 的 13 版开始,该layout.js
文件对于定义应用程序的整体布局和样式至关重要。它在整个应用程序中建立了一致的框架,涵盖了页眉、页脚和导航栏等元素。它layout.js
支持分层布局,这意味着应用程序的不同部分可以通过拥有各自的layout.js
文件来拥有独特的布局。它还允许动态布局,以适应不同的页面或应用程序状态。该文件是集成全局状态管理和主题提供程序等组件的关键,可确保所有页面的环境一致。此外,
layout.js
它还有利于 SEO 和性能优化,因为它集中管理元数据并减少重新渲染公共元素的需要。 -
style.css
:嗯,它包含项目的 CSS 代码。我们将把它注入到layout.js
文件中。
现在就开始写代码吧。我们将首先添加必要的css
类定义,以便专注于 Next.js 代码。
// src/app/style.css
.menu-container {
max-width: 70%;
height: 90vh;
margin: 0 auto;
padding: 20px;
background: #f9f9f9;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.menu-item {
border: 1px solid #ddd;
padding: 10px;
margin: 10px;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
background-color: #fff;
}
.menu-item-info {
display: flex;
flex-direction: column;
}
.menu-item-name {
font-size: 1.2rem;
font-weight: 600;
}
.menu-item-price {
color: #555;
}
.menu-item-actions {
display: flex;
gap: 10px;
}
button {
padding: 5px 10px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.edit-button {
background-color: #ffca28;
color: #333;
}
.add-button {
background-color: #008000;
color: #fff;
padding: 10px;
margin: 10px;
}
.delete-button {
background-color: #f44336;
color: #fff;
}
form {
width: 60%;
}
.form-item {
padding: 10px;
display: flex;
flex-direction: column;
}
input {
height: 22px;
border-radius: 4px;
border: solid black 0.5px;
}
.success-message {
color: #008000;
}
.error-message {
color: #f44336;
}
然后在中src/app/layout.js
,让我们导入CSS代码,同时将容器修改className
为menu-container
。
// src/app/layout.js
import { Inter } from "next/font/google";
import "./style.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "Restaurant Menu",
description: "A simple UI to handle menus",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>
<main className="menu-container">{children}</main>
</body>
</html>
);
}
在上面的代码中,我们导入了字体和 CSS 文件,主要是资源。我们用标题和描述声明了元数据对象。然后我们RootLayout
使用 定义组件menu-containerclassName
,并将字体导入到body
标签中。
可以肯定地说,我们layout.js
现在已经完成了,可以开始编写page.js
代码了。它将包含列出菜单的代码。因此,导航/
应该会将您带到所有菜单的列表。
构建列表页面
在前面的部分中,我们已经确保了 CSS 所需的代码,并定义了layout.js
相应的文件。现在,我们可以构建列表页面的界面了。
在中src/app/page.js
,确保文件中有以下导入以开始。
// src/app/page.js
"use client";
import { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
你可能注意到了这个use client
指令。它告诉 Next.js 在客户端渲染此页面,因此我们正在构建的页面应该包含客户端代码。
如果您希望此页面上有服务器端代码,请使用指令use server
。
除此之外,我们还导入了useState
和useEffect
钩子,分别用于管理应用程序中的状态和效果。useRouter
和useSearchParams
也很有用,因为我们有按钮可以重定向到编辑页面和添加页面。
接下来,我们将声明两个函数,一个用于从 API 中检索菜单列表,另一个用于删除菜单。
// src/app/page.js
...
/**
* Fetches a menu item by ID.
* @param {number} id The ID of the menu item to retrieve.
*/
async function deleteMenu(id) {
const res = await fetch(`http://127.0.0.1:8000/api/menu/${id}/`, {
method: "DELETE",
});
if (!res.ok) {
throw new Error("Failed to retrieve menu");
}
return Promise.resolve();
}
/**
* Fetches menu data from the server.
*/
async function getData() {
const res = await fetch("http://127.0.0.1:8000/api/menu/");
if (!res.ok) {
throw new Error("Failed to fetch data");
}
return res.json();
}
在上面的代码中,我们声明了两个函数:
-
deleteMenu
:它接受一个参数,即id
文章的地址,然后发送删除请求。我们使用fetch
API 发送请求。 -
getData
:请求从 API 检索所有菜单。我们返回响应的 json 格式。
我们已经有了在文章列表中使用的方法。现在我们需要编写用于在菜单列表中显示菜单信息的 item 组件。
在同一个page.js
文件中,添加以下MenuItem
组件。
...
/**
* Represents a single menu item.
*/
const MenuItem = ({ id, name, price, onEdit, onDelete }) => {
return (
<div className="menu-item" data-id={id}>
<div className="menu-item-info">
<div className="menu-item-name">{name}</div>
<div className="menu-item-price">${price.toFixed(2)}</div>
</div>
<div className="menu-item-actions">
<button className="edit-button" onClick={onEdit}>
Edit
</button>
<button
className="delete-button"
onClick={() => {
deleteMenu(id).then(() => onDelete(id));
}}
>
Delete
</button>
</div>
</div>
);
};
在上面的代码中,我们定义了一个MenuItem
组件,它接收一些 props,例如id
、menu
、name
,以及price
点击onEdit
“编辑”按钮时的行为方法,最后还有onDelete
创建“删除”按钮时触发的方法。
现在我们可以继续编写页面代码并使用MenuItem
组件了。
...
/**
* The main page component.
*/
export default function Page() {
const [menuItems, setMenuItems] = useState(null);
const router = useRouter();
const params = useSearchParams();
// State for displaying a success message
const [displaySuccessMessage, setDisplaySuccessMessage] = useState({
show: false,
type: "", // either 'add' or 'update'
});
// Fetch menu items on component mount
useEffect(() => {
const fetchData = async () => {
const data = await getData();
setMenuItems(data);
};
fetchData().catch(console.error);
}, []);
// Detect changes in URL parameters for success messages
useEffect(() => {
if (!!params.get("action")) {
setDisplaySuccessMessage({
type: params.get("action"),
show: true,
});
router.replace("/");
}
}, [params, router]);
// Automatically hide the success message after 3 seconds
useEffect(() => {
const timer = setTimeout(() => {
if (displaySuccessMessage.show) {
setDisplaySuccessMessage({ show: false, type: "" });
}
}, 3000);
return () => clearTimeout(timer);
}, [displaySuccessMessage.show]);
// Handle deletion of a menu item
const handleDelete = (id) => {
setMenuItems((items) => items.filter((item) => item.id !== id));
};
return (
<div>
<button className="add-button" onClick={() => router.push("/add")}>
Add
</button>
{displaySuccessMessage.show && (
<p className="success-message">
{displaySuccessMessage.type === "add" ? "Added a" : "Modified a"} menu
item.
</p>
)}
{menuItems ? (
menuItems.map((item) => (
<MenuItem
key={item.id}
id={item.id}
name={item.name}
price={item.price}
onEdit={() => router.push(`/update/${item.id}`)}
onDelete={handleDelete}
/>
))
) : (
<p>Loading...</p>
)}
</div>
);
}
上面的块代码很长,但让我们快速描述一下那里发生了什么。
-
Page
是组件的名称。Next.js 将在浏览器上显示来自该组件的代码。 -
接下来,我们将定义一些重要的状态,例如,
menuItems
用于从 库存 获取响应,getData
以及displaySuccessMessage
用于在创建/删除成功时显示反馈的 。我们还将检索router
和 等对象params
。这些对象将有助于实现删除逻辑以及菜单创建或编辑的路由。const [menuItems, setMenuItems] = useState(null); const router = useRouter(); const params = useSearchParams(); // State for displaying a success message const [displaySuccessMessage, setDisplaySuccessMessage] = useState({ show: false, type: "", // either 'add' or 'update' });
-
在接下来的代码行中,我们定义了三个
useEffect
钩子。第一个钩子帮助我们通过调用getData
方法从 API 中检索数据;第二个钩子用于在创建或更新成功时显示成功消息(我们使用 URL 中的参数来检查是否需要显示该消息);最后一个钩子useEffect
用于处理消息显示的时间。我们使用该setTimeout
方法仅显示 3 秒的消息。 -
接下来,我们编写添加按钮的 JSX 代码。我们还使用
MenuItem
上面声明的组件page.js
来显示菜单信息。
您可以在此处的GitHub 存储库中找到整个文件的代码。
我们现在有一个可以运行的列表页面。我们也可以删除文章了。现在让我们编写创建页面。
构建菜单的创建页面
在上一节中,我们构建了列出所有菜单的页面。在本节中,我们将构建创建菜单的页面。
这只是一个包含名称和价格字段的表单。我们开始吧。
在目录中src/app
,创建一个名为 的新目录add
。在这个新创建的目录中,创建一个名为 的文件page.js
。有了这个文件,当我们导航到该/add
路线时,我们将在 上显示代码src/app/add/page.js
。
让我们开始编写代码。
// src/app/add/page.js
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
/**
* Sends a POST request to create a new menu item.
* @param {Object} data The menu item data to be sent.
*/
async function createMenu(data) {
const res = await fetch("http://127.0.0.1:8000/api/menu/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (!res.ok) {
throw new Error("Failed to create data");
}
return res.json();
}
在上面的代码中,我们导入了管理副作用、状态和路由所需的钩子。下一个代码块更有趣,因为我们正在编写一个方法,createMenu
负责发送创建菜单的 POST 请求。此方法接受data
一个 JSON 对象作为参数,该对象包含创建菜单对象所需的数据。
我们现在可以开始编写Page
此页面的组件逻辑。
// src/app/add/page.js
...
const Page = () => {
const router = useRouter();
const [formData, setFormData] = useState({ name: "", price: "" });
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
/**
* Handles the form submission.
* @param {Event} event The form submission event.
*/
const onFinish = (event) => {
event.preventDefault();
setIsLoading(true);
createMenu(formData)
.then(() => {
// Navigate to the main page with a query parameter indicating success
router.replace("/?action=add");
})
.catch(() => {
setError("An error occurred");
setIsLoading(false);
});
};
// Cleanup effect for resetting loading state
useEffect(() => {
return () => setIsLoading(false);
}, []);
return (
<form onSubmit={onFinish}>
<div className="form-item">
<label htmlFor="name">Name</label>
<input
required
name="name"
value={formData.name}
onChange={(event) =>
setFormData({ ...formData, name: event.target.value })
}
/>
</div>
<div className="form-item">
<label htmlFor="price">Price</label>
<input
required
type="number"
name="price"
value={formData.price}
onChange={(event) =>
setFormData({ ...formData, price: event.target.value })
}
/>
</div>
{error && <p className="error-message">{error}</p>}
<div>
<button disabled={isLoading} className="add-button" type="submit">
Submit
</button>
</div>
</form>
);
};
export default Page;
上面的代码也很长,但让我们探索一下那里做了什么。
-
我们定义了三种状态来管理表单数据
formData
、在请求待处理时加载loading
,以及一种状态来存储从后端收到的错误并将其显示在前端error
。 -
接下来,我们有
onFinish
方法,它是用户提交表单时执行的方法。此方法将首先调用,event.preventDefault();
以防止用户点击表单的提交按钮时浏览器的默认行为,即重新加载页面。之后,我们将
loading
状态设置为 true,因为我们将启动一个创建请求。由于这是一个异步请求,因此我们会处理请求成功的情况,通过使用 URL 参数将用户重定向到列表页面,action=add
这将触发成功消息的显示。如果发生错误,我们会设置将在前端显示的错误消息。 -
接下来,我们有一个
useEffect
用于清理效果的方法,例如当用户离开页面或卸载组件时。 -
最后,我们使用常规 HTML 标签来构建表单、将所需的 props 值传递给输入并显示错误消息的 JSX 代码。
创建页面写好后,我们终于可以开始制作版本页面了。除了以下几点之外,与创建页面相比,其他方面并无太大变化:
-
使用编辑页面上的检索菜单
id
来用现有值填充表单,以便用户可以修改它们。 -
基本上就是这样。
让我们创建版本页面。
创建版本页面
在上一节中,我们学习了如何使用 Next.js 创建一个简单的表单,以及如何使用 API 向 API 发送请求fetch
。现在,我们有一个可以用于添加新菜单的创建页面。
在本节中,我们将构建一个用于修改菜单名称和价格的编辑页面。除了路由和我们需要修改菜单的信息之外,它与创建页面没有什么不同。
在目录中src/app/
,创建一个名为 的目录update
。在此目录中,创建一个名为 的新目录[menuId]
。这会告诉 Next.js 这是一个动态路由,因为menuId
可以根据菜单列表中选择进行编辑的项目进行更改。
例如,用户可以重定向到/update/1
或,其中/update/2
或。这也将帮助我们从 URL 中检索,以请求 API 从后端检索有关菜单的数据,并且我们可以使用表单中和的值来填充表单。menuId
1
2
menuId
price
name
在src/app/update/[menuId]/
目录中,创建page.js
文件。
// src/app/update/[menuId]/page.js
"use client"
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
/**
* Fetches a menu item by ID.
* @param {number} id The ID of the menu item to retrieve.
*/
async function getMenu(id) {
const res = await fetch(`http://127.0.0.1:8000/api/menu/${id}/`);
if (!res.ok) {
throw new Error("Failed to retrieve menu");
}
return res.json();
}
/**
* Updates a menu item by ID.
* @param {number} id The ID of the menu item to update.
* @param {Object} data The updated data for the menu item.
*/
async function updateMenu(id, data) {
const res = await fetch(`http://127.0.0.1:8000/api/menu/${id}/`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (!res.ok) {
throw new Error("Failed to update menu");
}
return res.json();
}
在上面的代码中,我们导入了管理副作用、状态和路由所需的钩子。在下一个代码块中,我们定义了两个方法:
-
getMenu
使用详细 API 端点从 API 中检索特定菜单。 -
updateMenu
发送 PUT 请求来更新有关特定菜单的信息。
让我们转到组件的代码Page
。
// src/app/update/[menuId]/page.js
...
const Page = ({ params }) => {
const router = useRouter();
const [formData, setFormData] = useState({ name: "", price: "" });
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
/**
* Handles form submission.
* @param {Event} event The form submission event.
*/
const onFinish = (event) => {
event.preventDefault();
setIsLoading(true);
updateMenu(params.menuId, formData)
.then(() => {
router.replace("/?action=update");
})
.catch(() => {
setError("An error occurred");
setIsLoading(false);
});
};
// Cleanup effect for resetting loading state
useEffect(() => {
return () => setIsLoading(false);
}, []);
// Fetch menu item data on component mount
useEffect(() => {
const fetchData = async () => {
try {
const data = await getMenu(params.menuId);
setFormData({ name: data.name, price: data.price });
} catch (error) {
setError(error.message);
}
};
fetchData();
}, [params.menuId]);
return (
<form onSubmit={onFinish}>
<div className="form-item">
<label htmlFor="name">Name</label>
<input
required
name="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
</div>
<div className="form-item">
<label htmlFor="price">Price</label>
<input
required
type="number"
name="price"
value={formData.price}
onChange={(e) => setFormData({ ...formData, price: e.target.value })}
/>
</div>
{error && <p className="error-message">{error}</p>}
<div>
<button disabled={isLoading} className="add-button" type="submit">
Submit
</button>
</div>
</form>
);
};
export default Page;
上面的代码与创建菜单的表单几乎相同,主要区别在于传递给Page
组件的 prop。
在 Next.js 13 中,params
对象是包含动态段值的 prop(在我们的例子中是menuId
)。有了 的值menuId
,我们可以通过传递值轻松触发getMenu
方法params.menuId
,还可以updateMenu
通过传递params.menuId
值和data
从表单中检索到的 来确保方法的更新请求。
太棒了!我们终于有了一个功能齐全的 Next.js 13 应用,它与 Django 集成,可以列出菜单,显示创建和编辑菜单信息的页面,还能处理删除操作。下面是该应用功能演示。
结论
以上就是使用 Django 和 Next.js 构建全栈应用程序的分步指南。该组合为 Web 开发提供了一个强大且可扩展的解决方案,将 Django 安全高效的后端功能与 Next.js 响应式、现代化的前端功能完美融合。
通过本指南,您学习了如何设置 Django REST API 以及如何在 Next.js 中创建前端应用程序。这个用于管理餐厅菜单的 CRUD 应用程序是一个实际示例,展示了如何将这两种技术无缝集成。
记住,旅程远不止于此。Django 和 Next.js 都拥有丰富的功能和可能性。我鼓励你深入探索,尝试更高级的功能,并继续磨练你作为全栈开发者的技能。
这是本文构建的应用程序代码库的链接。
文章来源:https://dev.to/koladev/building-a-fullstack-application-with-django-django-rest-nextjs-3e26