Django 全棧開發教程之03 - YaDjangoBlog 之前後端分離篇
0x00 前言
本文是 Django 全棧開發教程的第三篇 YaDjangoBlog 之前後端分離
目錄在這裡,已經更新的文章如下
- Django 全棧開發教程之目錄篇 - 2018 年不容錯過的 Django 全棧項目
- Django 全棧開發教程之01 - YaDjangoBlog 的開發環境配置
- Django 全棧開發教程之02 - YaDjangoBlog 的前後端設計
本文需要成四件事情:
- 第一件事情,解讀 DjangoRestFramework, 通過簡單的例子來引入用 DRF 的必要性,並且簡單介紹 DRF 的 CBV 實現。
- 第二件事情,簡單介紹 DRF 在本項目 YaDjangoBlog 中的使用
- 第三件事情,簡單聊聊 RESTFULAPI 規範,並給出最佳實踐參考。
- 第四件事情,簡單解讀一下 Django 處理請求流程代碼。
PS: 為了打字方便,下面的:
- DRF 指的是 DjangoRestFramework
- CBV 指的是 Class Based View
- FBV 指的是 Function Based View
坐穩了,開車了。
0x01 DjangorestFramework 解讀
為什麼要用 DRF 呢?
使用一個庫的原因,無非就是為了:
- 節省開發者自己造輪子的時間。
- 有利於代碼的可維護性 / 或者程序的健壯性。
具體落實到 DRF, 有哪些具體的優點呢?
- 可直接瀏覽調試的界面。讓前端調試起來欲罷不能的功能。
- 用 DRF 的方式快速批量開介面
- 分頁、序列化、校驗、登錄、許可權、Web 附加文檔、限流,高度的可擴展性。哪裡不爽擴展哪裡,so easy
- 算的上是 Django 社區最好的 RESTFUL 框架的輪子了。
- 完善的社區支持,比如 guardian/django-filter 等等結合。
不使用 DRF 應該如何寫 WebAPI 做呢?
我們先看看,不使用 DRF 的時代,API 是如何編寫的。
這裡我們用 function based view 來簡單說明。
# 最簡單版本def simple_hello(request): return JsonResponse({ "這就是 key": "這就是 value", "時間": time.time() })
剛開始學 DRF 的時候,我也有這種疑惑,這有必要需要一個 RESTFULAPI 的框架嘛?捋起袖子,JSON API 甩起來開咯。
之所以得出這個結論,是因為這個例子實在是過於簡單。
當涉及到一定複雜程度的 API 的時候,問題就來了:
- 許可權是否需要區分?
- 分頁需不需要做?
- 前端人員提交 Form 表單時,只能通過命令行或者是 POSTMAN 之類的工具提交參數,這會不會帶來不便?後端人員寫這些表單的各個欄位,也是很手酸的事情。
- 拼接字典或者是字元串倒也還好,能不能有個序列器幫我直接序列化這模型,並且如果模型和模型之間有聯繫,最好也可以幫我完成模型和模型之間的關聯。
- Profile API 應該如何做?
這都是我們需要考慮的。
如果不用 DRF, 而是由後端程序員直接寫這些代碼的話,也不是不行。
- 對於第一點,可以直接在 fbv 上面加裝飾器。
- 對於第二點,分頁的時候可以直接將邏輯寫在 fbv 裡面。
- 前端 er 直接使用 PostMan 之類的工具就好了。
- 序列化,可以藉助內置的序列化方法。
- Profile 可以在提交參數的時候,附加一個參數比如 debug, 渲染的時候,將使用 HTML 裡面內置一個 JSON 字元串的方式渲染出來。這樣的話,就可以使用 Django Debug Tools 進行 Profile 了。
很顯然,這是個系統性的活。 假如接下來還要考慮限流、RESTFULAPI 的設計,這就相當蛋疼了。
顯然,我們的 FBV 就會是這樣:
@a_authoritydef complex_hello(request): params = getParams(request) ..... query_results = SomeModels.some_query() ..... results = SomeModelsSerial(query_results) ..... return JsonResponse(results)
看起來似乎是有規律可循的,既然有規律可循,就能封裝一下,減輕負擔。FBV 已經這樣了,顯然只能每次都要硬編碼這些取參數,查詢,序列化。當然,如果用生成器也能簡化一部分函數代碼。yield 實現方法太丑還是棄用吧。
我們試試 CBV 看看如何。
# 繼承並重寫方法from django.views.generic import Viewclass APIView(View): def get(self,request): query_results = SomeModels.some_query() ..... results = SomeModelsSerial(query_results) ..... return results def post(self,request): query_results = SomeModels.some_query() ..... results = SomeModelsSerial(query_results) ..... return results ..... # 這裡相當於 view 函數 def dispatch(request, *args, **kwargs): # 這裡處理正式處理之前的邏輯,比如許可權判斷。 # 如果是 GET 方法,則調用 results = self.get(request, *args, **kwargs): # 這裡處理正式處理之後的邏輯,比如統計 list 的 total 值,加上時間戳 return JsonResponse(results)
於是,除了使用 FBV 進行硬編碼之外,還可以使用 CBV 的基類 進行擴展定製。
我們思考一下:
- 假如我想渲染某個模型的 JSON 列表,就可以定製一個 ListViewAPI 出來。如果需要一個 DetailViewAPI, 就定製一個 DetailViewAPI 出來。
- 我們再聲明一些 Permission 類,序列化類,模型,然後在 dispatch 中直接使用這些東西的話,就只需要在 get 和 post 裡面編寫一些最核心的邏輯了。
- 甚至,指定了分頁器和查詢,都完全不需要再 get 和 post 裡面寫代碼。
恭喜你,讀到這裡,你已經可以寫一個極簡的 DRF 出來了。
但寫成 DRF 這種量級的程序,還需要做很多很多事情。
DRF 處理請求的流程
要知道 DRF 的處理請求的流程,就要先知道 Django 的處理請求流程。
宏觀來看
- 請求先經過 MiddleWare , 接著判斷 urlconf (默認為 ROOT_URLCONF),
- 匹配 URL, 將請求上下文 dispatch 到具體的 view.
- 處理完畢,經過 MiddleWare
https://docs.djangoproject.com/en/2.0/topics/http/urls/
在本文的結尾的時候,我也將帶大家從源碼角度過一下,涉及到這個流程的相關的源碼。這裡先跳過。
那麼,DRF 是如何處理一個請求的呢?我們忽略路由之類的東西,直接看對應的 CBV 的源碼
class APIView(View): renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES parser_classes = api_settings.DEFAULT_PARSER_CLASSES authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS metadata_class = api_settings.DEFAULT_METADATA_CLASS versioning_class = api_settings.DEFAULT_VERSIONING_CLASS # ...... 其他方法 # Dispatch methods def initialize_request(self, request, *args, **kwargs): """ Returns the initial request object. """ parser_context = self.get_parser_context(request) return Request( request, parsers=self.get_parsers(), authenticators=self.get_authenticators(), negotiator=self.get_content_negotiator(), parser_context=parser_context ) def initial(self, request, *args, **kwargs): """ Runs anything that needs to occur prior to calling the method handler. """ self.format_kwarg = self.get_format_suffix(**kwargs) # Perform content negotiation and store the accepted info on the request neg = self.perform_content_negotiation(request) request.accepted_renderer, request.accepted_media_type = neg # Determine the API version, if versioning is in use. version, scheme = self.determine_version(request, *args, **kwargs) request.version, request.versioning_scheme = version, scheme # Ensure that the incoming request is permitted self.perform_authentication(request) self.check_permissions(request) self.check_throttles(request) # Note: Views are made CSRF exempt from within `as_view` as to prevent # accidental removal of this exemption in cases where `dispatch` needs to # be overridden. def dispatch(self, request, *args, **kwargs): """ `.dispatch()` is pretty much the same as Djangos regular dispatch, but with extra hooks for startup, finalize, and exception handling. """ self.args = args self.kwargs = kwargs # 這裡需要注意 request = self.initialize_request(request, *args, **kwargs) self.request = request self.headers = self.default_response_headers # deprecate? try: # 這裡需要注意 self.initial(request, *args, **kwargs) # Get the appropriate handler method if request.method.lower() in self.http_method_names: handler = getattr(self, request.method.lower(), self.http_method_not_allowed) else: handler = self.http_method_not_allowed response = handler(request, *args, **kwargs) except Exception as exc: response = self.handle_exception(exc) self.response = self.finalize_response(request, response, *args, **kwargs) return self.response
可以看出,當請求到達 dispatch 的時候,DRF 添加了一些鉤子函數,用於開始 / 結束 / 錯誤控制。
- 在 initialize_request 的時候,對 request 進行封裝,添加上 parser / auth / negoriator / parser context
- 接著在 initial 方法裡面校驗了版本,進行了認證和鑒權,檢查了限流
一看,其實與我們之前想封裝 APIView 的想法不謀而合,而我們只是想想,DRF 是詳細實現。
0x02 DjangorestFramework 的使用案例
如何開 WebAPI 介面
回到我們的 yadjangoblog 上面來。這個時候我們想開一個博文列表 API:
# 1. 定義序列器,用於序列化查詢的每一條。class BlogPostListSerializer(serializers.ModelSerializer): category = BlogCategorySerializer(read_only=True) tags = BlogTagSerializer(many=True, read_only=True) title = serializers.CharField() id = serializers.IntegerField() class Meta: model = BlogPost fields = (id, title, char_num, vote_num, category, tags, publish_date)# 2. 定義過濾器,可以通過過濾器進行查詢class BlogPostFilter(filters.FilterSet): title = filters.CharFilter(lookup_expr=contains) having_tags = filters.Filter(name="tags", lookup_expr=in) class Meta: model = BlogPost fields = (title, char_num, category, tags)# 3. 指定其他設置,具體大家看源碼就好了。class BlogPostListAPIView(generics.ListAPIView): """ 依照 category , tags , 時間 (年 / 月 / 日 年 / 月 年) """ queryset = BlogPost.objects.all() serializer_class = BlogPostListSerializer filter_backends = (filters.DjangoFilterBackend, OrderingFilter,) filter_class = BlogPostFilter ordering_fields = (publish_date,) ordering = (publish_date,) permission_classes = (permissions.AllowAny,) pagination_class = SmallResultsSetPagination
在指定上面的操作之後,一個介面就快速的開出來了。
當然,DRF 認認真真通讀一遍的話,還是可以給自己節省不少時間的。
這是開介面,前端應該如何使用介面呢
前端如何使用 WebAPI 介面
什麼是 CORS 可以參考阮一峰的文章 http://www.ruanyifeng.com/blog/2016/04/cors.html
在調試的時候,我們肯定是使用 ajax / fetch 方式請求。這就會遇到一個問題:
- 跨域
解決方式也很簡單,服務端只要伺服器實現了 CORS 介面,就可以跨源通信。
安裝 django-cors-headers, 並在 settings 中開啟 CORS_ORIGIN_ALLOW_ALL = True 即可。
這裡參考了臨書的解決方案,要感謝 @臨書 , 附上參考地址 https://zhuanlan.zhihu.com/p/24893786
對於本項目而言,使用了 axios 請求庫,直接 get 即可。詳細看前端代碼即可。
0x03 RESTFUL API 設計
開發過程中,盡量靠近 RESTFUL API 的設計,而不是照搬。
舉個其他領域的例子,有的人表述美就只有:
- 已擼
但是不同的美各有各的模樣:
- 手如柔荑,膚如凝脂,領如蝤蠐,齒如瓠犀,螓首蛾眉,巧笑倩兮,美目盼兮。
同樣,放在 RESFUL 的時候確實也出現了這種情況:
幾乎所有的業務邏輯最後會落實到數據表的 CURDE, 但是所有業務邏輯並不能完全使用 CRUDE 描述。
我們看下面的例子
關於請求
舉個例子,RESTFUL 適合純粹 CURDE 的設計風格。
比如,新增博客,更新博客,查詢博客,刪除博客,查看是否含有博客
但語義在某些場景下表述不足, 比如,設計訂單的時候,
URL: /api/v1/user/some_user/orders你查看訂單集合,這個好理解。get 方法你新增訂單,這個好理解。put 方法URL: /api/v1/user/some_user/order/xxxxxxx你刪除訂單,這個好理解。delete 方法你獲取訂單,這個好理解。get 方法你修改訂單,這個好理解。post 方法但修改訂單,有的時候可能會比較複雜,有可能是取消訂單,有可能是評價訂單,有可能是其他。而 RESTFUL 表達這種情況就有些語義不足了。
當然,個人經驗是,欄位越多,越難靠近 RESTFUL 規範
這個時候,就需要設計者做好 RESTFULAPI 的設計與語義化的平衡了。
關於響應
關於響應設計,主要有兩點需要注意:
- 狀態碼 (HTTP 狀態碼,也業務邏輯通用狀態碼)
- 響應內容 包含 業務邏輯通用狀態碼,剩下的視具體情況而定。
HTTP 狀態碼用於標記資源情況,比如:
200 表示獲取資源404 表示 NOT FOUND
但有時候也存在語義表達不足問題,一般前後端也會約定一個通用的狀態碼
通用狀態碼 錯誤信息 含義 HTTP 狀態碼999 unknow_v2_error 未知錯誤 4001000 need_permission 需要許可權 4031001 uri_not_found 資源不存在 4041002 missing_args 參數不全 4001003 image_too_large 上傳的圖片太大 400....
至於響應內容,一般都是見招拆招的。建議查看文章末尾的 Douban 的相關 API 規範來提升姿勢。
0x04 Django 的處理請求流程代碼解讀
這小節屬於一時興起寫的番外篇。和本文主體內容沒啥必要的關聯。不感興趣的可以直接跳轉到文章末尾點贊哈。
WSGI 全稱叫做 web 伺服器網關介面,通常情況下,gunicorn 或者 uwsgi 接收來自 nginx 轉發來的請求之後,向 web app 提供了環境信息(叫請求上下文會不會好些)以及一個 callback. 這樣的話,web app 就可以接收這個環境信息,處理完畢,通過回調函數處理請求,並返迴響應。一個極簡的 webapp 如下:
def app(environ, start_response): """Simplest possible application object""" data = Hello, World!
status = 200 OK response_headers = [ (Content-type,text/plain), (Content-Length, str(len(data))) ] start_response(status, response_headers) return iter([data])
現在我們看看 django 中是如何處理請求的。首先查看相關的 wsgi.py
# wsgi.pyimport osfrom django.core.wsgi import get_wsgi_applicationos.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")application = get_wsgi_application()# 接著查看 get_wsgi_applicationimport djangofrom django.core.handlers.wsgi import WSGIHandlerdef get_wsgi_application(): """ The public interface to Djangos WSGI support. Return a WSGI callable. Avoids making django.core.handlers.WSGIHandler a public API, in case the internal WSGI implementation changes or moves in the future. """ django.setup(set_prefix=False) return WSGIHandler()# 於是自然而言的看到了 WSGIHandlerclass WSGIHandler(base.BaseHandler): request_class = WSGIRequest def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.load_middleware() def __call__(self, environ, start_response): # 有木有看到 environ 和 start_response ?? 這就是極簡 web app 中的 webapp 核心方法。 set_script_prefix(get_script_name(environ)) signals.request_started.send(sender=self.__class__, environ=environ) request = self.request_class(environ) # 注意這一行,有請求處理邏輯 具體要見下面代碼 response = self.get_response(request) # ...... return response
嗯,看到了子類,就要看看基類
class BaseHandler: _request_middleware = None _view_middleware = None _template_response_middleware = None _response_middleware = None _exception_middleware = None _middleware_chain = None def load_middleware(self): """ 註冊 MiddleWare, 並賦值 _middleware_chain 方法,使之調用的時候可以先按照順序從 setting 的 middleware 裡面處理 requests 並在處理 request 的最後調用 私有方法 _get_response """ self._request_middleware = [] self._view_middleware = [] self._template_response_middleware = [] self._response_middleware = [] self._exception_middleware = [] handler = convert_exception_to_response(self._get_response) # 注意,這裡面是倒著來的 代碼中越在前面,實際運行的時候處理就越在後面 for middleware_path in reversed(settings.MIDDLEWARE): # 依次添加 view middleware / template middleware / exception middleware middleware = import_string(middleware_path) mw_instance = middleware(handler) handler = convert_exception_to_response(mw_instance) # We only assign to this when initialization is complete as it is used # as a flag for initialization being complete. self._middleware_chain = handler ..... def get_response(self, request): """Return an HttpResponse object for the given HttpRequest.""" # Setup default url resolver for this thread set_urlconf(settings.ROOT_URLCONF) response = self._middleware_chain(request) # ...... return response def _get_response(self, request): """ Resolve and call the view, then apply view, exception, and template_response middleware. This method is everything that happens inside the request/response middleware. """ response = None # 1. 接著判斷 urlconf (默認為 ROOT_URLCONF), 可以通過 middleware 進行設置 if hasattr(request, urlconf): urlconf = request.urlconf set_urlconf(urlconf) resolver = get_resolver(urlconf) else: resolver = get_resolver() resolver_match = resolver.resolve(request.path_info) callback, callback_args, callback_kwargs = resolver_match request.resolver_match = resolver_match # Apply view middleware.... # 注意,這個就是 view 函數 wrapped_callback = self.make_view_atomic(callback) response = wrapped_callback(request, *callback_args, **callback_kwargs) # Complain if the view returned None (a common error). return response def process_exception_by_middleware(self, exception, request): # ......
上面代碼比較表達的意思比較簡單,值得注意的地方我都加了注釋。
需要特別注意的就是 middleware_chain 這個屬性(實際上是一個方法), 正是這個方法使得註冊的 middleware (在 load_middleware 方法里)可以在 fbv 或者 cbv 處理 request 之前,通過對 request 進行處理。
0xEE. 參考鏈接
還猶豫啥,Django 前後端分離最佳實踐,點贊後,快上車吧
- 前端代碼 https://github.com/twocucao/YaVueBlog
- 後端代碼 https://github.com/twocucao/YaDjangoBlog
- 擴展閱讀之 douban restful api 設計 https://developers.douban.com/wiki/?title=api_v2
- Photo by Caspar Rubin on Unsplash
ChangeLog:
- 2018-02-22 開啟本文
- 2018-03-04 重修文字
推薦閱讀:
※LocalNote,讓你像使用本地文件一樣使用印象筆記(支持 markdown 格式)
※黃哥Python 所寫"windows下python學習環境設置"
※Python 閉包代碼理解?
※為什麼選擇Python
※怎樣自學Python?