赞
踩
视频 https://www.youtube.com/watch?v=c-QsfbznSXI 笔记
在windows 系统上开发此项目,Linux 命令有所不同。先写 Django,后写 React。
此项目实现的功能是,用户可以注册并登录网站,创建或删除 note,note 包含 title和 content。
以安装必要的 python 包: python -m venv env
, 此命令将生成一个文件夹 env
.
./env/Scripts/activate
或 activate
,命令运行成功之后,终端行之前将出现 (env)
前缀:
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
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,对项目的代码实现逻辑上的划分。
工程结构:
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
实现认证的步骤:
./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
./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]
./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")),
]
分两步:
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>
这两步用于配置数据库,确保正确设置所需的表格等等。
因此,每当连接到新数据库时,都需要再次执行上述相同的步骤来配置新数据库。
运行命令: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
实现 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:复制上面的 refresh token,打开url:http://127.0.0.1:8000/api/token/refresh/
,粘贴,提交, 可以得到新的 access token,如下图所示:
至此,用户注册、登录功能已经实现。Ctrl + C 停止服务器,接下来,实现创建 note 以及 删除 note 功能。
./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 对象。
./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}}
.\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]
./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"),
]
./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")),
]
再次运行数据库迁移的两条命令: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, 所以出现上述错误提示。后端到这里先结束,接下来写前端。
主目录运行命令:npm create vite@latest frontend -- --template react
,将生成新的前端目录 frontend
, 如下图:
进入 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>
frontend/src/
路径下的文件 index.css
以及 App.css
。App.jsx
中不需要的代码:import React from "react";
function App() {
return <></>;
}
export default App;
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>,
)
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"
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;
新建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.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
npm run dev
, 测试ok
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); } }
frontend
目录 运行 npm run dev
,同时 backend
目录运行 python manage.py runserver
在 register 路径输入用户名和密码,将自动重定向到 login 路径,再次输入用户名和密码登录,local storage 将会出现 access token 和 refresh token ,测试 ok。
目标: 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
等。
在网站 choreo上部署。左侧 tab 选择 database,然后创建 PostgreSQL 服务。
创建一个名称为 db 的数据库,虽然注明 0.03美元每小时,但不会真正收费,因为是免费的开发版,数据库每小时后会自动关闭服务,需手动开启。
copy 以上参数,paste 到 django 后端,实现数据库连接。
backend 目录新建文件: \backend\.env
:
DB_HOST=
DB_PORT=
DB_USER=
DB_NAME=
DB_PWD=
然后复制粘贴choreo上的各项参数,所有的值全部放在双引号中。
修改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.
需要将后端代码 push 到 github,之后,每次 commit 到 github, choreo 将自动更新部署。
.gitignore
文件:frontend\.gitignore
文件中也增加 env
。backend\.gitignore
:
.env
db.sqlite3
backend\.choreo\endpoints.yaml
:version: 0.1
endpoints:
- name: "REST API"
port: 8000
type: REST
networkVisibility: Public
context: /
0.0.0.0` 表示允许在任意 origin 或公共地址上允许访问 app.
web: python manage.py runserver 0.0.0.0:8000
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:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。