Django工程的分層結構,網上大部分代碼都是功能性的,當你面對稍微複雜一點的場景就應該考慮代碼分層,頁面、路由、表單驗證、業務邏輯、數據應該如何安排,看完這篇文章你或許就有了思路。 ...
前言
傳統上我們都知道在Django中的MTV模式,具體內容含義我們再來回顧一下:
M:是Model的簡稱,它的目標就是通過定義模型來處理和資料庫進行交互,有了這一層或者這種類型的對象,我們就可以通過對象來操作數據。
V:是View的簡稱,它的工作很少,就是接受用戶請求換句話說就是通過HTTP請求接受用戶的輸入;另外把輸入信息發送給處理程並獲取結果;最後把結果發送給用戶,當然最後這一步還可以使用模板來修飾數據。
T:是Template的簡稱,這裡主要是通過標記語言來定義頁面,另外還可以嵌入模板語言讓引擎來渲染動態數據。
這時候我們看到網上大多數的列子包括有些視頻課程裡面只講MVT以及語法和其他功能實現等,但大家有沒有想過一個問題,你的業務邏輯放在哪裡?課程中的邏輯通常放在了View裡面,就像下麵:
# urls.py
path('hello/', Hello),
path('helloworld/', HelloWorld.as_view())
# View
from django.views import View
# FVB
def Hello(request):
if request.method == "GET":
return HttpResponse("Hello world")
# CVB
class HelloWorld(View):
def get(self, request):
pass
def post(self, request):
pass
無論是FBV還是CBV,當用戶請求進來並通過URL路由找到對應的方法或者類,然後對請求進行處理,比如可以直接返回模型數據、驗證用戶輸入或者校驗用戶名和密碼等。在學習階段或者功能非常簡單的時候使用這種寫法沒問題,但是對於相對大一點的項目來說你很多具體的處理流程開始出現,而這些東西都寫到View里顯然你自己都看不下去。
FBV全名Function-based views,基於函數的視圖;CBV全名Class-based views,基於類的視圖
所以View,它就是一個控制器,它不應該包含業務邏輯,事實上它應該是一個很薄的層。
業務邏輯到底放哪裡
網上也有很多文章回答了這個問題,提到了Form層,這個其實是用於驗證用戶輸入數據的格式,比如郵件地址是否正確、是否填寫了用戶名和密碼,至於這個用戶名或者郵箱到底在資料庫中是否真實存在則不是它應該關心的,它只是一個數據格式驗證器。所以業務邏輯到底放哪裡呢?顯然要引入另外一層。
關於這一層的名稱有些人叫做UseCase,也有些人叫做Service,至於什麼名字無所謂只要是大家一看就明白的名稱就好。如果我們使用UseCase這個名字,那麼我們的Djaong工程架構就變成了MUVT,如果是Service那麼就MSVT。
這一層的目標是什麼呢?它專註於具體業務邏輯,也就是不同用例的具體操作,比如用戶註冊、登陸和註銷都一個用例。所有模型都只是工作流程的一部分並且這一層也知道模型有哪些API。這麼說有些空洞,我們用一個例子來說明:
場景是用戶註冊:
信息填寫規範且用戶不存在則註冊成功併發送賬戶激活郵件
如果用戶已存在則程式引發錯誤,然後傳遞到上層併進行告知用戶名已被占用
Django 2.2.1、Python 3.7
下圖是整個工程的結構
Models層
models.py
from django.db import models
from django.utils.translation import gettext as _
# Create your models here.
from django.contrib.auth.models import AbstractUser, UserManager, User
class UserAccountManager(UserManager):
# 管理器
def find_by_username(self, username):
queryset = self.get_queryset()
return queryset.filter(username=username)
class UserAccount(AbstractUser):
# 擴展一個欄位,家庭住址
home_address = models.CharField(_('home address'), max_length=150, blank=True)
# 賬戶是否被激活,與users表裡預設的is_active不是一回事
is_activated = models.BooleanField(_('activatition'), default=False, help_text=_('新賬戶註冊後是否通過郵件驗證激活。'),)
# 指定該模型的manager類
objects = UserAccountManager()
我們知道Django會為我們自動建立一個叫做auth_user的表,也就是它自己的認證內容,這個user表本身就是一個模型,它就是繼承了AbstractUser類,而這個類有繼承了AbstractBaseUser,而這個類繼承了models.Model,所以我們這裡就是一個模型。再說回AbstractUser類,這個類裡面定義了一些username、first_name、email、is_active等用戶屬性相關的欄位,如果你覺得不夠用還可以自己擴展。
為了讓Django使用我們擴展的用戶模型,所以需要在settings.py中添加如下內容:
AUTH_USER_MODEL = "users.UserAccount"
工具類
這個文件主要是放一些通用工具,比如發送郵件這種公共會調用的功能,utils.py內容如下:
from django.core.mail import send_mail
from django.contrib.sites.shortcuts import get_current_site
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.utils import six
from django.template.loader import render_to_string
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes, force_text
from mysite import settings
class TokenGenerator(PasswordResetTokenGenerator):
def __init__(self):
super(TokenGenerator, self).__init__()
# def _make_hash_value(self, user, timestamp):
# return (
# six.text_type(user.pk) + six.text_type(timestamp) + six.text_type(user.is_active)
# )
class WelcomeEmail:
subject = 'Activate Your Account'
@classmethod
def send_to(cls, request, user_account):
try:
current_site = get_current_site(request)
account_activation_token = TokenGenerator()
message = render_to_string('activate_account.html', {
'username': user_account.username,
'domain': current_site.domain,
'uid': urlsafe_base64_encode(force_bytes(user_account.id)),
'token': account_activation_token.make_token(user_account),
})
send_mail(
subject=cls.subject,
message=message,
from_email=settings.EMAIL_HOST_USER,
recipient_list=[user_account.email]
)
except Exception as err:
print(err)
TokenGenerator這個東西使用還是它父類本身的功能,之所以這樣做是為了在必要的時候可以重寫一些功能。父類PasswordResetTokenGenerator的功能主要是根據用戶主鍵來生成token,之後還會根據傳遞的token和用戶主鍵去檢查傳遞的token是否一致。
針對郵件發送我這裡使用Django提供的封裝,你需要在settings.py中添加如下內容:
# 郵件設置
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_SSL = True
EMAIL_HOST = 'smtp.163.com'
EMAIL_PORT = 465
EMAIL_HOST_USER = '' # 發件人郵箱地址
EMAIL_HOST_PASSWORD = '' # 發件人郵箱密碼
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
Services層
這層主要是根據用例來實現業務邏輯,比如註冊用戶賬號和激活用戶賬號。
"""
Service層,針對不同用例實現的業務邏輯代碼
"""
from django.utils.translation import gettext as _
from django.shortcuts import render
from .utils import (
WelcomeEmail,
TokenGenerator,
)
from users.models import (
UserAccount
)
class UsernameAlreadyExistError(Exception):
pass
class UserIdIsNotExistError(Exception):
"""
用戶ID,主鍵不存在
"""
pass
class ActivatitionTokenError(Exception):
pass
class RegisterUserAccount:
def __init__(self, request, username, password, confirm_password, email):
self._username = username
self._password = password
self._email = email
self._request = request
def valid_data(self):
"""
檢查用戶名是否已經被註冊
:return:
"""
user_query_set = UserAccount.objects.find_by_username(username=self._username).first()
if user_query_set:
error_msg = ('用戶名 {} 已被註冊,請更換。'.format(self._username))
raise UsernameAlreadyExistError(_(error_msg))
return True
def _send_welcome_email_to(self, user_account):
"""
註冊成功後發送電子郵件
:param user_account:
:return:
"""
WelcomeEmail.send_to(self._request, user_account)
def execute(self):
self.valid_data()
user_account = self._factory_user_account()
self._send_welcome_email_to(user_account)
return user_account
def _factory_user_account(self):
"""
這裡是創建用戶
:return:
"""
# 這樣創建需要調用save()
# ua = UserAccount(username=self._username, password=self._password, email=self._email)
# ua.save()
# return ua
# 直接通過create_user則不需要調用save()
return UserAccount.objects.create_user(
self._username,
self._email,
self._password,
)
class ActivateUserAccount:
def __init__(self, uid, token):
self._uid = uid
self._token = token
def _account_valid(self):
"""
驗證用戶是否存在
:return: 模型對象或者None
"""
return UserAccount.objects.all().get(id=self._uid)
def execute(self):
# 查詢是否有用戶
user_account = self._account_valid()
account_activation_token = TokenGenerator()
if user_account is None:
error_msg = ('激活用戶失敗,提供的用戶標識 {} 不正確,無此用戶。'.format(self._uid))
raise UserIdIsNotExistError(_(error_msg))
if not account_activation_token.check_token(user_account, self._token):
error_msg = ('激活用戶失敗,提供的Token {} 不正確。'.format(self._token))
raise ActivatitionTokenError(_(error_msg))
user_account.is_activated = True
user_account.save()
return True
這裡定義的異常類比如UsernameAlreadyExistError等裡面的內容就是空的,目的是raise異常到自定義的異常中,這樣調用方通過try就可以捕獲,有些時候代碼執行的結果影響調用方後續的處理,通常大家可能認為需要通過返回值來判斷,比如True或者False,但通常這不是一個好辦法或者說在有些時候不是,因為那樣會造成代碼冗長,比如下麵的代碼:
這是上面代碼中的一部分,
def valid_data(self):
"""
檢查用戶名是否已經被註冊
:return:
"""
user_query_set = UserAccount.objects.find_by_username(username=self._username).first()
if user_query_set:
error_msg = ('用戶名 {} 已被註冊,請更換。'.format(self._username))
raise UsernameAlreadyExistError(_(error_msg))
return True
def execute(self):
self.valid_data()
user_account = self._factory_user_account()
self._send_welcome_email_to(user_account)
return user_account
execute函數會執行valid_data()函數,如果執行成功我才會向下執行,可是你看我在execute函數中並沒有這樣的語句,比如:
def execute(self):
if self.valid_data():
user_account = self._factory_user_account()
self._send_welcome_email_to(user_account)
return user_account
else:
pass
換句話說你的每個函數都可能有返回值,如果每一個你都這樣寫代碼就太啰嗦了。其實你可以看到在valid_data函數中我的確返回了True,但是我希望你也應該註意,如果用戶存在的話我並沒有返回False,而是raise一個異常,這樣這個異常就會被調用方獲取而且還能獲取錯誤信息,這種方式將是一個很好的處理方式,具體你可以通過views.py中看到。
Forms表單驗證
這裡是對於用戶輸入做檢查
"""
表單驗證功能
"""
from django import forms
from django.utils.translation import gettext as _
class RegisterAccountForm(forms.Form):
username = forms.CharField(max_length=50, required=True, error_messages={
'max_length': '用戶名不能超過50個字元',
'required': '用戶名不能為空',
})
email = forms.EmailField(required=True)
password = forms.CharField(min_length=6, max_length=20, required=True, widget=forms.PasswordInput())
confirm_password = forms.CharField(min_length=6, max_length=20, required=True, widget=forms.PasswordInput())
def clean_confirm_password(self) -> str: # -> str 表示的含義是函數返回值類型是str,在列印函數annotation的時候回顯示。
"""
clean_XXXX XXXX是欄位名
比如這個方法是判斷兩次密碼是否一致,密碼框輸入的密碼就算符合規則但是也不代表兩個密碼一致,所以需要自己來進行檢測
:return:
"""
password = self.cleaned_data.get('password')
confirm_password = self.cleaned_data.get('confirm_password')
if confirm_password != password:
raise forms.ValidationError(message='Password and confirmation do not match each other')
return confirm_password
前端可以實現輸入驗證,但是也很容易被跳過,所以後端肯定也需要進行操作,當然我這裡並沒有做預防XSS攻擊的措施,因為這個不是我們今天要討論的主要內容。
Views
from django.shortcuts import render, HttpResponse, HttpResponseRedirect
from rest_framework.views import APIView
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes, force_text
from .forms import (
RegisterAccountForm,
)
from .services import (
RegisterUserAccount,
UsernameAlreadyExistError,
ActivateUserAccount,
ActivatitionTokenError,
UserIdIsNotExistError,
)
# Create your views here.
class Register(APIView):
def get(self, request):
return render(request, 'register.html')
def post(self, request):
# print("request.data 的內容: ", request.data)
# print("request.POST 的內容: ", request.POST)
# 針對數據輸入做檢查,是否符合規則
ra_form = RegisterAccountForm(request.POST)
if ra_form.is_valid():
# print("驗證過的數據:", ra_form.cleaned_data)
rua = RegisterUserAccount(request=request, **ra_form.cleaned_data)
try:
rua.execute()
except UsernameAlreadyExistError as err:
# 這裡就是捕獲自定義異常,並給form對象添加一個錯誤信息,並通過模板渲染然後返回前端頁面
ra_form.add_error('username', str(err))
return render(request, 'register.html', {'info': ra_form.errors})
return HttpResponse('We have sent you an email, please confirm your email address to complete registration')
# return HttpResponseRedirect("/account/login/")
else:
return render(request, 'register.html', {'info': ra_form.errors})
class Login(APIView):
def get(self, request):
return render(request, 'login.html')
def post(self, request):
print("request.data 的內容: ", request.data)
print("request.POST 的內容: ", request.POST)
pass
class ActivateAccount(APIView):
# 用戶激活賬戶
def get(self, request, uidb64, token):
try:
# 獲取URL中的用戶ID
uid = force_bytes(urlsafe_base64_decode(uidb64))
# 激活用戶
aua = ActivateUserAccount(uid, token)
aua.execute()
return render(request, 'login.html')
except(ActivatitionTokenError, UserIdIsNotExistError) as err:
return HttpResponse('Activation is failed.')
這裡就是視圖層不同URL由不同的類來處理,這裡只做基本的接收輸入和返回輸出功能,至於接收到的輸入該如何處理則有其他組件來完成,針對輸入格式規範則由forms中的類來處理,針對數據驗證過後的具體業務邏輯則由services中的類來處理。
Urls
from django.urls import path, re_path, include
from .views import (
Register,
Login,
ActivateAccount,
)
app_name = 'users'
urlpatterns = [
re_path(r'^register/$', Register.as_view(), name='register'),
re_path(r'^login/$', Login.as_view(), name='login'),
re_path(r'^activate/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
ActivateAccount.as_view(), name='activate'),
]
Templates
是我用到的html模板,我就不放在這裡了,大家可以去這裡https://files.cnblogs.com/files/rexcheny/mysite.zip
下載全部的代碼
頁面效果
激活郵件內容
點擊後就會跳轉到登陸頁。下麵我們從Django admin中查看,2個用戶是激活狀態的。