视频 https://www.youtube.com/watch?v=c-QsfbznSXI 笔记
在windows 系统上开发此项目,Linux 命令有所不同。先写 Django,后写 React。
此项目实现的功能是,用户可以注册并登录网站,创建或删除 note,note 包含 title和 content。
文章目录
- Django 后端
- React 前端
- 部署数据库
- 部署后端
- 部署前端
- 部署完成
Django 后端
1. 创建虚拟环境
以安装必要的 python 包: python -m venv env
, 此命令将生成一个文件夹 env
.
2. 激活虚拟环境: ./env/Scripts/activate
或 activate
,
命令运行成功之后,终端行之前将出现 (env)
前缀:
3. 安装项目依赖:
- 首先在项目文件夹中新建文件
requirements.txt
,含有项目所需的所有包:
asgiref
Django
django-cors-headers
djangorestframework
djangorestframework-simplejwt
PyJWT
pytz
sqlparse
psycopg2-binary
python-dotenv
其中:
django-cors-headers
: 用于解决 cross origin request 问题
psycopg2-binary
: postgreSQL(postgres, pg) 相关
python-dotenv
: 用于加载环境变量
- 安装以上的包:
pip install -r requirements.txt
4. 新建 Django 工程
4.1 新建工程 backend
运行命令:django-admin startproject backend
,此命令将生成一个新目录 backend
。
然后在此新的backend
目录中新建名称为api
的 app:python manage.py startapp api
(env) PS D:\yt\django\django-react-tutorial> django-admin startproject backend
(env) PS D:\yt\django\django-react-tutorial> cd .\backend\
(env) PS D:\yt\django\django-react-tutorial\backend> python manage.py startapp api
Django 中的 app:一个 Django 由若干 app 组成,例如实现 authentication 的 app,一个组件也可以是一个 app, 这些 app 用于组织客制化的 code,对项目的代码实现逻辑上的划分。
工程结构:
4.2 设置 settings.py
这里用#1
标记增加的或修改过的代码:
"""
Django settings for backend project.
Generated by 'django-admin startproject' using Django 5.0.6.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
"""
from pathlib import Path
from datetime import timedelta # 1
from dotenv import load_dotenv # 1
import os # 1
load_dotenv() # 1
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-r$)xv6rc71731q(5d)y3!!b*m=78d*fp*m9l0$-_nua(26m5q("
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ["*"] # 1 allow any host to host our django application
# 1 JWT tokens related
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
),
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
}
# 1 JWT tokens related
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=30),
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
}
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"api", # 1 新增的 app
"rest_framework", # 1
"corsheaders", # 1
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"corsheaders.middleware.CorsMiddleware", # 1 middleware for cors
]
ROOT_URLCONF = "backend.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "backend.wsgi.application"
# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
STATIC_URL = "static/"
# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
CORS_ALLOW_ALL_ORIGINS = True # 1
CORS_ALLOWS_CREDENTIALS = True # 1
4.3 JWT 认证
实现认证的步骤:
- 用户使用 username + password 登录前端
- 前端发送请求给后端,此请求包含第1步的 username + password
- 后端生成两个 token:即 jwt access token 和 jwt fresh token,发给前端
- 前端在 local storage 存储这两个token
- 前端之后每次发送请求,首先读取 local storage,
- 如果 access_token 为空,要求前端重新登录
- 如果 access_token 已过期,自动发送 refresh token 给后端某个 api,获得新的 access token 并存储到 local storage,将 access token 附加到请求头。如果由于 refresh token 过期等原因,未能从后端获得 access token,要求前端重新登录
- 如果 access_token 非空且未过期,将 access token 附加到请求头,不需执行其他操作。
4.3.1 新建文件 ./backend/api/serializers.py
:
from django.contrib.auth.models import User
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "username", "password"]
# don't want to return the password when returning the user
extra_kwargs = {"password": {"write_only": True}}
def create(self, validated_data):
print(validated_data)
user = User.objects.create_user(**validated_data)
return user
4.3.2 修改./backend/api/views.py
:
from django.shortcuts import render
from django.contrib.auth.models import User
from rest_framework import generics
from .serializers import UserSerializer
from rest_framework.permissions import IsAuthenticated, AllowAny
class CreateUserView(generics.CreateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [AllowAny]
4.3.3 修改 ./backend/backend/urls.py
:
from django.contrib import admin
from django.urls import path, include
from api.views import CreateUserView
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
urlpatterns = [
path("admin/", admin.site.urls),
path("api/user/register/", CreateUserView.as_view(), name="register"),
path("api/token/", TokenObtainPairView.as_view(), name="get_token"),
path("api/token/refresh/", TokenRefreshView.as_view(), name="refresh"),
path("api-auth/", include("rest_framework.urls")),
]
4.3.4 数据库迁移
分两步:
Step 1: make migrations
终端执行命令:python manage.py makemigrations
(env) PS D:\yt\django\django-react-tutorial\backend> python manage.py makemigrations
No changes detected
(env) PS D:\yt\django\django-react-tutorial\backend>
makemigrations
的作用是生成迁移文件,这些文件指定了需要执行的数据库迁移操作。
Step 2: apply migrations
终端执行命令:python manage.py migrate
:
PS D:\yt\django\django-react-tutorial\backend> python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
............................
Applying sessions.0001_initial... OK
PS D:\yt\django\django-react-tutorial\backend>
这两步用于配置数据库,确保正确设置所需的表格等等。
因此,每当连接到新数据库时,都需要再次执行上述相同的步骤来配置新数据库。
4.4 运行程序
运行命令:python manage.py runserver
PS D:\yt\django\django-react-tutorial\backend> python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
May 10, 2024 - 11:34:36
Django version 5.0.6, using settings 'backend.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
浏览器访问 http://127.0.0.1:8000/api/user/register
,
下一步要做的就是传一个 username 和 password 给 server,server 生成新的 user
4.5 实现用户注册
实现 sign in ,并从 server 获取 access token:
- 在
http://127.0.0.1:8000/api/user/register
界面创建用户,填写 Username 和 Password 并 post,
- 在
http://127.0.0.1:8000/api/token/
路径输入上述 Username 和 Password,就会生成 access token 和 refresh token:
前端将会存储这两个 token,以后前端每次向后端发送一条请求,请求里都必须附带 access token。(不受保护的路径除外)
复制上面的 refresh token,打开url:http://127.0.0.1:8000/api/token/refresh/
,粘贴,提交, 可以得到新的 access token,如下图所示:
至此,用户注册、登录功能已经实现。Ctrl + C 停止服务器,接下来,实现创建 note 以及 删除 note 功能。
4.6 创建或删除 note 实现
4.6.1 修改./backend/api/models.py
:
from django.db import models
from django.contrib.auth.models import User
class Note(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="notes")
def __str__(self):
return self.title
这里,一个 author 可以有多个 note,是 one-many 的关系。
ForeignKey
是说,一个 note 链接到 一个 user,
on_delete=models.CASCADE
含义:如果删除某个 user,那么,将同时删除此用户的全部 note。
related_name="notes"
含义: notes
字段引用所有的 note, 通过 .notes
可以获得一个用户创建的全部 note 对象。
4.6.2 修改文件./backend/api/serializers.py
:
from django.contrib.auth.models import User
from rest_framework import serializers
from .models import Note
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "username", "password"]
# don't want to return the password when returning the user
# 能写不能读
extra_kwargs = {"password": {"write_only": True}}
def create(self, validated_data):
print(validated_data)
user = User.objects.create_user(**validated_data)
return user
class NoteSerializer(serializers.ModelSerializer):
class Meta:
model = Note
fields = ["id", "title", "content", "created_at", "author"]
# 能读不能写
extra_kwargs = {"author": {"read_only": True}}
4.6.3 修改文件 .\backend\api\views.py
from django.shortcuts import render
from django.contrib.auth.models import User
from rest_framework import generics
from .serializers import UserSerializer, NoteSerializer
from rest_framework.permissions import IsAuthenticated, AllowAny
from .models import Note
# 创建 note
# ListCreateAPIView, do two things:
# listing all notes created by a user or create a new note
class NoteListCreate(generics.ListCreateAPIView):
serializer_class = NoteSerializer
# Cannot call this route, unless authenticated and pass a valid jwt token
permission_classes = [IsAuthenticated]
# overriding get_queryset(django docs)
def get_queryset(self):
user = self.request.user
# get all notes written by this "user", that's what filter means
return Note.objects.filter(author=user)
# overriding perform_create(django docs)
def perform_create(self, serializer):
if serializer.is_valid():
serializer.save(author=self.request.user)
else:
print(serializer.errors)
# 删除 note
class NoteDelete(generics.DestroyAPIView):
serializer_class = NoteSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
user = self.request.user
return Note.objects.filter(author=user)
class CreateUserView(generics.CreateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [AllowAny]
4.6.4 新建文件 ./backend/api/urls.py
:
from django.urls import path
from . import views
urlpatterns = [
path("notes/", views.NoteListCreate.as_view(), name="note-list"),
path("notes/delete/<int:pk>/", views.NoteDelete.as_view(), name="delete-note"),
]
4.6.5 链接 url,修改: ./backend/backend/urls.py
:
from django.contrib import admin
from django.urls import path, include
from api.views import CreateUserView
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
urlpatterns = [
path("admin/", admin.site.urls),
path("api/user/register/", CreateUserView.as_view(), name="register"),
path("api/token/", TokenObtainPairView.as_view(), name="get_token"),
path("api/token/refresh/", TokenRefreshView.as_view(), name="refresh"),
path("api-auth/", include("rest_framework.urls")),
# 新增行,如果非以上任何路径,将转发到文件:api.urls
path("api/", include("api.urls")),
]
4.6.6 测试
再次运行数据库迁移的两条命令:ython manage.py makemigrations
以及 python manage.py migrate
:
PS D:\yt\django\django-react-tutorial> cd backend
PS D:\yt\django\django-react-tutorial\backend> python manage.py makemigrations
Migrations for 'api':
api\migrations\0001_initial.py
- Create model Note
PS D:\yt\django\django-react-tutorial\backend> python manage.py migrate
Operations to perform:
Apply all migrations: admin, api, auth, contenttypes, sessions
Running migrations:
Applying api.0001_initial... OK
PS D:\yt\django\django-react-tutorial\backend>
运行程序:python manage.py runserver
因为未传 token, 所以出现上述错误提示。后端到这里先结束,接下来写前端。
React 前端
1 新建 React 工程
主目录运行命令:npm create vite@latest frontend -- --template react
,将生成新的前端目录 frontend
, 如下图:
2 安装必要的 React npm 包
进入 frontend
目录,运行命令: npm i axios react-router-dom jwt-decode
,以下是运行结果,
PS D:\yt\django\django-react-tutorial> cd frontend
PS D:\yt\django\django-react-tutorial\frontend> npm i axios react-router-dom jwt-decode
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE package: 'vite@5.2.11',
npm WARN EBADENGINE required: { node: '^18.0.0 || >=20.0.0' },
npm WARN EBADENGINE current: { node: 'v16.20.2', npm: '8.5.5' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE package: 'jwt-decode@4.0.0',
npm WARN EBADENGINE required: { node: '>=18' },
npm WARN EBADENGINE current: { node: 'v16.20.2', npm: '8.5.5' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE package: 'rollup@4.17.2',
npm WARN EBADENGINE required: { node: '>=18.0.0', npm: '>=8.0.0' },
npm WARN EBADENGINE current: { node: 'v16.20.2', npm: '8.5.5' }
npm WARN EBADENGINE }
added 291 packages, and audited 292 packages in 27s
104 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
PS D:\yt\django\django-react-tutorial\frontend>
因为出现了若干版本相关的警告,所以接下来卸载 node, 重新下载最新版本 node-v20.13.1-x64.msi
并安装,然后运行如下两条命令卸载以上包,重新安装:
npm un axios react-router-dom jwt-decode
npm i axios react-router-dom jwt-decode
PS D:\yt\django\django-react-tutorial\frontend> npm un axios react-router-dom jwt-decode
removed 13 packages, and audited 279 packages in 1s
103 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
PS D:\yt\django\django-react-tutorial\frontend> npm i axios react-router-dom jwt-decode
added 13 packages, and audited 292 packages in 3s
104 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
PS D:\yt\django\django-react-tutorial\frontend>
3 组织 React 工程
3.1 删除 frontend/src/
路径下的文件 index.css
以及 App.css
。
3.2 删除 App.jsx
中不需要的代码:
import React from "react";
function App() {
return <></>;
}
export default App;
3.3 删除 main.jsx
中的import './index.css'
:
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
3.4 新建文件夹以及文件
src
目录下新建3个文件夹 pages
, styles
, components
,新建 2 个文件:constants.js
, api.js
,
frontend
目录下新建文件 .env
:
constants.js
:
export const ACCESS_TOKEN = "access"
export const REFRESH_TOKEN = "refresh"
acess token 和 refresh token 都将存储在 local storage 中,constants.js
文件用于访问存储的 token。
api.js
中写拦截器代码,拦截器用于拦截将要发送的任何请求,它会自动添加正确的请求头(request header),就不必每个请求中手动重复写相关代码。这里设置 axios
拦截器:
// api.js
import axios from "axios";
import { ACCESS_TOKEN } from "./constants";
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL,
});
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem(ACCESS_TOKEN);
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
export default api;
.env
文件设置:
VITE_API_URL="http://localhost:8000"
3.5 设置受保护的路由
components
文件夹下新建文件 ProtectedRoute.jsx
,访问此文件中的路由需要 token。
import { Navigate } from "react-router-dom";
import { jwtDecode } from "jwt-decode";
import api from "../api";
import { REFRESH_TOKEN, ACCESS_TOKEN } from "../constants";
import { useState, useEffect } from "react";
function ProtectedRoute({ children }) {
const [isAuthorized, setIsAuthorized] = useState(null);
useEffect(() => {
auth().catch(() => setIsAuthorized(false));
}, []);
// refresh the access token for us automatically
const refreshToken = async () => {
const refreshToken = localStorage.getItem(REFRESH_TOKEN);
try {
const res = await api.post("/api/token/refresh/", {
refresh: refreshToken,
});
if (res.status === 200) {
localStorage.setItem(ACCESS_TOKEN, res.data.access);
setIsAuthorized(true);
} else {
setIsAuthorized(false);
}
} catch (error) {
console.log(error);
setIsAuthorized(false);
}
};
// check if we need to refresh the token or we are good to go
const auth = async () => {
const token = localStorage.getItem(ACCESS_TOKEN);
if (!token) {
setIsAuthorized(false);
return;
}
const decoded = jwtDecode(token);
const tokenExpiration = decoded.exp;
const now = Date.now() / 1000; // in seconds
if (tokenExpiration < now) {
await refreshToken();
} else {
setIsAuthorized(true);
}
};
if (isAuthorized === null) {
return <div>Loading...</div>;
}
return isAuthorized ? children : <Navigate to="/login" />;
}
export default ProtectedRoute;
3.6 设置 pages 目录
新建4 个文件:Home.jsx
, Login.jsx
, NotFound.jsx
, Register.jsx
:
这4个文件输入 rafce
, 生成初始代码 (vscode 需要安装 extension: VS Code ES7+ React/Redux/React-Native/JS snippets):
以 register.jsx
为例:
import React from 'react'
const Register = () => {
return (
<div>Register</div>
)
}
export default Register
3.7 实现 Navigation
用 3.6 的 4 个page页测试导航。
src\App.js
:
import react from "react"
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"
import Login from "./pages/Login"
import Register from "./pages/Register"
import Home from "./pages/Home"
import NotFound from "./pages/NotFound"
import ProtectedRoute from "./components/ProtectedRoute"
function Logout() {
localStorage.clear()
return <Navigate to="/login" />
}
// Before registering, clear local storage to prevent from the possibilities
// of reading old tokens
function RegisterAndLogout() {
localStorage.clear()
return <Register />
}
function App() {
return (
<BrowserRouter>
<Routes>
<Route
path="/"
element={
<ProtectedRoute>
<Home />
</ProtectedRoute>
}
/>
<Route path="/login" element={<Login />} />
<Route path="/logout" element={<Logout />} />
<Route path="/register" element={<RegisterAndLogout />} />
<Route path="*" element={<NotFound />}></Route>
</Routes>
</BrowserRouter>
)
}
export default App
3.8 测试 Navigation
npm run dev
, 测试ok
3.9 register/login 实现
components
目录下新建文件: Form.jsx
,此组件为 register/login 共用。
import { useState } from "react";
import api from "../api";
import { useNavigate } from "react-router-dom";
import { ACCESS_TOKEN, REFRESH_TOKEN } from "../constants";
import "../styles/Form.css";
import LoadingIndicator from "./LoadingIndicator";
function Form({ route, method }) {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const name = method === "login" ? "Login" : "Register";
const handleSubmit = async (e) => {
setLoading(true);
e.preventDefault();
try {
const res = await api.post(route, { username, password });
if (method === "login") {
localStorage.setItem(ACCESS_TOKEN, res.data.access);
localStorage.setItem(REFRESH_TOKEN, res.data.refresh);
navigate("/");
} else {
navigate("/login");
}
} catch (error) {
alert(error);
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit} className="form-container">
<h1>{name}</h1>
<input
className="form-input"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Username"
/>
<input
className="form-input"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
{loading && <LoadingIndicator />}
<button className="form-button" type="submit">
{name}
</button>
</form>
);
}
export default Form;
对应的 css styles/Form.css
:
.form-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 50px auto;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
max-width: 400px;
}
.form-input {
width: 90%;
padding: 10px;
margin: 10px 0;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
.form-button {
width: 95%;
padding: 10px;
margin: 20px 0;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s ease-in-out;
}
.form-button:hover {
background-color: #0056b3;
}
login.jsx
代码:
import Form from "../components/Form";
function Login() {
return <Form route="/api/token/" method="login" />;
}
export default Login;
register.jsx
代码:
import Form from "../components/Form";
function Register() {
return <Form route="/api/user/register/" method="register" />;
}
export default Register;
新建两个文件 components/LoadingIndicator.jsx
和 styles/LoadingIndicator.css
components/LoadingIndicator.jsx
:
import "../styles/LoadingIndicator.css"
const LoadingIndicator = () => {
return <div className="loading-container">
<div className="loader"></div>
</div>
}
export default LoadingIndicator
styles/LoadingIndicator.css
.loader-container {
display: flex;
justify-content: center;
align-items: center;
}
.loader {
border: 5px solid #f3f3f3; /* Light grey */
border-top: 5px solid #3498db; /* Blue */
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
3.10 测试 register/login
frontend
目录 运行 npm run dev
,同时 backend
目录运行 python manage.py runserver
在 register 路径输入用户名和密码,将自动重定向到 login 路径,再次输入用户名和密码登录,local storage 将会出现 access token 和 refresh token ,测试 ok。
3.11 Homepage 实现
目标: http://localhost:5173/
界面实际展示的内容应该如下所示,列出所有的 notes,允许删除,并且能创建新的 note:
src/pages/Home.jsx
:
import { useState, useEffect } from "react";
import api from "../api";
import Note from "../components/Note";
import "../styles/Home.css";
function Home() {
const [notes, setNotes] = useState([]);
const [content, setContent] = useState("");
const [title, setTitle] = useState("");
useEffect(() => {
getNotes();
}, []);
const getNotes = () => {
api
.get("/api/notes/")
.then((res) => res.data)
.then((data) => {
setNotes(data);
console.log(data);
})
.catch((err) => alert(err));
};
const deleteNote = (id) => {
api
.delete(`/api/notes/delete/${id}/`)
.then((res) => {
if (res.status === 204) alert("Note deleted!");
else alert("Failed to delete note.");
getNotes();
})
.catch((error) => alert(error));
};
const createNote = (e) => {
e.preventDefault();
api
.post("/api/notes/", { content, title })
.then((res) => {
if (res.status === 201) alert("Note created!");
else alert("Failed to make note.");
getNotes();
})
.catch((err) => alert(err));
};
return (
<div>
<div>
<h2>Notes</h2>
{notes.map((note) => (
<Note note={note} onDelete={deleteNote} key={note.id} />
))}
</div>
<h2>Create a Note</h2>
<form onSubmit={createNote}>
<label htmlFor="title">Title:</label>
<br />
<input
type="text"
id="title"
name="title"
required
onChange={(e) => setTitle(e.target.value)}
value={title}
/>
<label htmlFor="content">Content:</label>
<br />
<textarea
id="content"
name="content"
required
value={content}
onChange={(e) => setContent(e.target.value)}
></textarea>
<br />
<input type="submit" value="Submit"></input>
</form>
</div>
);
}
export default Home;
Note.jsx
:
import React from "react";
import "../styles/Note.css"
function Note({ note, onDelete }) {
const formattedDate = new Date(note.created_at).toLocaleDateString("en-US")
return (
<div className="note-container">
<p className="note-title">{note.title}</p>
<p className="note-content">{note.content}</p>
<p className="note-date">{formattedDate}</p>
<button className="delete-button" onClick={() => onDelete(note.id)}>
Delete
</button>
</div>
);
}
export default Note
Note.css
:
.note-container {
padding: 10px;
margin: 20px 0;
border: 1px solid #ccc;
border-radius: 5px;
}
.note-title {
color: #333;
}
.note-content {
color: #666;
}
.note-date {
color: #999;
font-size: 0.8rem;
}
.delete-button {
background-color: #f44336; /* Red */
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
}
.delete-button:hover {
background-color: #d32f2f; /* Darker red */
}
省略一些文件例如 LoadingIndicator.jsx
等。
部署数据库
1 创建远程数据库服务
在网站 choreo上部署。左侧 tab 选择 database,然后创建 PostgreSQL 服务。
创建一个名称为 db 的数据库,虽然注明 0.03美元每小时,但不会真正收费,因为是免费的开发版,数据库每小时后会自动关闭服务,需手动开启。
copy 以上参数,paste 到 django 后端,实现数据库连接。
backend 目录新建文件: \backend\.env
:
DB_HOST=
DB_PORT=
DB_USER=
DB_NAME=
DB_PWD=
然后复制粘贴choreo上的各项参数,所有的值全部放在双引号中。
2 连接远程数据库
修改backend\backend\settings.py
中的数据库设置:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
改为:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.getenv("DB_NAME"),
"USER": os.getenv("DB_USER"),
"PASSWORD": os.getenv("DB_PWD"),
"HOST": os.getenv("DB_HOST"),
"PORT": os.getenv("DB_PORT"),
}
}
依次运行命令:
python manage.py migrate
,此命令连接远程数据库,因此需要较长时间,这一命令完成后,可以像之前那样启动后端,但此时数据库是远程的 PostgreSQL.
python manage.py runserver
,前端正常 work.
部署后端
1. 新建 github 仓库
需要将后端代码 push 到 github,之后,每次 commit 到 github, choreo 将自动更新部署。
1.1 新建 .gitignore
文件:
1.2 同时在 frontend\.gitignore
文件中也增加 env
。
backend\.gitignore
:
.env
db.sqlite3
1.3 新建 Choreo 配置文件:backend\.choreo\endpoints.yaml
:
version: 0.1
endpoints:
- name: "REST API"
port: 8000
type: REST
networkVisibility: Public
context: /
1.4 新建 Procfile, 用以设置执行此应用程序的命令, `
0.0.0.0` 表示允许在任意 origin 或公共地址上允许访问 app.
web: python manage.py runserver 0.0.0.0:8000
1.5 本地新建 git 仓库
- 将前端和后端代码都加到 git 中:
PS D:\yt\django\django-react-tutorial> git init
Initialized empty Git repository in D:/yt/django/django-react-tutorial/.git/
PS D:\yt\django\django-react-tutorial> git add .
warning: in the working copy of 'frontend/.eslintrc.cjs', LF will be replaced by CRLF the next time Git touches it
.....................
warning: in the working copy of 'frontend/vite.config.js', LF will be replaced by CRLF the next time Git touches it
PS D:\yt\django\django-react-tutorial> git commit -m "first commit"
[master (root-commit) aa8caac] first commit
59 files changed, 8381 insertions(+)
create mode 100644 .gitignore
.....
PS D:\yt\django\django-react-tutorial>
- 然后将主分支名称改为
main
, github 要求使用此分支名:
PS D:\yt\django\django-react-tutorial> git branch
* master
PS D:\yt\django\django-react-tutorial> git branch -M main
PS D:\yt\django\django-react-tutorial> git branch
* main
PS D:\yt\django\django-react-tutorial>
-
github 新建一个名称为 "Django-React-Full-Stack` 的仓库,必须设为 “public", 因为 Choreo 免费开发版只支持访问 public 仓库。
-
执行 github 上列出的如下命令:
PS D:\yt\django\django-react-tutorial> git remote add origin https://github.com/alice201601/Django-React-Full-Stack.git
error: remote origin already exists.
PS D:\yt\django\django-react-tutorial> git push -u origin main
Enumerating objects: 74, done.
1.6 部署
Choreo 新建工程:Django-React-Tutorial,
创建两个组件,组件类型 backend 选 service,frontend 选 web application,
frontend:
Choreo 构建相应的组件,并部署。
部署前端
代码修改后,使用的 git 命令:
git push origin main
上传到 github
若干步骤都和 Choreo 相关,省略。
部署完成
Django + React + PostgreSQL,运行ok: