視圖高級

目標

  • 限制請求method
  • 頁面重定向
  • HttpRequest 對象
  • HttpResponse 對象
  • 生產CVS 文件
  • 類視圖
  • 錯誤處理

一 限制請求method

1-1 經常使用的請求method

1. GET請求:GET請求通常用來向服務器索取數據,但不會向服務器提交數據,不會對服務器的狀態進行更改。好比向服務器獲取某篇文章的詳情。javascript

2. POST請求 :POST請求通常是用來向服務器提交數據,會對服務器的狀態進行更改。好比提交一篇文章給服務器。css

 

1-2 限制請求裝飾器

  Django內置的視圖裝飾器能夠給視圖提供一些限制。好比這個視圖只能經過GETmethod訪問等。如下將介紹一些經常使用的內置視圖裝飾器。html

1.  django.http.decorators.http.require_http_methods:這個裝飾器須要傳遞一個容許訪問的方法的列表。好比只能經過GET的方式訪問。那麼示例代碼以下:java

from django.views.decorators.http import require_http_methods

@require_http_methods(["GET"])
def my_view(request):
    pass

2. django.views.decorators.http.require_GET:這個裝飾器至關因而require_http_methods(['GET'])的簡寫形式,只容許使用GETmethod來訪問視圖。示例代碼以下:python

from django.views.decorators.http import require_GET

@require_GET
def my_view(request):
    pass

3. django.views.decorators.http.require_POST:這個裝飾器至關因而require_http_methods(['POST'])的簡寫形式,只容許使用POSTmethod來訪問視圖。示例代碼以下:nginx

from django.views.decorators.http import require_POST

@require_POST
def my_view(request):
    pass

4. django.views.decorators.http.require_safe:這個裝飾器至關因而require_http_methods(['GET','HEAD'])的簡寫形式,只容許使用相對安全的方式來訪問視圖。由於GETHEAD不會對服務器產生增刪改的行爲。所以是一種相對安全的請求方式。示例代碼以下:ajax

from django.views.decorators.http import require_safe

@require_safe
def my_view(request):
    pass

 

二 頁面重定向

  重定向分爲永久性重定向暫時性重定向,在頁面上體現的操做就是瀏覽器會從一個頁面自動跳轉到另一個頁面。好比用戶訪問了一個須要權限的頁面,可是該用戶當前並無登陸,所以咱們應該給他重定向到登陸頁面。django

  • 永久性重定向:http的狀態碼是301,多用於舊網址被廢棄了要轉到一個新的網址確保用戶的訪問,最經典的就是京東網站,你輸入www.jingdong.com的時候,會被重定向到www.jd.com,由於jingdong.com這個網址已經被廢棄了,被改爲jd.com,因此這種狀況下應該用永久重定向。
  • 暫時性重定向:http的狀態碼是302,表示頁面的暫時性跳轉。好比訪問一個須要權限的網址,若是當前用戶沒有登陸,應該重定向到登陸頁面,這種狀況下,應該用暫時性重定向。

  在Django中,重定向是使用redirect(to, *args, permanent=False, **kwargs)來實現的。to是一個urlpermanent表明的是這個重定向是不是一個永久的重定向,默認是False。關於重定向的使用。請看如下例子:json

from django.shortcuts import reverse,redirect
def profile(request):
    if request.GET.get("username"):
        return HttpResponse("%s,歡迎來到我的中心頁面!")
    else:
        return redirect(reverse("user:login"))

 

三 HttpRequest 對象

3-1 WSGIRequest 對象

  Django在接收到http請求以後,會根據http請求攜帶的參數以及報文信息建立一個WSGIRequest對象,而且做爲視圖函數第一個參數傳給視圖函數。也就是咱們常常看到的request參數。在這個對象上咱們能夠找到客戶端上傳上來的全部信息。這個對象的完整路徑是django.core.handlers.wsgi.WSGIRequest瀏覽器

