我如何用Django開發一個專案

50Percent發表於2018-11-18

前言

網路上關於Django的內容其實已經很多了,包括自己也是從網路上的內容一點一從零開始學習Django的,但是這部分內容都過於零散,可以說大部分都是在講怎麼從django-admin startproject到建立Model,再到寫一個TODO List或者一個Blog之類的內容,對於已經瞭解Django的人來說,並沒有太好渠道去了解一些別人的實踐,在開發自己的專案時,遇到的需求很難去了解其他人會怎麼設計,當然這也是社群存在的通病,大家更多在討論語言層面的問題.
我希望能把自己平時在工作中總結出來的tips分享出來,也許在某些地方能幫助到別人,也算是自己梳理經驗,自我提高的一個過程.
既然標題是如何開發,那我就按照一個專案的生命週期來整理.

需求分析

討論需求的事情就不談了.

一個專案需求給到之後,第一步肯定不是django-admin startproject,而是選型,換句話說,我們得先決定要不要使用django.它適合什麼樣的專案,不適合什麼樣的專案

避免websocket
Django出現的太早了,它甚至和ajax的年紀差沒幾年,所以對很多現代化的需求和功能支援都不太好,比如websocket,比如非同步任務.
如果需求中有一些功能會比較依賴websocket,那麼就建議不要選擇Django,並不是說完全不行,但是Django Channelasgi目前來說都算不上很成熟的解決方案,不得不承認在這方面django的確是沒有優勢.

避免對效能敏感的需求
這是老生常談了,我說一下我自己遇到的效能問題都出現在哪裡(不談惡俗的abtest).

服務端RSA加解密

這是我偶然發現的,python進行rsa運算簡直慢出天際.但是這個問題並不會非常明顯的爆發,畢竟只算一次的話,最多也就是零點幾秒甚至更少的時間,當訪問量不大的時候並沒有那麼明顯,很難發現.
但是有一天我使用多執行緒進行rsa加密的時候,發現我無論使用多少個執行緒,加密的速度都是一樣的慢,才算是發現這個問題.它很容易成為伺服器效能的瓶頸.
當然這個問題還是可以解決的.用Golang寫了一個加解密的模組,編譯成.so給python呼叫,這個問題就算解決了.

類似上傳Excel匯入資料

這是一個很複雜的情況.大檔案傳過來之後,究竟選擇什麼策略進行處理不能一概而論.比如前期我選擇直接讀出資料寫入到資料庫中,後來給我來了一個10W行的excel,直接GG了,python的迴圈操作是非常慢的,1W行的檔案已經需要載入很久了,這就導致服務端處理Excel並實時對資料進行格式檢查和去重並立即返回結果是很不合理的需求,當然不是說別的語言能做的完美,而是想說因為django不能直接處理非同步任務,完成類似需求就必須提高系統複雜度,引入訊息佇列和額外的worker才行,不划算.

實際上使用Django真正能遇到效能問題的時候不多,因為絕大部分產品不存在效能瓶頸,一個產品QPS能達到10,就已經美滋滋了,這個吞吐量Django完全沒問題,到了Django不行了那個時候換效能更好的方案完全不是問題.

我的通用選型

我所有使用Django開發的專案,架構上都差不多.

  • Nginx 沒什麼說的,配合gunicorn使用proxy_pass,方便可靠,省心.
  • gunicorn 效能優秀,使用簡單,可靠,完全對uwsgi沒興趣
  • Django
  • Redis 在python技術棧中天然扮演著快取和訊息佇列的雙重身份,與DjangoCelery配合完美.省一個系統元件.
  • Celery 彌補Django非同步任務缺陷,妙用無窮.
  • MySQL 其實postgreSQLDjango更搭,不過各個雲服務都是對MySQL支援更好一些.

這套東西我使用起來可以說並沒有遇到過解決不了的問題,不說無所不能,但是混口飯吃絕對是沒問題了.非常適合中小型專案快速開發試錯,基本上不需要開發基礎功能.

前100行程式碼

很久沒寫後端渲染的專案了,這裡都是前後端分離的內容

一個Django專案初始化之後,第一個要考慮的就是模組的劃分,app建立好之後,我一般會在settings.py那個專案同名資料夾下建立如下幾個檔案:

basic.py

這個檔案我用來定義一些基礎的元件,最常用的是這個.
繼承於HttpResponse,定義返回結構的類

import json

from django.core.serializers.json import DjangoJSONEncoder
from django.http import HttpResponse

from Myapp.settings import DEBUG 


class Response(HttpResponse):

    def __init__(self, data=None, msg="成功",
                 status_code=200, encoder=DjangoJSONEncoder,
                 json_dumps_params=None, **kwargs):
        if json_dumps_params is None:
            json_dumps_params = {}
        if DEBUG:
            # 開發模式增加縮排,方便人工檢查資料
            json_dumps_params["indent"] = 2

        json_dumps_params["ensure_ascii"] = False
        kwargs.setdefault(`content_type`, `application/json`)
        ret = dict(
            status_code=status_code,
            msg=msg,
        )
        if data:
            ret["data"] = data
        s = json.dumps(ret, cls=encoder, **json_dumps_params)
        super().__init__(content=s, **kwargs)

繼承DjangoHttpResponse類,參考(抄)了django.http.JsonResponse的結構.接收status_code,msg,data,避免在檢視函式中重複的構建返回結構,只需要關心業務資料就可以了.同時可以約束一起開發的人,避免不小心寫錯返回結構之類的事兒.與之配合的還有一個:
code.py

# 請求成功
OK = 200

# 資源不存在
NOT_FOUND = 404

這樣在構建返回結構的時候,直接

return Response(msg="資源不存在",status_code=NOT_FOUND)

前端接收到的返回就是統一格式的JSON,開發模式帶有縮排,生產環境DEBUG模式關掉就是普通JSON字串了

{
    "status_code":404,
    "msg":"資源不存在"
}

概括的說就是,將返回結構抽象成一個物件,並且統一管理返回碼,這樣在開發中能少些一些重複程式碼,節省精力,最重要的是方便維護.

自定義中介軟體:

Django本身繼承了模板引擎,預設開啟,但是現在流行的前後端分離方案下,模板是可以放棄的(但是初期的管理後臺我更喜歡用自帶的admin模組,可以通過配置兩套不同的settings.py檔案和manage.py,實現使用不同的專案配置).同樣,基於session的使用者驗證是否要繼續採用也是可以靈活選擇的.這裡我說一下我們是怎麼用JWT來代替session的.
Django本身是通過SessionMiddlewareAuthenticationMiddleware來管理session和使用者身份的.我們使用JWT代替session,那麼自然就要用一箇中介軟體來代替SessionMiddleware,AuthenticationMiddleware依賴於session同樣也就無法使用了.我們用下邊這個中介軟體來代替他們

class JwtMiddleware(object):

    def __init__(self, get_response=None):
        self.get_response = get_response
    def __call__(self, request, *args, **kwargs):
        auth_token = request.META.get("HTTP_AUTHORIZATION")
        try:
            _id = jwt.decode(auth_token, SECRET_KEY)["id"]
            user = User.objects.get(id=_id)

        except:
            user = AnonymousUser()
        request.user = user

        response = self.get_response(request, *args, **kwargs)
        return response

和前端約定在header中攜帶Authorization欄位,作為身份證明.這個token是登入介面簽發的,這麼做的好處:

  • 可以先不開發使用者系統,使用djangoadmin模組進行登陸,進行開發,這意味著使用者管理等模組和業務模組可以直接拆分,在兩個分支上開發
  • LoginRequiredMixin等框架自身依賴request.user介面的功能依然可用,當然類似的功能我們一般也自己寫了.不過在儘量不破壞框架的可用性的前提下進行自定義,是我們快速開發的基本原則之一.可以有效降低團隊溝通學習成本,文件都省了,業務邏輯部分完全不需要關注自定義之後的使用者鑑權方式.

