前言
網路上關於Django的內容其實已經很多了,包括自己也是從網路上的內容一點一從零開始學習Django的,但是這部分內容都過於零散,可以說大部分都是在講怎麼從django-admin startproject
到建立Model
,再到寫一個TODO List或者一個Blog之類的內容,對於已經瞭解Django
的人來說,並沒有太好渠道去了解一些別人的實踐,在開發自己的專案時,遇到的需求很難去了解其他人會怎麼設計,當然這也是社群存在的通病,大家更多在討論語言層面的問題.
我希望能把自己平時在工作中總結出來的tips分享出來,也許在某些地方能幫助到別人,也算是自己梳理經驗,自我提高的一個過程.
既然標題是如何開發,那我就按照一個專案的生命週期來整理.
需求分析
討論需求的事情就不談了.
一個專案需求給到之後,第一步肯定不是django-admin startproject
,而是選型,換句話說,我們得先決定要不要使用django
.它適合什麼樣的專案,不適合什麼樣的專案
避免websocket
Django出現的太早了,它甚至和ajax的年紀差沒幾年,所以對很多現代化的需求和功能支援都不太好,比如websocket,比如非同步任務.
如果需求中有一些功能會比較依賴websocket
,那麼就建議不要選擇Django
,並不是說完全不行,但是Django Channel
和asgi
目前來說都算不上很成熟的解決方案,不得不承認在這方面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技術棧中天然扮演著快取和訊息佇列的雙重身份,與Django
和Celery
配合完美.省一個系統元件. -
Celery
彌補Django
非同步任務缺陷,妙用無窮. -
MySQL
其實postgreSQL
與Django
更搭,不過各個雲服務都是對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)
繼承Django
的HttpResponse
類,參考(抄)了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
本身是通過SessionMiddleware
和AuthenticationMiddleware
來管理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
是登入介面簽發的,這麼做的好處:
- 可以先不開發使用者系統,使用
django
的admin
模組進行登陸,進行開發,這意味著使用者管理等模組和業務模組可以直接拆分,在兩個分支上開發 -
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…