3-1-1 WSGIRequest對象經常使用屬性:

  WSGIRequest對象上大部分的屬性都是隻讀的。由於這些屬性是從客戶端上傳上來的,不必作任何的修改。如下將對一些經常使用的屬性進行講解:

  • path:請求服務器的完整「路徑」,但不包含域名和參數。好比http://www.baidu.com/xxx/yyy/,那麼path就是/xxx/yyy/
  • method:表明當前請求的http方法。好比是GET仍是POST
  • GET:一個django.http.request.QueryDict對象。操做起來相似於字典。這個屬性中包含了全部以?xxx=xxx的方式上傳上來的參數。
  • POST:也是一個django.http.request.QueryDict對象。這個屬性中包含了全部以POST方式上傳上來的參數。
  • FILES:也是一個django.http.request.QueryDict對象。這個屬性中包含了全部上傳的文件。
  • COOKIES:一個標準的Python字典,包含全部的cookie,鍵值對都是字符串類型。
  • session:一個相似於字典的對象。用來操做服務器的session
  • META:存儲的客戶端發送上來的全部header信息。
  • CONTENT_LENGTH:請求的正文的長度(是一個字符串)。
  • HTTP_ACCEPT:響應可接收的Content-Type。
  • HTTP_ACCEPT_ENCODING:響應可接收的編碼。
  • HTTP_ACCEPT_LANGUAGE: 響應可接收的語言。
  • HTTP_HOST:客戶端發送的HOST值。
  • HTTP_REFERER:在訪問這個頁面上一個頁面的url。
  • QUERY_STRING:單個字符串形式的查詢字符串(未解析過的形式)。
  • REMOTE_ADDR:客戶端的IP地址。若是服務器使用了nginx作反向代理或者負載均衡,那麼這個值返回的是127.0.0.1,這時候可使用HTTP_X_FORWARDED_FOR來獲取,因此獲取ip地址的代碼片斷以下:
if request.META.has_key('HTTP_X_FORWARDED_FOR'):  
    ip =  request.META['HTTP_X_FORWARDED_FOR']  
else:  
    ip = request.META['REMOTE_ADDR']
  • REMOTE_HOST:客戶端的主機名。
  • REQUEST_METHOD:請求方法。一個字符串相似於GET或者POST。
  • SERVER_NAME:服務器域名。
  • SERVER_PORT:服務器端口號,是一個字符串類型。

3-1-2 WSGIRequest對象經常使用方法

  1. is_secure():是不是採用https協議。
  2. is_ajax():是否採用ajax發送的請求。原理就是判斷請求頭中是否存在X-Requested-With:XMLHttpRequest。
  3. get_host():服務器的域名。若是在訪問的時候還有端口號,那麼會加上端口號。好比www.baidu.com:9000。
  4. get_full_path():返回完整的path。若是有查詢字符串,還會加上查詢字符串。好比/music/bands/?print=True。
  5. get_raw_uri():獲取請求的完整url。

 

3-2 QueryDict 對象

  咱們平時用的request.GETrequest.POST都是QueryDict對象,這個對象繼承自dict,所以用法跟dict相差無幾。其中用得比較多的是get方法和getlist方法。

  1. get方法:用來獲取指定key的值,若是沒有這個key,那麼會返回None
  2. getlist方法:若是瀏覽器上傳上來的key對應的值有多個,那麼就須要經過這個方法獲取。

 

四 HttpResponse 對象

   Django服務器接收到客戶端發送過來的請求後,會將提交上來的這些數據封裝成一個HttpRequest對象傳給視圖函數。那麼視圖函數在處理完相關的邏輯後,也須要返回一個響應給瀏覽器。而這個響應,咱們必須返回HttpResponseBase或者他的子類的對象。而HttpResponse則是HttpResponseBase用得最多的子類。那麼接下來就來介紹一下HttpResponse及其子類。

4-1 經常使用屬性

  • content:返回的內容。
  • status_code:返回的HTTP響應狀態碼。
  • content_type:返回的數據的MIME類型,默認爲text/html。瀏覽器會根據這個屬性,來顯示數據。若是是text/html,那麼就會解析這個字符串,若是text/plain,那麼就會顯示一個純文本。經常使用的Content-Type以下
    • text/html(默認的,html文件)
    • text/plain(純文本)
    • text/css(css文件)
    • text/javascript(js文件)
    • multipart/form-data(文件提交)
    • application/json(json傳輸)
    • application/xml(xml文件)
  • 設置請求頭:response['X-Access-Token'] = 'xxxx'。

 