值得注意的地方是,讀取使用者是一個資料庫操作,Django原生中介軟體使用了懶載入的模式來操作,這麼做的原因(我猜的)是,django作為框架,無法確定是否需要用到user物件,如果每次都載入會很蠢,所以採用懶載入,第一次載入的時候才獲取一次並快取user,避免第二次讀庫.我們的專案要求每次操作都要讀,並且只會讀一次,就不需要使用懶載入了.更好的設計是根據自己的需求選擇是直接讀取使用者還是使用懶載入讀取使用者資訊.

小結: 還有一個將請求body讀取成dict繫結到request的中介軟體,避免業務邏輯進行json.loads(request.body)的操作,因為這個是需要try..except..的操作,最少也需要四行程式碼才能做完.通過這些,算是約定了一個團隊內部的開發基本規則,保證大家寫的邏輯返回資料結構是可以統一維護的,定義的errcode都集中在一個檔案.業務邏輯中需要的資訊(request.user/request.json)都已提供.

資料邏輯與業務邏輯的劃分

我們寫業務邏輯的時候,可能會遇到將一個Model物件序列化為一個JSON的情況,大名鼎鼎的drfDjango restful framework就是做這個的,但是今天不提他了.

Model序列化為JSON的的程式碼,我通常是寫在Model中,哪怕他只用了一次我也習慣定義在Model中,View層只呼叫方法而不需要將資料分別取出進行構建.這樣我們View層的邏輯會顯得非常簡潔,同樣也更容易維護.

除了最終的序列化,Model很多時候還會出現計算屬性.

  • 內容已經發表了多久?
  • 優惠當前是否在可用時段?

這些如果寫在業務邏輯裡,難免要寫(datetime.datetime.now()-obj.publish_time).total_seconds()這種又臭又長的程式碼,可能還會在多個地方使用到.這些資料雖然不是直接寫在資料庫中,但依然屬於Model層該做的事兒,那麼我們就寫在Model就完了.配合@property,進一步保證了程式碼可讀性.

在專案外訪問ORM

假如現在我們需要做一個定時任務,定時從網上抓取資訊存入Django專案的資料庫中.我們可以選擇直接寫SQL語句插入,但是有些問題不太好處理.

  • 資料庫遷移,要多維護一個資料庫連線配置
  • 開發中手動維護SQL語句使其和Model行為一致

所以我推薦使用在定時任務中使用Django ORM,保證和專案內程式碼邏輯一致,降低維護成本.

官方文件

現在我們需要編寫一個獨立於專案執行的指令碼,官方給出的例子是:

import django
from django.conf import settings
from myapp import myapp_defaults

settings.configure(default_settings=myapp_defaults, DEBUG=True)
django.setup()

# Now this script or any imported module can use any part of Django it needs.
from myapp import models

我常用的是

import os
import django
os.environ.setdefault(`DJANGO_SETTINGS_MODULE`, `Myapp.settings`)
    django.setup()

其實都差不多,我也建議使用官方的方法,可以避免出現環境變數路徑不對的問題.
這個操作相當於載入了django專案的配置,在這之後就可以引用你想用的專案Model,進行必要的操作了.必須在django.setup()之後引入Model
通過這個方法,也可以不用django-celery了,配合處理非同步任務.

結尾

Python進行WEB開發的唯一優勢大概就是速度了.並不是說用python的人比用java的人快,而是說同一個人用python開發會更快一些.
使用Django而不是Flask或者Tornado也是為了開發快,迭代快.做這些邊邊角角的工作,做這些細節的東西,都是為了快.寫一行程式碼也許需要5秒鐘,改一行程式碼可能需要一整天.也許今天多花幾分鐘定義的Response可以避免和前端扯皮的導致衝突升級打架鬥毆住院一個月.還是非常值得的.自己看著自己寫的程式碼整潔乾淨,心情也更好不是?
https://luliangce.gitee.io/bl…

相關文章