深刻解析當下大熱的先後端分離組件django-rest_framework系列一

 Nodejs的逐漸成熟和日趨穩定,使得愈來愈多的公司開始嘗試使用Nodejs來練一下手,嘗一嚐鮮。在傳統的web應用開發中,大多數的程序員會將瀏覽器做爲先後端的分界線。將瀏覽器中爲用戶進行頁面展現的部分稱之爲前端,而將運行在服務器,爲前端提供業務邏輯和數據準備的全部代碼統稱爲後端html

      先後端分離是web應用的一種架構模式。在開發階段,先後端工程師約定好數據交互接口,實現並行開發和測試;在運行階段先後端分離模式須要對web應用進行分離部署,先後端之間使用HTTP或者其餘協議進行交互請求。在先後端分離架構中,後端只須要負責按照約定的數據格式向前端提供可調用的API服務便可。先後端之間經過HTTP請求進行交互,前端獲取到數據後,進行頁面的組裝和渲染,最終返回給瀏覽器。前端

從目前應用軟件開發的發展趨勢來看,主要有兩方面須要注意:python

  1. 愈來愈注重用戶體驗,隨着互聯網的發展,開始多終端化。程序員

  2. 大型應用架構模式正在向雲化、微服務化發展。web

咱們主要經過先後端分離架構,爲咱們帶來如下四個方面的提高:django

  • 爲優質產品打造精益團隊
    經過將開發團隊先後端分離化,讓先後端工程師只須要專一於前端或後端的開發工做,是的先後端工程師實現自治,培養其獨特的技術特性,而後構建出一個全棧式的精益開發團隊。json

  • 提高開發效率
    先後端分離之後,能夠實現先後端代碼的解耦,只要先後端溝通約定好應用所需接口以及接口參數,即可以開始並行開發,無需等待對方的開發工做結束。與此同時,即便需求發生變動,只要接口與數據格式不變,後端開發人員就不須要修改代碼,只要前端進行變更便可。如此一來整個應用的開發效率必然會有質的提高。後端

  • 完美應對複雜多變的前端需求
    若是開發團隊能完成先後端分離的轉型,打造優秀的先後端團隊,開發獨立化,讓開發人員作到專一專精,開發能力必然會有所提高,可以完美應對各類複雜多變的前端需求。api

  • 加強代碼可維護性
    先後端分離後,應用的代碼再也不是先後端混合,只有在運行期纔會有調用依賴關係。    瀏覽器

  應用代碼將會變得整潔清晰,不管是代碼閱讀仍是代碼維護都會比之前輕鬆。

rest_framework的簡單瞭解  

  在Django中,有一個個的應用(app),好比admin、form、contenttype等,rest_framework也是一個應用,只不過這個應用是基於restful協議實現的。經過一些接口實現對數據快速的增刪改查。這套應用主要是經過發送不一樣的請求方式,來實現對數據的增刪改查。

  以book爲例:

  url                                              請求方式                                                 相關操做

 /books/                                             get                                                        查看書籍

 /books/                                            post                                                       添加書籍

   /books/1/                                          put/patch                                               修改書籍

  /books/1/                                          delete                                                     刪除書籍

備註:put與patch請求的區別:都是編輯修改,put是總體修改,patch是局部的修改。

這樣的設計風格是應用於CBV的(class based view),因此在瞭解rest_framework以前,咱們先了解Django中是如何經過CBV實現路由的分發。

Django中CBV的內部實現流程

  首先,從路由出發,在每個url後面,對應的會執行一個as_view()的類方法,在Django啓動的時候 ,會直接執行,返回一個視圖函數。

#cbv形式的url

urlpatterns = [
        url(r'^login/',views.Login.as_view()),
]

  首先,從Login這個類中找as_view()方法,沒有就從父類中找,繼承View,在父類中有一個as_view()的類方法。

