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 呢?

使用一個庫的原因,無非就是為了:

  1. 節省開發者自己造輪子的時間。
  2. 有利於代碼的可維護性 / 或者程序的健壯性。

具體落實到 DRF, 有哪些具體的優點呢?

  1. 可直接瀏覽調試的界面。讓前端調試起來欲罷不能的功能。
  2. 用 DRF 的方式快速批量開介面
  3. 分頁、序列化、校驗、登錄、許可權、Web 附加文檔、限流,高度的可擴展性。哪裡不爽擴展哪裡,so easy
  4. 算的上是 Django 社區最好的 RESTFUL 框架的輪子了。
  5. 完善的社區支持,比如 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 的時候,問題就來了:

  1. 許可權是否需要區分?
  2. 分頁需不需要做?
  3. 前端人員提交 Form 表單時,只能通過命令行或者是 POSTMAN 之類的工具提交參數,這會不會帶來不便?後端人員寫這些表單的各個欄位,也是很手酸的事情。
  4. 拼接字典或者是字元串倒也還好,能不能有個序列器幫我直接序列化這模型,並且如果模型和模型之間有聯繫,最好也可以幫我完成模型和模型之間的關聯。
  5. Profile API 應該如何做?

這都是我們需要考慮的。

如果不用 DRF, 而是由後端程序員直接寫這些代碼的話,也不是不行。

  1. 對於第一點,可以直接在 fbv 上面加裝飾器。
  2. 對於第二點,分頁的時候可以直接將邏輯寫在 fbv 裡面。
  3. 前端 er 直接使用 PostMan 之類的工具就好了。
  4. 序列化,可以藉助內置的序列化方法。
  5. 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 的基類 進行擴展定製。

我們思考一下:

  1. 假如我想渲染某個模型的 JSON 列表,就可以定製一個 ListViewAPI 出來。如果需要一個 DetailViewAPI, 就定製一個 DetailViewAPI 出來。
  2. 我們再聲明一些 Permission 類,序列化類,模型,然後在 dispatch 中直接使用這些東西的話,就只需要在 get 和 post 裡面編寫一些最核心的邏輯了。
  3. 甚至,指定了分頁器和查詢,都完全不需要再 get 和 post 裡面寫代碼。

恭喜你,讀到這裡,你已經可以寫一個極簡的 DRF 出來了。

但寫成 DRF 這種量級的程序,還需要做很多很多事情。

DRF 處理請求的流程

要知道 DRF 的處理請求的流程,就要先知道 Django 的處理請求流程。

宏觀來看

  1. 請求先經過 MiddleWare , 接著判斷 urlconf (默認為 ROOT_URLCONF),
  2. 匹配 URL, 將請求上下文 dispatch 到具體的 view.
  3. 處理完畢,經過 MiddleWare

docs.djangoproject.com/

在本文的結尾的時候,我也將帶大家從源碼角度過一下,涉及到這個流程的相關的源碼。這裡先跳過。

那麼,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 添加了一些鉤子函數,用於開始 / 結束 / 錯誤控制。

  1. 在 initialize_request 的時候,對 request 進行封裝,添加上 parser / auth / negoriator / parser context
  2. 接著在 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 可以參考阮一峰的文章 ruanyifeng.com/blog/201

在調試的時候,我們肯定是使用 ajax / fetch 方式請求。這就會遇到一個問題:

  • 跨域

解決方式也很簡單,服務端只要伺服器實現了 CORS 介面,就可以跨源通信。

安裝 django-cors-headers, 並在 settings 中開啟 CORS_ORIGIN_ALLOW_ALL = True 即可。

這裡參考了臨書的解決方案,要感謝 @臨書 , 附上參考地址 zhuanlan.zhihu.com/p/24

對於本項目而言,使用了 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 前後端分離最佳實踐,點贊後,快上車吧

  • 前端代碼 github.com/twocucao/YaV
  • 後端代碼 github.com/twocucao/YaD
  • 擴展閱讀之 douban restful api 設計 developers.douban.com/w
  • Photo by Caspar Rubin on Unsplash

ChangeLog:

  • 2018-02-22 開啟本文
  • 2018-03-04 重修文字

推薦閱讀:

LocalNote,讓你像使用本地文件一樣使用印象筆記(支持 markdown 格式)
黃哥Python 所寫"windows下python學習環境設置"
Python 閉包代碼理解?
為什麼選擇Python
怎樣自學Python?

TAG:Vuejs | Python | Django框架 |