您好,登錄后才能下訂單哦!
前言
傳統(tǒng)上我們都知道在Django中的MTV模式,具體內(nèi)容含義我們再來回顧一下:
M:是Model的簡稱,它的目標就是通過定義模型來處理和數(shù)據(jù)庫進行交互,有了這一層或者這種類型的對象,我們就可以通過對象來操作數(shù)據(jù)。
V:是View的簡稱,它的工作很少,就是接受用戶請求換句話說就是通過HTTP請求接受用戶的輸入;另外把輸入信息發(fā)送給處理程并獲取結(jié)果;最后把結(jié)果發(fā)送給用戶,當然最后這一步還可以使用模板來修飾數(shù)據(jù)。
T:是Template的簡稱,這里主要是通過標記語言來定義頁面,另外還可以嵌入模板語言讓引擎來渲染動態(tài)數(shù)據(jù)。
這時候我們看到網(wǎng)上大多數(shù)的列子包括有些視頻課程里面只講MVT以及語法和其他功能實現(xià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
無論是FBV還是CBV,當用戶請求進來并通過URL路由找到對應(yīng)的方法或者類,然后對請求進行處理,比如可以直接返回模型數(shù)據(jù)、驗證用戶輸入或者校驗用戶名和密碼等。在學習階段或者功能非常簡單的時候使用這種寫法沒問題,但是對于相對大一點的項目來說你很多具體的處理流程開始出現(xiàn),而這些東西都寫到View里顯然你自己都看不下去。
FBV全名Function-based views,基于函數(shù)的視圖;CBV全名Class-based views,基于類的視圖
所以View,它就是一個控制器,它不應(yīng)該包含業(yè)務(wù)邏輯,事實上它應(yīng)該是一個很薄的層。
業(yè)務(wù)邏輯到底放哪里
網(wǎng)上也有很多文章回答了這個問題,提到了Form層,這個其實是用于驗證用戶輸入數(shù)據(jù)的格式,比如郵件地址是否正確、是否填寫了用戶名和密碼,至于這個用戶名或者郵箱到底在數(shù)據(jù)庫中是否真實存在則不是它應(yīng)該關(guān)心的,它只是一個數(shù)據(jù)格式驗證器。所以業(yè)務(wù)邏輯到底放哪里呢?顯然要引入另外一層。
關(guān)于這一層的名稱有些人叫做UseCase,也有些人叫做Service,至于什么名字無所謂只要是大家一看就明白的名稱就好。如果我們使用UseCase這個名字,那么我們的Djaong工程架構(gòu)就變成了MUVT,如果是Service那么就MSVT。
這一層的目標是什么呢?它專注于具體業(yè)務(wù)邏輯,也就是不同用例的具體操作,比如用戶注冊、登陸和注銷都一個用例。所有模型都只是工作流程的一部分并且這一層也知道模型有哪些API。這么說有些空洞,我們用一個例子來說明:
場景是用戶注冊:
Django 2.2.1、Python 3.7
下圖是整個工程的結(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): # 擴展一個字段,家庭住址 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的表,也就是它自己的認證內(nèi)容,這個user表本身就是一個模型,它就是繼承了AbstractUser類,而這個類有繼承了AbstractBaseUser,而這個類繼承了models.Model,所以我們這里就是一個模型。再說回AbstractUser類,這個類里面定義了一些username、first_name、email、is_active等用戶屬性相關(guān)的字段,如果你覺得不夠用還可以自己擴展。
為了讓Django使用我們擴展的用戶模型,所以需要在settings.py中添加如下內(nèi)容:
AUTH_USER_MODEL = "users.UserAccount"
工具類
這個文件主要是放一些通用工具,比如發(fā)送郵件這種公共會調(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這個東西使用還是它父類本身的功能,之所以這樣做是為了在必要的時候可以重寫一些功能。父類PasswordResetTokenGenerator的功能主要是根據(jù)用戶主鍵來生成token,之后還會根據(jù)傳遞的token和用戶主鍵去檢查傳遞的token是否一致。
針對郵件發(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ù)用例來實現(xiàn)業(yè)務(wù)邏輯,比如注冊用戶賬號和激活用戶賬號。
""" Service層,針對不同用例實現(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)被注冊 :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): """ 注冊成功后發(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 # 直接通過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): """ 驗證用戶是否存在 :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等里面的內(nèi)容就是空的,目的是raise異常到自定義的異常中,這樣調(diào)用方通過try就可以捕獲,有些時候代碼執(zhí)行的結(jié)果影響調(diào)用方后續(xù)的處理,通常大家可能認為需要通過返回值來判斷,比如True或者False,但通常這不是一個好辦法或者說在有些時候不是,因為那樣會造成代碼冗長,比如下面的代碼:
這是上面代碼中的一部分,
def valid_data(self): """ 檢查用戶名是否已經(jīng)被注冊 :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函數(shù)會執(zhí)行valid_data()函數(shù),如果執(zhí)行成功我才會向下執(zhí)行,可是你看我在execute函數(shù)中并沒有這樣的語句,比如:
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
換句話說你的每個函數(shù)都可能有返回值,如果每一個你都這樣寫代碼就太啰嗦了。其實你可以看到在valid_data函數(shù)中我的確返回了True,但是我希望你也應(yīng)該注意,如果用戶存在的話我并沒有返回False,而是raise一個異常,這樣這個異常就會被調(diào)用方獲取而且還能獲取錯誤信息,這種方式將是一個很好的處理方式,具體你可以通過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 表示的含義是函數(shù)返回值類型是str,在打印函數(shù)annotation的時候回顯示。 """ clean_XXXX XXXX是字段名 比如這個方法是判斷兩次密碼是否一致,密碼框輸入的密碼就算符合規(guī)則但是也不代表兩個密碼一致,所以需要自己來進行檢測 :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
前端可以實現(xiàn)輸入驗證,但是也很容易被跳過,所以后端肯定也需要進行操作,當然我這里并沒有做預(yù)防XSS攻擊的措施,因為這個不是我們今天要討論的主要內(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) # 針對數(shù)據(jù)輸入做檢查,是否符合規(guī)則 ra_form = RegisterAccountForm(request.POST) if ra_form.is_valid(): # print("驗證過的數(shù)據(jù):", 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 的內(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由不同的類來處理,這里只做基本的接收輸入和返回輸出功能,至于接收到的輸入該如何處理則有其他組件來完成,針對輸入格式規(guī)范則由forms中的類來處理,針對數(shù)據(jù)驗證過后的具體業(yè)務(wù)邏輯則由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模板,我就不放在這里了
下載全部的代碼
頁面效果
激活郵件內(nèi)容
點擊后就會跳轉(zhuǎn)到登陸頁。下面我們從Django admin中查看,2個用戶是激活狀態(tài)的。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。