在Django中使用基于类的视图(ClassView),类中所定义的方法名称与Http的请求方法相对应,才能基于路由将请求分发(dispatch)到ClassView中的方法进行处理,而Django REST framework中能够突破这一点,经过ViewSets能够实现自定义路由。css
为get_stocks方法添加list_route装饰器,url_path参数是暴露在外的接口名称app
class StockViewSet(viewsets.ModelViewSet): queryset = AppStock.objects.all() @list_route(url_path='getstocklist') def get_stocks(self, request, *args, **kwargs): '''获取股票列表''' return Response({'succss':True,'msg':'操做成功'})
来看一下list_route的定义:post
def list_route(methods=None, **kwargs): """ Used to mark a method on a ViewSet that should be routed for list requests. """ methods = ['get'] if (methods is None) else methods def decorator(func): func.bind_to_methods = methods func.detail = False func.kwargs = kwargs return func return decorator
对于接口,通常有获取列表页和获取详情两种形式。一样的,还有detail_route装饰器。list_route、detail_route的做用都是为方法添加了bind_to_methods、detail、kwargs属性,惟一的区别是detail属性值的不一样ui
def detail_route(methods=None, **kwargs): """ Used to mark a method on a ViewSet that should be routed for detail requests. """ methods = ['get'] if (methods is None) else methods def decorator(func): func.bind_to_methods = methods func.detail = True func.kwargs = kwargs return func return decorator
router=DefaultRouter() router.register(r'stock',StockViewSet) urlpatterns = [ url(r'',include(router.urls)), url(r'^admin/', admin.site.urls), ]
DefaultRouter是BaseRouter的子类,register方法内部将其注册的prefix与之对应的viewset保存在registry列表中url
class BaseRouter(object): def __init__(self): self.registry = [] def register(self, prefix, viewset, base_name=None): if base_name is None: base_name = self.get_default_base_name(viewset) self.registry.append((prefix, viewset, base_name))
其urls属性是一个描述符,内部调用了get_urls方法spa
从get_routes中能够看出些眉目了,遍历ViewSet中定义的方法,获取到方法的bind_to_method和detail属性(list_route、detail_route的功劳),根据detial属性将它们分别保存到detail_routes和list_routes列表中,保存的是httpmethod与methodname的元祖对象code
def get_routes(self, viewset): """ 省略若干... """ # Determine any `@detail_route` or `@list_route` decorated methods on the viewset detail_routes = [] list_routes = [] for methodname in dir(viewset): attr = getattr(viewset, methodname) httpmethods = getattr(attr, 'bind_to_methods', None) detail = getattr(attr, 'detail', True) httpmethods = [method.lower() for method in httpmethods] if detail: detail_routes.append((httpmethods, methodname)) else: list_routes.append((httpmethods, methodname)) def _get_dynamic_routes(route, dynamic_routes): ret = [] for httpmethods, methodname in dynamic_routes: method_kwargs = getattr(viewset, methodname).kwargs initkwargs = route.initkwargs.copy() initkwargs.update(method_kwargs) url_path = initkwargs.pop("url_path", None) or methodname url_name = initkwargs.pop("url_name", None) or url_path ret.append(Route( url=replace_methodname(route.url, url_path), mapping={httpmethod: methodname for httpmethod in httpmethods}, name=replace_methodname(route.name, url_name), initkwargs=initkwargs, )) return ret ret = [] for route in self.routes: if isinstance(route, DynamicDetailRoute): # Dynamic detail routes (@detail_route decorator) ret += _get_dynamic_routes(route, detail_routes) elif isinstance(route, DynamicListRoute): # Dynamic list routes (@list_route decorator) ret += _get_dynamic_routes(route, list_routes) else: # Standard route ret.append(route) return ret
接着,遍历routes列表,看到这个代码,我也是看了挺久才看懂这用意,routes列表包含固定的四个Route对象orm
routes = [ # List route. Route( url=r'^{prefix}{trailing_slash}$', mapping={ 'get': 'list', 'post': 'create' }, name='{basename}-list', initkwargs={'suffix': 'List'} ), # Dynamically generated list routes. DynamicListRoute( url=r'^{prefix}/{methodname}{trailing_slash}$', name='{basename}-{methodnamehyphen}', initkwargs={} ), # Detail route. Route( url=r'^{prefix}/{lookup}{trailing_slash}$', mapping={ 'get': 'retrieve', 'put': 'update', 'patch': 'partial_update', 'delete': 'destroy' }, name='{basename}-detail', initkwargs={'suffix': 'Instance'} ), # Dynamically generated detail routes. DynamicDetailRoute( url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$', name='{basename}-{methodnamehyphen}', initkwargs={} ), ]
其用意是经过调用_get_dynamic_routes内嵌方法,把routes列表中项做为模板,将list_routes和detail_routes中的项依次进行替换,最终获得一个Route对象的列表(Route是一个namedtuple,包含如url、mapping、name等项)router
[ Route(url='^{prefix}{trailing_slash}$', mapping={'get': 'list', 'post': 'create'}, name='{basename}-list', initkwargs={'suffix': 'List'}), Route(url='^{prefix}/getstocklist{trailing_slash}$', mapping={'get': 'get_stocks'}, name='{basename}-getstocklist', initkwargs={}), Route(url='^{prefix}/{lookup}{trailing_slash}$', mapping={'get': 'retrieve', 'patch': 'partial_update', 'put': 'update', 'delete': 'destroy'}, name='{basename}-detail', initkwargs={'suffix': 'Instance'}) ]
get_route方法的功能到此结束了,回到get_urls方法中csrf
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:] view = viewset.as_view(mapping, **route.initkwargs) name = route.name.format(basename=basename) ret.append(url(regex, view, name=name)) return ret
这里的核心点是viewset的as_view方法,是否是很熟悉,Django中基于类的视图注册路由时也是调用的ClassView的as_view方法。as_view方法是在父类ViewSetMixin中定义的,传入的action参数是httpmethod与methodname的映射一个字典,如 {'get': 'get_stocks'}
def as_view(cls, actions=None, **initkwargs): """ 省略若干... """ 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) # And continue as usual return self.dispatch(request, *args, **kwargs) view.cls = cls view.initkwargs = initkwargs view.suffix = initkwargs.get('suffix', None) view.actions = actions return csrf_exempt(view)
核心点是这个view方法以及dispatch方法,view方法中遍历anctions字典,经过setattr设置名称为httpmethod的属性,属性值为methodname所对应的方法。在dispathch方法中,就可经过getattr获取到httpmethod所对应的handler
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
get_urls方法最终返回的结果是url(regex, view, name=name)的列表,这也就是ViewSet帮咱们建立的自定义路由,其实现与咱们在urls.py注册路由是同样的。url方法获得的是RegexURLPattern对象
[ <RegexURLPattern appstock-list ^stock/$>, <RegexURLPattern appstock-getstocklist ^stock/getstocklist/$>, <RegexURLPattern appstock-detail ^stock/(?P<pk>[^/.]+)/$> ]
访问 http://127.0.0.1:8000/stock/getstocklist/
,请求就会交由StockViewSet中的get_stocks方法进行处理了。
整个过程大体就是这样了。