1. 程式人生 > >基於django 開發的框架 jumpserver 原始碼解析(三)

基於django 開發的框架 jumpserver 原始碼解析(三)

 

 基於 rest_framework 的 url 路由 跟 資料 跟 前端互動。

  為了要讓 django 支援 RESTful 風格 的 介面 需要 用到 第三方元件,來 分析下rest_framework  對 request 做了什麼 ,又對 context 做了什麼?
照 慣例,先貼出原始碼

 
router = BulkRouter()
router.register(r'v1/assets', api.AssetViewSet, 'asset')
router.register(r'v1/admin-user', api.AdminUserViewSet, 'admin-user')
router.register(r'v1/system-user', api.SystemUserViewSet, 'system-user')
router.register(r'v1/labels', api.LabelViewSet, 'label')
router.register(r'v1/nodes', api.NodeViewSet, 'node')
router.register(r'v1/domain', api.DomainViewSet, 'domain')
router.register(r'v1/gateway', api.GatewayViewSet, 'gateway')
jumpserver 大量使用了 這種形式 的 路由。但事實 what the fuck 基於類的檢視為什麼沒有as_view() 方法? 其實as_view 方法 依然實現了,只不過又 封裝了幾層,更加抽象了一點。
這個 register 方法什麼樣,首先看原始碼。
class BaseRouter(six.with_metaclass(RenameRouterMethods)):
    def __init__(self):
        self.registry = []

    def register(self, prefix, viewset, basename=None, base_name=None):
        if base_name is not None:
            msg = "The `base_name` argument is pending deprecation in favor of `basename`."
            warnings.warn(msg, PendingDeprecationWarning, 2)

        assert not (basename and base_name), (
            "Do not provide both the `basename` and `base_name` arguments.")

        if basename is None:
            basename = base_name

        if basename is None:
            basename = self.get_default_basename(viewset)
        self.registry.append((prefix, viewset, basename))
其實register 方法 是呼叫了父類BaseRouter 的 register ,這個 方法 其實就是在內部 維持了一個 列表registry,這個 列表放了 prefix,viewset ,basename。接著往下看,
urlpatterns += router.urls

 原始碼中用到了 router的urls 屬性。

接下來 看 urls 屬性應該怎麼看?

 @property
    def urls(self):
        if not hasattr(self, '_urls'):
            self._urls = self.get_urls()
        return self._urls

這個 urls 屬性呼叫了 get_urls 方法。而 get_urls 方法 實在父類SimpleRouter中。

 def get_urls(self):
        """
        Use the registered viewsets to generate a list of URL patterns.
        """
        ret = []

        for prefix, viewset, basename in self.registry:
            lookup = self.get_lookup_regex(viewset)
            routes = self.get_routes(viewset)

            for route in routes:

                # Only actions which actually exist on the viewset will be bound
                mapping = self.get_method_map(viewset, route.mapping)
                if not mapping:
                    continue

                # Build the url pattern
                regex = route.url.format(
                    prefix=prefix,
                    lookup=lookup,
                    trailing_slash=self.trailing_slash
                )

                # If there is no prefix, the first part of the url is probably
                #   controlled by project's urls.py and the router is in an app,
                #   so a slash in the beginning will (A) cause Django to give
                #   warnings and (B) generate URLS that will require using '//'.
                if not prefix and regex[:2] == '^/':
                    regex = '^' + regex[2:]

                initkwargs = route.initkwargs.copy()
                initkwargs.update({
                    'basename': basename,
                    'detail': route.detail,
                })

                view = viewset.as_view(mapping, **initkwargs)
                name = route.name.format(basename=basename)
                ret.append(url(regex, view, name=name))

        return ret

這個 get_url 方法 就是 取出剛才 在 regester 方法中放進registry列表中 中的 東西,然後遍歷 routes 列表,route列表存放了Route容器,原始碼如下。

outes = [
        # List route.
        Route(
            url=r'^{prefix}{trailing_slash}$',
            mapping={
                'get': 'list',
                'post': 'create'
            },
            name='{basename}-list',
            detail=False,
            initkwargs={'suffix': 'List'}
        ),
        # Dynamically generated list routes. Generated using
        # @action(detail=False) decorator on methods of the viewset.
        DynamicRoute(
            url=r'^{prefix}/{url_path}{trailing_slash}$',
            name='{basename}-{url_name}',
            detail=False,
            initkwargs={}
        ),
        # Detail route.
        Route(
            url=r'^{prefix}/{lookup}{trailing_slash}$',
            mapping={
                'get': 'retrieve',
                'put': 'update',
                'patch': 'partial_update',
                'delete': 'destroy'
            },
            name='{basename}-detail',
            detail=True,
            initkwargs={'suffix': 'Instance'}
        ),
        # Dynamically generated detail routes. Generated using
        # @action(detail=True) decorator on methods of the viewset.
        DynamicRoute(
            url=r'^{prefix}/{lookup}/{url_path}{trailing_slash}$',
            name='{basename}-{url_name}',
            detail=True,
            initkwargs={}
        ),
    ]