4-2 經常使用方法

  1. set_cookie:用來設置cookie信息。後面講到受權的時候會着重講到。
  2. delete_cookie:用來刪除cookie信息。
  3. writeHttpResponse是一個相似於文件的對象,能夠用來寫入數據到數據體(content)中。

 

4-3 JsonResponse 類

  用來對象dumpjson字符串,而後返回將json字符串封裝成Response對象返回給瀏覽器。而且他的Content-Typeapplication/json。示例代碼以下:

from django.http import JsonResponse
def index(request):
    return JsonResponse({"username":"zhiliao","age":18})

  默認狀況下JsonResponse只能對字典進行dump,若是想要對非字典的數據進行dump,那麼須要給JsonResponse傳遞一個safe=False參數。示例代碼以下:

from django.http import JsonResponse
def index(request):
    persons = ['張三','李四','王五']
    return HttpResponse(persons)

  以上代碼會報錯,應該在使用HttpResponse的時候,傳入一個safe=False參數,示例代碼以下:

return HttpResponse(persons,safe=False)

 

五 生產 CVS 文件

   有時候咱們作的網站,須要將一些數據,生成有一個CSV文件給瀏覽器,而且是做爲附件的形式下載下來。如下將講解如何生成CSV文件。

5-1 生成小的CSV文件

  這裏將用一個生成小的CSV文件爲例,來把生成CSV文件的技術要點講到位。咱們用Python內置的csv模塊來處理csv文件,而且使用HttpResponse來將csv文件返回回去。示例代碼以下:

import csv
from django.http import HttpResponse

def csv_view(request):
    response = HttpResponse(content_type='text/csv')
    response['Content-Disposition'] = 'attachment; filename="somefilename.csv"'

    writer = csv.writer(response)
    writer.writerow(['username', 'age', 'height', 'weight'])
    writer.writerow(['zhiliao', '18', '180', '110'])

    return response

  這裏再來對每一個部分的代碼進行解釋:

  1. 咱們在初始化HttpResponse的時候,指定了Content-Typetext/csv,這將告訴瀏覽器,這是一個csv格式的文件而不是一個HTML格式的文件,若是用默認值,默認值就是html,那麼瀏覽器將把csv格式的文件按照html格式輸出,這確定不是咱們想要的。
  2. 第二個咱們還在response中添加一個Content-Disposition頭,這個東西是用來告訴瀏覽器該如何處理這個文件,咱們給這個頭的值設置爲attachment;,那麼瀏覽器將不會對這個文件進行顯示,而是做爲附件的形式下載,第二個filename="somefilename.csv"是用來指定這個csv文件的名字。
  3. 咱們使用csv模塊的writer方法,將相應的數據寫入到response中。

5-2 將csv文件定義成模板

   咱們還能夠將csv格式的文件定義成模板,而後使用Django內置的模板系統,並給這個模板傳入一個Context對象,這樣模板系統就會根據傳入的Context對象,生成具體的csv文件。示例代碼以下:

模板文件:

{% for row in data %}"{{ row.0|addslashes }}", "{{ row.1|addslashes }}", "{{ row.2|addslashes }}", "{{ row.3|addslashes }}", "{{ row.4|addslashes }}"
{% endfor %}

視圖函數:

from django.http import HttpResponse
from django.template import loader, Context

def some_view(request):
    response = HttpResponse(content_type='text/csv')
    response['Content-Disposition'] = 'attachment; filename="somefilename.csv"'


    csv_data = (
        ('First row', 'Foo', 'Bar', 'Baz'),
        ('Second row', 'A', 'B', 'C', '"Testing"', "Here's a quote"),
    )

    t = loader.get_template('my_template_name.txt')
    response.write(t.render({"data": csv_data}))
    return response

 

5-3 生成大的CSV文件

   以上的例子是生成的一個小的csv文件,若是想要生成大型的csv文件,那麼以上方式將有可能會發生超時的狀況(服務器要生成一個大型csv文件,須要的時間可能會超過瀏覽器默認的超時時間)。這時候咱們能夠藉助另一個類,叫作StreamingHttpResponse對象,這個對象是將響應的數據做爲一個流返回給客戶端,而不是做爲一個總體返回。示例代碼以下:

class Echo:
    """
    定義一個能夠執行寫操做的類,之後調用csv.writer的時候,就會執行這個方法
    """
    def write(self, value):
        return value