複製代碼
class View(object):
    """
    Intentionally simple parent class for all views. Only implements
    dispatch-by-method and simple sanity checking.
    """

    http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']

    def __init__(self, **kwargs):
        """
        Constructor. Called in the URLconf; can contain helpful extra
        keyword arguments, and other things.
        """
        # Go through keyword arguments, and either save their values to our
        # instance, or raise an error.
        for key, value in six.iteritems(kwargs):
            setattr(self, key, value)

    @classonlymethod
    def as_view(cls, **initkwargs):
        """
        Main entry point for a request-response process.
        """
        for key in initkwargs:
            if key in cls.http_method_names:
                raise TypeError("You tried to pass in the %s method name as a "
                                "keyword argument to %s(). Don't do that."
                                % (key, cls.__name__))
            if not hasattr(cls, key):
                raise TypeError("%s() received an invalid keyword %r. as_view "
                                "only accepts arguments that are already "
                                "attributes of the class." % (cls.__name__, key))

        def view(request, *args, **kwargs):
            self = cls(**initkwargs)
            if hasattr(self, 'get') and not hasattr(self, 'head'):
                self.head = self.get
            self.request = request
            self.args = args
            self.kwargs = kwargs
            return self.dispatch(request, *args, **kwargs)
        view.view_class = cls
        view.view_initkwargs = initkwargs

        # take name and docstring from class
        update_wrapper(view, cls, updated=())

        # and possible attributes set by decorators
        # like csrf_exempt from dispatch
        update_wrapper(view, cls.dispatch, assigned=())
        return view
複製代碼

  咱們看看as_view()中都實現了什麼?返回一個view,view是as_view()內部的一個函數,因此當用戶訪問url時,直接執行這個view(),返回一個self.dispatch(request, *args, **kwargs),首先在本類中找,沒有這樣一個方法,從父類View中找,

複製代碼
class View(object):
    def dispatch(self, request, *args, **kwargs):
        # Try to dispatch to the right method; if a method doesn't exist,
        # defer to the error handler. Also defer to the error handler if the
        # request method isn't on the approved list.
        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
        return handler(request, *args, **kwargs)
複製代碼

咱們來瞅瞅這個dispatch方法中都幹了什麼?

  首先,先判斷請求方式在不在http_method_names這個列表中,若是在,就經過反射,獲得一個handler(以請求方式小寫的形式的函數),而後調用這個handler(),因此,在View函數中,dispatch方法實現了按照請求方式分發的功能。

  咱們發送get請求,對應的就執行類中的get()方法,也就是發送什麼請求,就對應執行對應的方法。經過這種方式,咱們直接在對應的方法下實現對應的視圖函數便可。

  這就是django內部的視圖類的路由分發的實現方式,說這麼多,對於咱們的rest_framework有什麼用呢?很直白的告訴你,rest_framework就是基於這種方式二次封裝,從而實現經過請求方式來對對應數據的增刪改查。

rest_framework的準備工做

  瞭解rest_framework以前,首先,要下載rest_framework:

pip install djangorestframework

既然是Django的一個應用,就要先將這個應用添加到settings.py中的INSTALLED_APPS中:

INSTALLED_APPS = (
    ...
    'rest_framework',
)

 

使用rest_framework快速實現一個簡單的增刪改查

在深刻了解rest_framework以前,咱們先快速實現一個基於rest_framework的示例:

urls.py

複製代碼
from django.conf.urls import url, include
from rest_framework import routers

from framework import views

# Routers provide an easy way of automatically determining the URL conf.
router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet)
router.register(r'books',views.BookViewSet)

# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
    url(r'^', include(router.urls)),
    url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]
複製代碼

 

app下的models.py

複製代碼
from django.db import models

# Create your models here.

class Book(models.Model):
    title = models.CharField(max_length=32)

    def __str__(self):
        return self.title
複製代碼

 

app下的views.py

複製代碼
from django.contrib.auth.models import User
from rest_framework import serializers, viewsets
from framework import models
# Serializers define the API representation.
class UserSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = User
        fields = ('url', 'username', 'email', 'is_staff')

# ViewSets define the view behavior.
class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer


# Book 模型
class BookSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = models.Book
        fields = ['url','title']


class BookViewSet(viewsets.ModelViewSet):
    queryset = models.Book.objects.all()
    serializer_class = BookSerializer
複製代碼

  這樣,咱們就實現了一個書籍的增刪改查,啓動Django,訪問http://127.0.0.1:8000/,就會出現下面這個頁面:

  固然,這個頁面不是咱們寫的,是rest_framework內置的一個頁面。這是rest_framework提供給咱們用做測試的頁面。

 

  小技巧:訪問rest_framework中對應的url時,在路徑後面拼上?format=json,咱們能夠獲得該數據的一個json字符串形式的列表。

  使用rest_framework,咱們並無返回一個HTML頁面,只是返回了一堆json格式的字符串。沒錯,這就是restful協議的實現方式,經過不一樣的請求返回固定格式的數據,從而實現先後端分離,這就是restful所要實現的。

  django的rest_framework組件實現了先後端的分離,後端只需提供相應接口便可,前端發送什麼請求,就返回什麼數據,從而使得先後端各司其職。

  使用rest_framework在後端的開發中,確定會發送響應的請求測試數據,這裏,咱們藉助一個很NB的插件:Postman

  

  經過rest_framework,能夠快速的實現對一個模型表的增刪改查,那它內部到底都幫咱們作了哪些事情?這個應用都實現了哪些功能呢?

組件1、APIView

首先,先導入

from rest_framework.views import APIView

看看這個APIView都實現了什麼?

複製代碼
class APIView(View):


    @classmethod
    def as_view(cls, **initkwargs):
        """
        Store the original class on the view function.

        This allows us to discover information about the view when we do URL
        reverse lookups.  Used for breadcrumb generation.
        """
        if isinstance(getattr(cls, 'queryset', None), models.query.QuerySet):
            def force_evaluation():
                raise RuntimeError(
                    'Do not evaluate the `.queryset` attribute directly, '
                    'as the result will be cached and reused between requests. '
                    'Use `.all()` or call `.get_queryset()` instead.'
                )
            cls.queryset._fetch_all = force_evaluation

        view = super(APIView, cls).as_view(**initkwargs)
        view.cls = cls
        view.initkwargs = initkwargs

        # Note: session based authentication is explicitly CSRF validated,
        # all other authentication is CSRF exempt.
        return csrf_exempt(view)
複製代碼

  很明顯,APIView繼承了View,而且從新封裝了as_view()方法,返回了view,這麼一看,好像跟 父類View中的as_view()方法沒什麼特大的區別,只是作了一些擴展。rest_framework真正NB的地方是下面這個方法的封裝,在父類View中,view返回self.dispatch(),APIView重寫了dispatch方法,意義重大。