遍歷route 列表,呼叫 get_method_map 這個方法,這個方法原始碼如下。

 def get_method_map(self, viewset, method_map):
        """
        Given a viewset, and a mapping of http methods to actions,
        return a new mapping which only includes any mappings that
        are actually implemented by the viewset.
        """
        bound_methods = {}
        for method, action in method_map.items():
            if hasattr(viewset, action):
                bound_methods[method] = action
        return bound_methods

其實這個方法 就是  返回了一個 method 跟 action 的 對映,mapping。然後把這個mapping 作為引數 放進 檢視類的 as_view 方法中。那 基於 rest_framework 的 as_view 跟普通  類檢視 有什麼不一樣?

舉個例子,jumpserver 中 常用的BulkModelViewSet 類,當中的as_view 方法,需要在父類中找,按照python 最新版,多繼承的定址順序為 從左 至右,廣度優先的原則。比如

class A (object):
    pass

class B(A):
    pass

class C(A):
    pass


class E(B,C):
    pass


if __name__ == '__main__':
    print(E.__mro__)

 

就是 多繼承 尋找方法順序為 E >> B>>C>>A>>object.

class BulkModelViewSet(bulk_mixins.BulkCreateModelMixin,
                       bulk_mixins.BulkUpdateModelMixin,
                       bulk_mixins.BulkDestroyModelMixin,
                       ModelViewSet):
    pass

 

最後在BulkModelViewSet 的 父類 ModelViewSet 的父類  GenericViewSet 的父類 ViewSetMixin 找到了 as_view 方法。

原始碼貼出 如下,這個方法 ,actions 就是剛才傳進去的mapping 引數,然後dispaher 方法,ViewSetMixin 本身沒有實現,老辦法找 父類,發現,其實父類 使用得是 ajdango 得  base 類View dispath 方法,這裡就不貼出了,然後呼叫檢視類 經行對映得那個方法比如 get——list ,或者 post-create 方法。


    @classonlymethod
    def as_view(cls, actions=None, **initkwargs):
        """
        Because of the way class based views create a closure around the
        instantiated view, we need to totally reimplement `.as_view`,
        and slightly modify the view function that is created and returned.
        """
        # The name and description initkwargs may be explicitly overridden for
        # certain route confiugurations. eg, names of extra actions.
        cls.name = None
        cls.description = None

        # The suffix initkwarg is reserved for displaying the viewset type.
        # This initkwarg should have no effect if the name is provided.
        # eg. 'List' or 'Instance'.
        cls.suffix = None

        # The detail initkwarg is reserved for introspecting the viewset type.
        cls.detail = None

        # Setting a basename allows a view to reverse its action urls. This
        # value is provided by the router through the initkwargs.
        cls.basename = None

        # actions must not be empty
        if not actions:
            raise TypeError("The `actions` argument must be provided when "
                            "calling `.as_view()` on a ViewSet. For example "
                            "`.as_view({'get': 'list'})`")

        # sanitize keyword arguments
        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" % (
                    cls.__name__, key))

        # name and suffix are mutually exclusive
        if 'name' in initkwargs and 'suffix' in initkwargs:
            raise TypeError("%s() received both `name` and `suffix`, which are "
                            "mutually exclusive arguments." % (cls.__name__))

        def view(request, *args, **kwargs):
            self = cls(**initkwargs)
            # We also store the mapping of request methods to actions,
            # so that we can later set the action attribute.
            # eg. `self.action = 'list'` on an incoming GET request.
            self.action_map = actions

            # Bind methods to actions
            # This is the bit that's different to a standard view
            for method, action in actions.items():
                handler = getattr(self, action)
                setattr(self, method, handler)

            if hasattr(self, 'get') and not hasattr(self, 'head'):
                self.head = self.get

            self.request = request
            self.args = args
            self.kwargs = kwargs

            # And continue as usual
            return self.dispatch(request, *args, **kwargs)

        # 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=())

        # We need to set these on the view function, so that breadcrumb
        # generation can pick out these bits of information from a
        # resolved URL.
        view.cls = cls
        view.initkwargs = initkwargs
        view.actions = actions
        return csrf_exempt(view)

在 檢視類 呼叫 list 方法 時候會 呼叫 get_queryset 方法只需要重寫queryset 方法 跟 制定 序列化類就好了。