def large_csv(request):
    rows = (["Row {}".format(idx), str(idx)] for idx in range(655360))
    pseudo_buffer = Echo()
    writer = csv.writer(pseudo_buffer)
    response = StreamingHttpResponse((writer.writerow(row) for row in rows),content_type="text/csv")
    response['Content-Disposition'] = 'attachment; filename="somefilename.csv"'
    return response

  這裏咱們構建了一個很是大的數據集rows,而且將其變成一個迭代器。而後由於StreamingHttpResponse的第一個參數只能是一個生成器,所以咱們使用圓括號(writer.writerow(row) for row in rows),而且由於咱們要寫的文件是csv格式的文件,所以須要調用writer.writerowrow變成一個csv格式的字符串。而調用writer.writerow又須要一箇中間的容器,所以這裏咱們定義了一個很是簡單的類Echo,這個類只實現一個write方法,之後在執行csv.writer(pseudo_buffer)的時候,就會調用Echo.writer方法。
注意:StreamingHttpResponse會啓動一個進程來和客戶端保持長鏈接,因此會很消耗資源。因此若是不是特殊要求,儘可能少用這種方法。

 

 5-4 關於StreamingHttpResponse

  這個類是專門用來處理流數據的。使得在處理一些大型文件的時候,不會由於服務器處理時間過長而到時鏈接超時。這個類不是繼承自HttpResponse,而且跟HttpResponse對比有如下幾點區別:

  1. 這個類沒有屬性content,相反是streaming_content。
  2. 這個類的streaming_content必須是一個能夠迭代的對象。
  3. 這個類沒有write方法,若是給這個類的對象寫入數據將會報錯。

注意:StreamingHttpResponse會啓動一個進程來和客戶端保持長鏈接,因此會很消耗資源。因此若是不是特殊要求,儘可能少用這種方法。

 

六 類視圖

  在寫視圖的時候,Django除了使用函數做爲視圖,也可使用類做爲視圖。使用類視圖可使用類的一些特性,好比繼承等。

6-1 View

  django.views.generic.base.View是主要的類視圖,全部的類視圖都是繼承自他。若是咱們寫本身的類視圖,也能夠繼承自他。而後再根據當前請求的method,來實現不一樣的方法。好比這個視圖只能使用get的方式來請求,那麼就能夠在這個類中定義get(self,request,*args,**kwargs)方法。以此類推,若是隻須要實現post方法,那麼就只須要在類中實現post(self,request,*args,**kwargs)。示例代碼以下:

from django.views import View
class BookDetailView(View):
    def get(self,request,*args,**kwargs):
        return render(request,'detail.html')

  類視圖寫完後,還應該在urls.py中進行映射,映射的時候就須要調用View的類方法as_view()來進行轉換。示例代碼以下:

urlpatterns = [        
    path("detail/<book_id>/",views.BookDetailView.as_view(),name='detail')
]

  除了get方法,View還支持如下方法['get','post','put','patch','delete','head','options','trace']

  若是用戶訪問了View中沒有定義的方法。好比你的類視圖只支持get方法,而出現了post方法,那麼就會把這個請求轉發給http_method_not_allowed(request,*args,**kwargs)。示例代碼以下:

class AddBookView(View):
    def post(self,request,*args,**kwargs):
        return HttpResponse("書籍添加成功!")

    def http_method_not_allowed(self, request, *args, **kwargs):
        return HttpResponse("您當前採用的method是:%s,本視圖只支持使用post請求!" % request.method)

  urls.py中的映射以下:

path("addbook/",views.AddBookView.as_view(),name='add_book')

  若是你在瀏覽器中訪問addbook/,由於瀏覽器訪問採用的是get方法,而addbook只支持post方法,所以以上視圖會返回您當前採用的method是:GET,本視圖只支持使用post請求!。

  其實不論是get請求仍是post請求,都會走dispatch(request,*args,**kwargs)方法,因此若是實現這個方法,將可以對全部請求都處理到。

 

6-2 TemplateView

  django.views.generic.base.TemplateView,這個類視圖是專門用來返回模版的。在這個類中,有兩個屬性是常常須要用到的,一個是template_name,這個屬性是用來存儲模版的路徑,TemplateView會自動的渲染這個變量指向的模版。另一個是get_context_data,這個方法是用來返回上下文數據的,也就是在給模版傳的參數的。示例代碼以下:

