後端
在 UserProfile 模型中加入 avatar 欄位
建立 API 讓用戶能上傳/更新頭像
設定 Django 的 MEDIA_URL 讓圖片可存取
確保未登入用戶無法上傳頭像
model(新增用戶頭貼欄位)
透過擴展 User 模型(資料表) 加入 avatar
欄位,這樣可以不影響現有的 User 模型(資料表),而是額外增加一個 UserProfile
模型(資料表)來存放用戶的頭像圖片
在 Django ORM 物件關聯映射(Object-Relational Mapping)中,在 UserProfile
模型(資料表)內這樣定義:
1 2 3 4 5 6 7 8 9 10 11 class UserProfile (models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) avatar = models.ImageField( upload_to='avatars/' , null=True , blank=True , default='avatars/default_avatar.png' ) def __str__ (self ): return str (self.name)
📌 OneToOneField 會自動在 User 模型產生 userprofile 屬性
OneToOneField(User, on_delete=models.CASCADE) 表示 每個 UserProfile 只對應一個 User
Django 自動在 User 物件上 建立一個 userprofile 屬性,讓我們可以這樣存取 request.user.userprofile
,這樣會返回一個 UserProfile 物件
建立完 UserProfile
模型後,記得一樣要 migrate
:
1 2 python manage.py makemigrations python manage.py migrate
signals(新增用戶時自動建立 Profile)
在 signals.py 內加入一個信號(signals) 來確保每次建立 User 時都會自動建立對應的 UserProfile
1 2 3 4 from django.db.models.signals import post_save from django.dispatch import receiver...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from django.apps import AppConfigclass ApiConfig (AppConfig ): default_auto_field = 'django.db.models.BigAutoField' name = 'api' def ready (self ): import api.signals class UsersConfig (AppConfig ): default_auto_field = 'django.db.models.BigAutoField' name = 'users' def ready (self ): import api.signals
寫測試檔
此步驟是為了測試 signals
是否有正常觸發,確保每次建立 User 時都朋自動建立對應的 UserProfile
1 2 3 4 5 6 7 8 from django.test import TestCasefrom django.contrib.auth.models import Userfrom .models import UserProfileclass UserProfileSignalTest (TestCase ): def test_user_profile_creation_signal (self ): user = User.objects.create(username="testuser" , email="test@example.com" , password="password123" ) self.assertTrue(UserProfile.objects.filter (user=user).exists(), "UserProfile 未正確建立" )
終端機執行測試,api 為目錄名稱:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 python manage.py test api ... Found 1 test (s). Creating test database for alias 'default' ... System check identified no issues (0 silenced). Signal triggered User Created User saved . ---------------------------------------------------------------------- Ran 1 test in 0.013s OK
serializer(用戶資料序列化)
由於 Django 內部使用 Python 物件 (即 UserProfile 模型),但 API 需要回傳 JSON 格式的資料。所以建立相應的 serializer 將 Django ORM 模型轉換為 JSON 格式,
比如說:
Django 模型
1 2 user_profile = UserProfile.objects.get(user=request.user) print (user_profile.avatar)
透過 Serializer 轉換為 JSON
1 2 serializer = UserProfileSerializer(user_profile) print (serializer.data)
1 2 3 4 5 6 class UserProfileSerializer (serializers.ModelSerializer): class Meta : model = UserProfile fields = ['avatar' ]
然後為了之後能讓用戶多了 avatar
欄位的資料能夠被序列化,在 UserSerializer
中加入 avatar
欄位,這樣就能在用戶註冊或重設密碼時一併返回頭像:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class UserSerializer (serializers.ModelSerializer): ... avatar = serializers.SerializerMethodField(read_only=True ) class Meta : model = User fields = [...'' , 'avatar' ] def get_avatar (self, obj ): try : if obj.userprofile.avatar: return obj.userprofile.avatar.url except AttributeError: pass return '/media/avatars/default_avatar.png' ... class UserSerializerWithToken (UserSerializer ): token = serializers.SerializerMethodField(read_only=True ) class Meta : model = User fields = [...,'avatar' , 'token' ] def get_token (self, obj ): token = RefreshToken.for_user(obj) return str (token.access_token)
view(用戶更新/上傳頭像 API)
UserProfile
的 user
欄位是 OneToOneField
連結到 User
資料表(Django 預設模型) ,Django 會自動在 User
模型上自動產生一個反向關聯,名稱是 userprofile(Django 預設會取小寫類名)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @api_view(['POST' ] ) def updateAvatar (request ): if isinstance (request.user, AnonymousUser): return Response({"error" : "未登入的使用者" }, status=401 ) try : user_profile = request.user.userprofile except UserProfile.DoesNotExist: return Response({"error" : "找不到 UserProfile,請確保已登入" }, status=400 ) serializer = UserProfileSerializer(instance=user_profile, data=request.data, partial=True ) if serializer.is_valid(): serializer.save() return Response({"message" : "頭像更新成功" , "avatar" : serializer.data['avatar' ]}) return Response(serializer.errors, status=400 )
url
1 2 path('profile/avatar/' , views.updateAvatar, name='users_profile_avatar' ),
前端
允許用戶選擇圖片並預覽
使用 axios 上傳頭像
Redux 即時更新用戶資訊
確保瀏覽器不快取舊圖片
傳統方式實作方式:
後端提供 POST /avatar API,負責上傳照片並回傳其網址,確保不覆蓋舊照片。
前端發送 /avatar requset,成功後獲取照片網址,設置 img
的 src 屬性以顯示預覽。
作法缺點:
耗時:每次預覽都需重新上傳照片,等待時間長、影響使用體驗
處理預覽照片:預覽照片清除問題,不管?或用 cron job 定期處理?
因此以下將使用更為節省資源、簡單的方式
用戶編輯個人資料前端頁面
總共會有 3 個 State:
avatarPic: 存放選擇的圖片 File 物件,確保上傳時 FormData
包含正確的圖片
preview: 存放 URL.createObjectURL(file) 預覽圖片,即時預覽選擇到圖片,不用等上傳
profilePicUploading: 控制上傳狀態
1 2 3 4 5 6 7 8 import axios from "axios" ;... const [avatarPic, setAvatarPic] = useState ("" );const [preview, setPreview] = useState ("" );const [profilePicUploading, setProfilePicUploading] = useState (false );...
handleFileChange
- 選擇圖片並預覽,選擇檔案按鈕
updateAvatar
- 上傳新頭像,更新頭像按鈕
handleFileChange 用戶不需上傳才能看到結果,透過 URL.createObjectURL(file)
來預覽圖片,這樣用戶就能即時看到選擇的圖片
URL.createObjectURL()
為瀏覽器內建 API,能為選擇的檔案聲成一個短暫的本地 URL(類似 blob:http://localhost/xxxx):
在上傳頭像按鈕處,上傳成功後,dispatch(getUserDetails("profile"))
立即重新獲取用戶資訊,確保 Redux 狀態更新,讓此頁面立即顯示最新頭像
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 const handleFileChange = (e ) => { const file = e.target .files [0 ]; if (file) { setAvatarPic (file); setPreview (URL .createObjectURL (file)); } }; const updateAvatar = async (file ) => { if (!preview) { setMessage ({ text : "請選擇檔案!沒檔案就不要按了==" , variant : "danger" }); return ; } const formData = new FormData (); formData.append ("avatar" , avatarPic); try { setProfilePicUploading (true ); const { data } = await axios.post ("/api/users/profile/avatar/" , formData, { headers : { "Content-Type" : "multipart/form-data" , Authorization : `Bearer ${userInfo.token} ` , }, }); console .log ("頭像更新成功" , data); setAvatarPic ("" ); dispatch (getUserDetails ("profile" )); } catch (error) { console .error ("更新失敗" , error.response ?.data || error.message ); } finally { setProfilePicUploading (false ); } };
<Image>
邏輯為若有選擇新圖檔 preview
就顯示 preview
,若無則顯示此使用者的舊頭像 avatarPic
或預設頭像
<Form.Control>
負責讓使用者選擇圖片 onChange={handleFileChange}
,<Button>
則負責真正的與後端交互控制上傳 onClick={updateAvatar}
1 2 3 4 5 6 7 8 9 10 11 <Form .Group controlId='profilePic' className='m-3' > <Form.Label > 個人照片</Form.Label > <div className ='d-col align-items-center' > <Image src ={preview || avatarPic || "/media /avatars /default_avatar.png "} roundedCircle className ='mb-2' style ={{ width: "100px ", height: "100px ", objectFit: "cover ", marginRight: "10px " }} /> <Form.Control type ='file' accept ='image/*' onChange ={handleFileChange} disabled ={profilePicUploading} /> </div > <Button className ='mt-2' onClick ={updateAvatar} disabled ={profilePicUploading} > {profilePicUploading ? "上傳中..." : "上傳頭像"} </Button > {profilePicUploading && <Loader /> } </Form .Group >
成果
上傳頭像完成後,應就能在先前在 settings.py
及 urls.py
設定好的 media/
目錄下看到上傳的新頭像
最終成功畫面,Header 及 Profile 頁面頭像皆能同步即時更新:
若您覺得這篇文章對您有幫助,歡迎分享出去讓更多人看到⊂◉‿◉つ~
留言版