Django工程的分層結(jié)構(gòu)詳解
前言
傳統(tǒng)上我們都知道在Django中的MTV模式,具體內(nèi)容含義我們?cè)賮?lái)回顧一下:
M:是Model的簡(jiǎn)稱,它的目標(biāo)就是通過(guò)定義模型來(lái)處理和數(shù)據(jù)庫(kù)進(jìn)行交互,有了這一層或者這種類型的對(duì)象,我們就可以通過(guò)對(duì)象來(lái)操作數(shù)據(jù)。
V:是View的簡(jiǎn)稱,它的工作很少,就是接受用戶請(qǐng)求換句話說(shuō)就是通過(guò)HTTP請(qǐng)求接受用戶的輸入;另外把輸入信息發(fā)送給處理程并獲取結(jié)果;最后把結(jié)果發(fā)送給用戶,當(dāng)然最后這一步還可以使用模板來(lái)修飾數(shù)據(jù)。
T:是Template的簡(jiǎn)稱,這里主要是通過(guò)標(biāo)記語(yǔ)言來(lái)定義頁(yè)面,另外還可以嵌入模板語(yǔ)言讓引擎來(lái)渲染動(dòng)態(tài)數(shù)據(jù)。
這時(shí)候我們看到網(wǎng)上大多數(shù)的列子包括有些視頻課程里面只講MVT以及語(yǔ)法和其他功能實(shí)現(xiàn)等,但大家有沒(méi)有想過(guò)一個(gè)問(wèn)題,你的業(yè)務(wù)邏輯放在哪里?課程中的邏輯通常放在了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
無(wú)論是FBV還是CBV,當(dāng)用戶請(qǐng)求進(jìn)來(lái)并通過(guò)URL路由找到對(duì)應(yīng)的方法或者類,然后對(duì)請(qǐng)求進(jìn)行處理,比如可以直接返回模型數(shù)據(jù)、驗(yàn)證用戶輸入或者校驗(yàn)用戶名和密碼等。在學(xué)習(xí)階段或者功能非常簡(jiǎn)單的時(shí)候使用這種寫(xiě)法沒(méi)問(wèn)題,但是對(duì)于相對(duì)大一點(diǎn)的項(xiàng)目來(lái)說(shuō)你很多具體的處理流程開(kāi)始出現(xiàn),而這些東西都寫(xiě)到View里顯然你自己都看不下去。
FBV全名Function-based views,基于函數(shù)的視圖;CBV全名Class-based views,基于類的視圖
所以View,它就是一個(gè)控制器,它不應(yīng)該包含業(yè)務(wù)邏輯,事實(shí)上它應(yīng)該是一個(gè)很薄的層。
業(yè)務(wù)邏輯到底放哪里
網(wǎng)上也有很多文章回答了這個(gè)問(wèn)題,提到了Form層,這個(gè)其實(shí)是用于驗(yàn)證用戶輸入數(shù)據(jù)的格式,比如郵件地址是否正確、是否填寫(xiě)了用戶名和密碼,至于這個(gè)用戶名或者郵箱到底在數(shù)據(jù)庫(kù)中是否真實(shí)存在則不是它應(yīng)該關(guān)心的,它只是一個(gè)數(shù)據(jù)格式驗(yàn)證器。所以業(yè)務(wù)邏輯到底放哪里呢?顯然要引入另外一層。
關(guān)于這一層的名稱有些人叫做UseCase,也有些人叫做Service,至于什么名字無(wú)所謂只要是大家一看就明白的名稱就好。如果我們使用UseCase這個(gè)名字,那么我們的Djaong工程架構(gòu)就變成了MUVT,如果是Service那么就MSVT。
這一層的目標(biāo)是什么呢?它專注于具體業(yè)務(wù)邏輯,也就是不同用例的具體操作,比如用戶注冊(cè)、登陸和注銷(xiāo)都一個(gè)用例。所有模型都只是工作流程的一部分并且這一層也知道模型有哪些API。這么說(shuō)有些空洞,我們用一個(gè)例子來(lái)說(shuō)明:
場(chǎng)景是用戶注冊(cè):
- 信息填寫(xiě)規(guī)范且用戶不存在則注冊(cè)成功并發(fā)送賬戶激活郵件
- 如果用戶已存在則程序引發(fā)錯(cuò)誤,然后傳遞到上層并進(jìn)行告知用戶名已被占用
Django 2.2.1、Python 3.7
下圖是整個(gè)工程的結(jié)構(gòu)
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): # 擴(kuò)展一個(gè)字段,家庭住址 home_address = models.CharField(_('home address'), max_length=150, blank=True) # 賬戶是否被激活,與users表里默認(rèn)的is_active不是一回事 is_activated = models.BooleanField(_('activatition'), default=False, help_text=_('新賬戶注冊(cè)后是否通過(guò)郵件驗(yàn)證激活。'),) # 指定該模型的manager類 objects = UserAccountManager()
我們知道Django會(huì)為我們自動(dòng)建立一個(gè)叫做auth_user的表,也就是它自己的認(rèn)證內(nèi)容,這個(gè)user表本身就是一個(gè)模型,它就是繼承了AbstractUser類,而這個(gè)類有繼承了AbstractBaseUser,而這個(gè)類繼承了models.Model,所以我們這里就是一個(gè)模型。再說(shuō)回AbstractUser類,這個(gè)類里面定義了一些username、first_name、email、is_active等用戶屬性相關(guān)的字段,如果你覺(jué)得不夠用還可以自己擴(kuò)展。
為了讓Django使用我們擴(kuò)展的用戶模型,所以需要在settings.py中添加如下內(nèi)容:
AUTH_USER_MODEL = "users.UserAccount"
工具類
這個(gè)文件主要是放一些通用工具,比如發(fā)送郵件這種公共會(huì)調(diào)用的功能,utils.py內(nèi)容如下:
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這個(gè)東西使用還是它父類本身的功能,之所以這樣做是為了在必要的時(shí)候可以重寫(xiě)一些功能。父類PasswordResetTokenGenerator的功能主要是根據(jù)用戶主鍵來(lái)生成token,之后還會(huì)根據(jù)傳遞的token和用戶主鍵去檢查傳遞的token是否一致。
針對(duì)郵件發(fā)送我這里使用Django提供的封裝,你需要在settings.py中添加如下內(nèi)容:
# 郵件設(shè)置 EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_USE_SSL = True EMAIL_HOST = 'smtp.163.com' EMAIL_PORT = 465 EMAIL_HOST_USER = '' # 發(fā)件人郵箱地址 EMAIL_HOST_PASSWORD = '' # 發(fā)件人郵箱密碼 DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
Services層
這層主要是根據(jù)用例來(lái)實(shí)現(xiàn)業(yè)務(wù)邏輯,比如注冊(cè)用戶賬號(hào)和激活用戶賬號(hào)。
""" Service層,針對(duì)不同用例實(shí)現(xiàn)的業(yè)務(wù)邏輯代碼 """ 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): """ 檢查用戶名是否已經(jīng)被注冊(cè) :return: """ user_query_set = UserAccount.objects.find_by_username(username=self._username).first() if user_query_set: error_msg = ('用戶名 {} 已被注冊(cè),請(qǐng)更換。'.format(self._username)) raise UsernameAlreadyExistError(_(error_msg)) return True def _send_welcome_email_to(self, user_account): """ 注冊(cè)成功后發(fā)送電子郵件 :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): """ 這里是創(chuàng)建用戶 :return: """ # 這樣創(chuàng)建需要調(diào)用save() # ua = UserAccount(username=self._username, password=self._password, email=self._email) # ua.save() # return ua # 直接通過(guò)create_user則不需要調(diào)用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): """ 驗(yàn)證用戶是否存在 :return: 模型對(duì)象或者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 = ('激活用戶失敗,提供的用戶標(biāo)識(shí) {} 不正確,無(wú)此用戶。'.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等里面的內(nèi)容就是空的,目的是raise異常到自定義的異常中,這樣調(diào)用方通過(guò)try就可以捕獲,有些時(shí)候代碼執(zhí)行的結(jié)果影響調(diào)用方后續(xù)的處理,通常大家可能認(rèn)為需要通過(guò)返回值來(lái)判斷,比如True或者False,但通常這不是一個(gè)好辦法或者說(shuō)在有些時(shí)候不是,因?yàn)槟菢訒?huì)造成代碼冗長(zhǎng),比如下面的代碼:
這是上面代碼中的一部分,
def valid_data(self): """ 檢查用戶名是否已經(jīng)被注冊(cè) :return: """ user_query_set = UserAccount.objects.find_by_username(username=self._username).first() if user_query_set: error_msg = ('用戶名 {} 已被注冊(cè),請(qǐng)更換。'.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函數(shù)會(huì)執(zhí)行valid_data()函數(shù),如果執(zhí)行成功我才會(huì)向下執(zhí)行,可是你看我在execute函數(shù)中并沒(méi)有這樣的語(yǔ)句,比如:
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
換句話說(shuō)你的每個(gè)函數(shù)都可能有返回值,如果每一個(gè)你都這樣寫(xiě)代碼就太啰嗦了。其實(shí)你可以看到在valid_data函數(shù)中我的確返回了True,但是我希望你也應(yīng)該注意,如果用戶存在的話我并沒(méi)有返回False,而是raise一個(gè)異常,這樣這個(gè)異常就會(huì)被調(diào)用方獲取而且還能獲取錯(cuò)誤信息,這種方式將是一個(gè)很好的處理方式,具體你可以通過(guò)views.py中看到。
Forms表單驗(yàn)證
這里是對(duì)于用戶輸入做檢查
""" 表單驗(yàn)證功能 """ 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': '用戶名不能超過(guò)50個(gè)字符', '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 表示的含義是函數(shù)返回值類型是str,在打印函數(shù)annotation的時(shí)候回顯示。 """ clean_XXXX XXXX是字段名 比如這個(gè)方法是判斷兩次密碼是否一致,密碼框輸入的密碼就算符合規(guī)則但是也不代表兩個(gè)密碼一致,所以需要自己來(lái)進(jìn)行檢測(cè) :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
前端可以實(shí)現(xiàn)輸入驗(yàn)證,但是也很容易被跳過(guò),所以后端肯定也需要進(jìn)行操作,當(dāng)然我這里并沒(méi)有做預(yù)防XSS攻擊的措施,因?yàn)檫@個(gè)不是我們今天要討論的主要內(nèi)容。
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 的內(nèi)容: ", request.data) # print("request.POST 的內(nèi)容: ", request.POST) # 針對(duì)數(shù)據(jù)輸入做檢查,是否符合規(guī)則 ra_form = RegisterAccountForm(request.POST) if ra_form.is_valid(): # print("驗(yàn)證過(guò)的數(shù)據(jù):", ra_form.cleaned_data) rua = RegisterUserAccount(request=request, **ra_form.cleaned_data) try: rua.execute() except UsernameAlreadyExistError as err: # 這里就是捕獲自定義異常,并給form對(duì)象添加一個(gè)錯(cuò)誤信息,并通過(guò)模板渲染然后返回前端頁(yè)面 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 的內(nèi)容: ", request.data) print("request.POST 的內(nèi)容: ", 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由不同的類來(lái)處理,這里只做基本的接收輸入和返回輸出功能,至于接收到的輸入該如何處理則有其他組件來(lái)完成,針對(duì)輸入格式規(guī)范則由forms中的類來(lái)處理,針對(duì)數(shù)據(jù)驗(yàn)證過(guò)后的具體業(yè)務(wù)邏輯則由services中的類來(lái)處理。
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模板,我就不放在這里了
下載全部的代碼
頁(yè)面效果
激活郵件內(nèi)容
點(diǎn)擊后就會(huì)跳轉(zhuǎn)到登陸頁(yè)。下面我們從Django admin中查看,2個(gè)用戶是激活狀態(tài)的。
- django框架模型層功能、組成與用法分析
- 利用Django模版生成樹(shù)狀結(jié)構(gòu)實(shí)例代碼
- django模板結(jié)構(gòu)優(yōu)化的方法
- Django-Model數(shù)據(jù)庫(kù)操作(增刪改查、連表結(jié)構(gòu))詳解
- Django框架視圖層URL映射與反向解析實(shí)例分析
- Django框架視圖函數(shù)設(shè)計(jì)示例
- 使用PyCharm配合部署Python的Django框架的配置紀(jì)實(shí)
- Python的Django框架中設(shè)置日期和字段可選的方法
- 分析Python的Django框架的運(yùn)行方式及處理流程
- Linux下將Python的Django項(xiàng)目部署到Apache服務(wù)器
- Python的Django框架中settings文件的部署建議
- Django框架組成結(jié)構(gòu)、基本概念與文件功能分析
相關(guān)文章
Python中ValueError報(bào)錯(cuò)的原因和解決辦法
在Python編程中,ValueError是一種非常常見(jiàn)的異常類型,它通常發(fā)生在函數(shù)接收到一個(gè)有效類型但不適合該函數(shù)操作的值時(shí),本文將深入探討ValueError的報(bào)錯(cuò)原因、提供詳細(xì)的解決辦法,并通過(guò)豐富的代碼示例來(lái)加深理解,需要的朋友可以參考下2024-07-07Python畫(huà)圖小案例之小雪人超詳細(xì)源碼注釋
在看了很多Python教程之后,覺(jué)得是時(shí)候做點(diǎn)什么小項(xiàng)目來(lái)練練手了,于是想來(lái)想去,用python寫(xiě)了一個(gè)小雪人,代碼注釋無(wú)比詳細(xì)清楚,快來(lái)看看吧2021-09-09關(guān)于Python turtle庫(kù)使用時(shí)坐標(biāo)的確定方法
這篇文章主要介紹了關(guān)于Python turtle庫(kù)使用時(shí)坐標(biāo)的確定方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03如何使用Python做個(gè)自定義動(dòng)態(tài)壁紙
這篇文章主要介紹了如何使用Python做個(gè)自定義動(dòng)態(tài)壁紙的相關(guān)資料,需要的朋友可以參考下方法2021-08-08