from django.views.generic.base import TemplateView

class HomePageView(TemplateView):

    template_name = "home.html"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['username'] = "黃勇"
        return context

  在urls.py中的映射代碼以下:

from django.urls import path

from myapp.views import HomePageView

urlpatterns = [
    path('', HomePageView.as_view(), name='home'),
]

  若是在模版中不須要傳遞任何參數,那麼能夠直接只在urls.py中使用TemplateView來渲染模版。示例代碼以下:

from django.urls import path
from django.views.generic import TemplateView

urlpatterns = [
    path('about/', TemplateView.as_view(template_name="about.html")),
]

 

6-3 ListView

  在網站開發中,常常會出現須要列出某個表中的一些數據做爲列表展現出來。好比文章列表,圖書列表等等。在Django中可使用ListView來幫咱們快速實現這種需求。示例代碼以下:

class ArticleListView(ListView):
    model = Article
    template_name = 'article_list.html'
    paginate_by = 10
    context_object_name = 'articles'
    ordering = 'create_time'
    page_kwarg = 'page'

    def get_context_data(self, **kwargs):
        context = super(ArticleListView, self).get_context_data(**kwargs)
        print(context)
        return context

    def get_queryset(self):
        return Article.objects.filter(id__lte=89)

  對以上代碼進行解釋:

  1. 首先ArticleListView是繼承自ListView
  2. model:重寫model類屬性,指定這個列表是給哪一個模型的
  3. template_name:指定這個列表的模板
  4. paginate_by:指定這個列表一頁中展現多少條數據
  5. context_object_name:指定這個列表模型在模板中的參數名稱
  6. ordering:指定這個列表的排序方式
  7. page_kwarg:獲取第幾頁的數據的參數名稱。默認是page
  8. get_context_data:獲取上下文的數據
  9. get_queryset:若是你提取數據的時候,並非要把全部數據都返回,那麼你能夠重寫這個方法。將一些不須要展現的數據給過濾掉

 

6-4 Paginator和Page類

   PaginatorPage類都是用來作分頁的。他們在Django中的路徑爲django.core.paginator.Paginatordjango.core.paginator.Page。如下對這兩個類的經常使用屬性和方法作解釋:

6-4-1 Paginator經常使用屬性和方法

  1. count:總共有多少條數據
  2. num_pages:總共有多少頁
  3. page_range:頁面的區間。好比有三頁,那麼就range(1,4)

6-4-2 Page經常使用屬性和方法

  1. has_next:是否還有下一頁
  2. has_previous:是否還有上一頁
  3. next_page_number:下一頁的頁碼
  4. previous_page_number:上一頁的頁碼
  5. number:當前頁
  6. start_index:當前這一頁的第一條數據的索引值
  7. end_index:當前這一頁的最後一條數據的索引值

6-4-3 分頁案例

1)定義視圖類

from django.views.generic import ListView,View
from django.core.paginator import Paginator,Page

class ArticleListView(ListView):
    model = Article
    template_name = 'article_list1.html'
    context_object_name = 'articles'
    paginate_by = 10
    ordering = 'create_time'
    page_kwarg = 'p'

    def get_context_data(self, **kwargs):
        context = super(ArticleListView, self).get_context_data(**kwargs)
        context['username'] = 'zhiliao'
        paginator = context.get('paginator')
        page_obj = context.get('page_obj')
        pagination_data = self.get_pagination_data(paginator, page_obj, 3)
        context.update(pagination_data)
        return context

    def get_pagination_data(self,paginator,page_obj,around_count=2):
        current_page = page_obj.number
        num_pages = paginator.num_pages

        left_has_more = False
        right_has_more = False

        if current_page <= around_count + 2:
            left_pages = range(1,current_page)
        else:
            left_has_more = True
            left_pages = range(current_page-around_count,current_page)

        if current_page >= num_pages - around_count - 1:
            right_pages = range(current_page+1,num_pages+1)
        else:
            right_has_more = True
            right_pages = range(current_page+1,current_page+around_count+1)

        return {
            'left_pages': left_pages,
            'right_pages': right_pages,
            'current_page': current_page,
            'left_has_more': left_has_more,
            'right_has_more': right_has_more,
            'num_pages': num_pages
        }

2)模板頁面