複製代碼
class APIView(View):
    def dispatch(self, request, *args, **kwargs):
        """
        `.dispatch()` is pretty much the same as Django's 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
複製代碼

備註:在try中的self.initial(request,...)執行了auth組件,權限組件,頻率組件。

  View Code

這個dispatch方法中,try裏面的內容跟父類中view的實現如出一轍,也是實現了一個分發,沒有多大的意義,可是,在try上面,

request = self.initialize_request(request, *args, **kwargs)
self.request = request

這兩行代碼,重中之重,實現了對request方法的封裝和重寫。

  爲何要重寫request呢?

  由於在Django中,信息的解析與封裝是經過wsgiref這個模塊實現的,這個模塊有一個缺陷。封裝的request.POST方法不支持json格式的字符串,也就是說這個模塊中沒有提供json格式的解析器。而先後端的分離,就須要一種先後端都支持的數據格式,那就是json,而這剛好是wsgiref所沒法實現的,要實現先後端分離,必需要對request方法重寫。

  rest_framework是如何實現request的重寫的。

那咱們看看self.initialize_request(request, *args, **kwargs)裏面都實現了那些東西?

複製代碼
from rest_framework.request import Request

class APIView(View):
    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
        )
複製代碼

  這個方法return了一個類Request的實例對象,這個Request類傳入舊的request,對舊的equest進行了一系列的加工,返回新的request對象

複製代碼
class Request(object):
    """
    Wrapper allowing to enhance a standard `HttpRequest` instance.

    Kwargs:
        - request(HttpRequest). The original request instance.
        - parsers_classes(list/tuple). The parsers to use for parsing the
          request content.
        - authentication_classes(list/tuple). The authentications used to try
          authenticating the request's user.
    """

    def __init__(self, request, parsers=None, authenticators=None,
                 negotiator=None, parser_context=None):
        assert isinstance(request, HttpRequest), (
            'The `request` argument must be an instance of '
            '`django.http.HttpRequest`, not `{}.{}`.'
            .format(request.__class__.__module__, request.__class__.__name__)
        )

        self._request = request
        self.parsers = parsers or ()
        self.authenticators = authenticators or ()
        self.negotiator = negotiator or self._default_negotiator()
        self.parser_context = parser_context
        self._data = Empty
        self._files = Empty
        self._full_data = Empty
        self._content_type = Empty
        self._stream = Empty
複製代碼

  初始化的過程當中,咱們能夠獲得一些對咱們有用的信息,咱們能夠經過這個類Request的實例對象,也就是新的request,經過

request._request 就能夠獲得咱們舊的request。

複製代碼
    
class Request(object):

   @property def query_params(self): """ More semantically correct name for request.GET. """ return self._request.GET @property def data(self): if not _hasattr(self, '_full_data'): self._load_data_and_files() return self._full_data
複製代碼

  因此,咱們能夠經過這個新的request.query_params 獲得舊的request.GET的值(這個方法沒啥卵用),最關鍵的是request.data這個方式,能夠獲得序列化後的數據,這個方法補全了舊request的不足。

  備註:在request.data中,提供了不少數據類型的解析器,包括json的,因此,對於提交的數據,咱們能夠直接經過這個方法獲取到。而不須要經過request.POST。

  那麼咱們接下來講說rest_framework的下一個組件-------序列化組件

組件2、serializers

  開發咱們的Web API的第一件事是爲咱們的Web API提供一種將代碼片斷實例序列化和反序列化爲諸如json之類的表示形式的方式。

  咱們在使用json序列化時,有這麼幾種方式:

  第一種,藉助Python內置的模塊j。手動構建一個字典,經過json.dumps(obj)獲得一個json格式的字符串,使用這種方式有必定的侷限性,那就是咱們沒法控制這張表中的字段,有多少個字段,咱們就須要添加多少個鍵值對,一旦後期表結構發生變化,就會很麻煩。

  第二種方式:解決上述方式中字段的不可控性,就須要藉助Django中內置的一個方法model_to_dict,(from django.forms.models import model_to_dict),咱們能夠將取出的全部的字段循環,依次將每一個對象傳入model_to_dict中,這樣就解決了字段的問題。

  還有一種方式,也是Django提供的一個序列化器:from django.core.serializers import serialize   ,咱們能夠直接將queryset數據類型直接傳進去,而後指定咱們要轉化的格式便可,這兩種Django提供給咱們的方法雖然能夠解決這個序列化字段的問題,可是有一個缺點,那就是咱們能夠直接將一個數據轉化爲json字符串形式,可是卻沒法反序列化爲queryset類型的數據。

  rest_framework提供了一個序列化組件---serializers,完美的幫助咱們解決了上述的問題:from rest_framework import serializers ,用法跟Django中forms組件的用法很是類似,也須要先自定製一個類,而後這個類必須繼承serializers.Serializer,而後咱們須要序列化那些字段就在這個類中配置那些字段,還能夠自定製字段的展現格式,很是的靈活。

models部分:

複製代碼
複製代碼
from django.db import models

# Create your models here.


class Book(models.Model):
    title=models.CharField(max_length=32)
    price=models.IntegerField()
    pub_date=models.DateField()
    publish=models.ForeignKey("Publish")
    authors=models.ManyToManyField("Author")
    def __str__(self):
        return self.title

class Publish(models.Model):
    name=models.CharField(max_length=32)
    email=models.EmailField()
    def __str__(self):
        return self.name

class Author(models.Model):
    name=models.CharField(max_length=32)
    age=models.IntegerField()
    def __str__(self):
        return self.name
複製代碼
複製代碼

views部分:

複製代碼
複製代碼
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import *
from django.shortcuts import HttpResponse
from django.core import serializers


from rest_framework import serializers

class BookSerializers(serializers.Serializer):
    title=serializers.CharField(max_length=32)
    price=serializers.IntegerField()
    pub_date=serializers.DateField()
    publish=serializers.CharField(source="publish.name")
    #authors=serializers.CharField(source="authors.all")
    authors=serializers.SerializerMethodField()
    def get_authors(self,obj):
        temp=[]
        for author in obj.authors.all():
            temp.append(author.name)
        return temp


class BookViewSet(APIView):

    def get(self,request,*args,**kwargs):
        book_list=Book.objects.all()
        # 序列化方式1:
        # from django.forms.models import model_to_dict
        # import json
        # data=[]
        # for obj in book_list:
        #     data.append(model_to_dict(obj))
        # print(data)
        # return HttpResponse("ok")

        # 序列化方式2:
        # data=serializers.serialize("json",book_list)
        # return HttpResponse(data)

        # 序列化方式3:
        bs=BookSerializers(book_list,many=True)
        return Response(bs.data)
複製代碼
複製代碼

  值得注意的時,咱們使用這種方式序列化時,須要先實例化一個對象,而後在傳值時,若是值爲一個queryset對象時,須要指定一個參數many=True,若是值爲一個obj時,不須要指定many=False,默認爲False。

複製代碼
#咱們自定義一個類Bookserialize,繼承serializers.Serializer

from framework.views import Bookserialize
from framework import models
book_list = models.Book.objects.all()
bs= Bookserialize(book_list,many=True)
bs.data
[OrderedDict([('title', '神墓')]), OrderedDict([('title', '完美世界')])]
# queryset對象  結果爲一個列表,裏面放着每個有序字典
book_obj = models.Book.objects.first()
bs_ = Bookserialize(book_obj)
bs_.data
{'title': '神墓'}
# 若是爲一個對象時,結果爲一個字典
複製代碼

特別須要注意的是:使用這種方式序列化時,對於特殊字段(一對多ForeignKey、多對多ManyToMany),serializers沒有提供對應的字段,須要指定特殊的方式,由於obj.這個字段時,獲得的是一個對象,因此咱們對於FK,須要使用一個CharField字段,而後在這個字段中指定一個source屬性,指定顯示這個對象的那個字段。一樣的,對於多對多的字段,咱們也要使用特殊的顯示方式:SerializerMethodField(),指定爲這種字段類型時,顯示的結果爲一個自定義的函數的返回值,這個自定義函數的名字必須是get_字段名,固定寫法,接收一個obj對象,返回值就是該字段在序列化時的顯示結果。

  另外,咱們在取值時,直接經過這個對象.data的方式取值,這是rest_framework提供給咱們的序列化接口。

  其實。咱們應該明白它內部的實現方式:若是值爲一個queryset對象,就建立一個list,循環這個queryset獲得每個數據對象,而後在循環配置類下面的每個字段,直接這個對象obj.字段  得出值,添加到一個字典中,在將這個字典添加到這個列表中。因此,對於這些特殊字段,咱們取值時,經過這種方式獲得的是一個對象。

  經過這種方式就會出現一個問題,咱們每序列化一個表,就要將這個表中的字段所有寫一遍,這樣顯得很麻煩。在Djangoforms組件中,有一個ModelForm,能夠幫咱們將咱們模型表中的全部字段轉化爲forms組件中對應的字段,一樣的,在serializers中,一樣有一個,能夠幫咱們將咱們的模型類轉化爲serializers中對應的字段。這個組件就是ModelSerializer。

組件3、ModelSerializer

class BookSerializers(serializers.ModelSerializer):
      class Meta:
          model=Book
          fields="__all__"
          depth=1

備註:

1.定義一個depth=1  會將特殊字段  FK和M2M中每一個對象的全部字段所有取出來。

2.對於FK字段,顯示的是主鍵值,對於多對多字段,默認顯示方式爲:[pk1,pk2,...],一個列表中,包含全部字段對象的主鍵值。若是咱們不想顯示主鍵值,能夠重寫對應字段屬性。

複製代碼
class BookModelSerializers(ModelSerializer):
    class Meta:
        model=Book
        fields="__all__"

    authors=serializers.SerializerMethodField()
    def get_authors(self,obj):
         temp=[]
         for obj in  obj.authors.all():
             temp.append(obj.name)
         return temp
複製代碼

 3.對於含有choices的字段,咱們能夠經過指定字段的source來顯示展現的值

  好比:course_class = models.Integerfield(choices=((1,'初級'),(2,'中級')))

course_class = serializers.CharField(source='get_course_class_display')

提交post請求

複製代碼
複製代碼
  def post(self,request,*args,**kwargs):
       
        bs=BookSerializers(data=request.data,many=False)
        if bs.is_valid():
            # print(bs.validated_data)
            bs.save()
            return Response(bs.data)
        else:
            return HttpResponse(bs.errors)
複製代碼
複製代碼

備註:跟form組件相似,若是校驗不經過,能夠經過這個對象.errors將錯誤信息返回。

重寫save中的create方法

複製代碼
複製代碼
class BookSerializers(serializers.ModelSerializer):

      class Meta:
          model=Book
          fields="__all__"
          # exclude = ['authors',]
          # depth=1

      def create(self, validated_data):
        
          authors = validated_data.pop('authors')
          obj = Book.objects.create(**validated_data)
          obj.authors.add(*authors)
          return obj
複製代碼
複製代碼

 單條數據的get和put請求

複製代碼
複製代碼
class BookDetailViewSet(APIView):

    def get(self,request,pk):
        book_obj=Book.objects.filter(pk=pk).first()
        bs=BookSerializers(book_obj)
        return Response(bs.data)

    def put(self,request,pk):
        book_obj=Book.objects.filter(pk=pk).first()
        bs=BookSerializers(book_obj,data=request.data)
        if bs.is_valid():
            bs.save()
            return Response(bs.data)
        else:
            return HttpResponse(bs.errors)
複製代碼
複製代碼

超連接API:Hyperlinked

複製代碼
複製代碼
class BookSerializers(serializers.ModelSerializer):
      publish= serializers.HyperlinkedIdentityField(
view_name='publish_detail',
lookup_field="publish_id",
lookup_url_kwarg="pk") class Meta: model=Book fields="__all__" #depth=1
複製代碼
複製代碼

urls部分:

1
2
3
4
5
6
urlpatterns  =  [
     url(r '^books/$' , views.BookViewSet.as_view(),name = "book_list" ),
     url(r '^books/(?P<pk>\d+)$' , views.BookDetailViewSet.as_view(),name = "book_detail" ),
     url(r '^publishers/$' , views.PublishViewSet.as_view(),name = "publish_list" ),
     url(r '^publishers/(?P<pk>\d+)$' , views.PublishDetailViewSet.as_view(),name = "publish_detail" ),
]

   

 

 

 

總結

  restframework中的APIView組件,彌補了Django中對於json數據格式的支持,經過先後端都支持的json,完美的實現了先後端分離,使得後端只須要提供數據接口便可,並且restframework組件中的serializers組件,提供了更快捷、更靈活的序列化支持。下個系列中,將主要闡述restframework對視圖函數的三層封裝,感覺一下視圖三部曲的神奇。

 

 

做者:高賽出處:https://i.cnblogs.com/EditPosts.aspx?opt=1本文版權歸做者和博客園共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,不然保留追究法律責任的權利。

相關文章
相關標籤/搜索