<ul>
        {% for article in articles %}
            <li>{{ article.title }}</li>
        {% endfor %}
        <ul class="pagination">
{#             上一頁#}
            {% if page_obj.has_previous %}
                <li><a href="{% url 'front:article_list' %}?p={{ page_obj.previous_page_number }}">上一頁</a></li>
            {% else %}
                <li class="disabled"><a href="javascript:void(0);">上一頁</a></li>
            {% endif %}

            {% if left_has_more %}
                <li><a href="{% url 'front:article_list' %}?p=1">1</a></li>
                <li><a href="javascript:void(0);">...</a></li>
            {% endif %}

            {# 左邊的頁碼 #}
            {% for left_page in left_pages %}
                <li><a href="{% url 'front:article_list' %}?p={{ left_page }}">{{ left_page }}</a></li>
            {% endfor %}

            {# 當前的頁面 #}
            <li class="active"><a href="{% url 'front:article_list' %}?p={{ current_page }}">{{ current_page }}</a></li>

            {# 右邊的頁碼 #}
            {% for right_page in right_pages %}
                <li><a href="{% url 'front:article_list' %}?p={{ right_page }}">{{ right_page }}</a></li>
            {% endfor %}

            {% if right_has_more %}
                <li><a href="javascript:void(0);">...</a></li>
                <li><a href="{% url 'front:article_list' %}?p={{ num_pages }}">{{ num_pages }}</a></li>
            {% endif %}

{#             下一頁#}
            {% if page_obj.has_next %}
                <li><a href="{% url 'front:article_list' %}?p={{ page_obj.next_page_number }}">下一頁</a></li>
            {% else %}
                <li class="disabled"><a href="javascript:void(0);">下一頁</a></li>
            {% endif %}

        </ul>
    </ul>

 

6-5 給類視圖添加裝飾器

   在開發中,有時候須要給一些視圖添加裝飾器。若是用函數視圖那麼很是簡單,只要在函數的上面寫上裝飾器就能夠了。可是若是想要給類添加裝飾器,那麼能夠經過如下兩種方式來實現:

6-5-1 裝飾dispatch方法

from django.utils.decorators import method_decorator

def login_required(func):
    def wrapper(request,*args,**kwargs):
        if request.GET.get("username"):
            return func(request,*args,**kwargs)
        else:
            return redirect(reverse('index'))
    return wrapper


class IndexView(View):
    def get(self,request,*args,**kwargs):
        return HttpResponse("index")

    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        super(IndexView, self).dispatch(request,*args,**kwargs)

6-5-2 直接裝飾在整個類上

from django.utils.decorators import method_decorator
def login_required(func):
    def wrapper(request,*args,**kwargs):
        if request.GET.get("username"):
            return func(request,*args,**kwargs)
        else:
            return redirect(reverse('login'))
    return wrapper


@method_decorator(login_required,name='dispatch')
class IndexView(View):
    def get(self,request,*args,**kwargs):
        return HttpResponse("index")

    def dispatch(self, request, *args, **kwargs):
        super(IndexView, self).dispatch(request,*args,**kwargs)

  

七 錯誤處理

  在一些網站開發中。常常會須要捕獲一些錯誤,而後將這些錯誤返回比較優美的界面,或者是將這個錯誤的請求作一些日誌保存。那麼咱們本節就來說講如何實現。

7-1 經常使用的錯誤碼

  1. 400bad request,請求的參數錯誤
  2. 403:沒有權限訪問相關的數據
  3. 404:服務器沒有指定的url
  4. 405:請求的method錯誤
  5. 500:服務器內部錯誤,通常是代碼出bug了
  6. 502:通常部署的時候見得比較多,通常是nginx啓動了,而後uwsgi有問題

 

7-2 自定義錯誤模板

  在碰到好比404500錯誤的時候,想要返回本身定義的模板。那麼能夠直接在templates文件夾下建立相應錯誤代碼的html模板文件。那麼之後在發生相應錯誤後,會將指定的模板返回回去。 

 

7-3 錯誤處理的解決方案

   對於404500這種自動拋出的錯誤。咱們能夠直接在templates文件夾下新建相應錯誤代碼的模板文件。而對於其餘的錯誤,咱們能夠專門定義一個app,用來處理這些錯誤。 

相關文章
相關標籤/搜索