让DRF的URL支持前缀的一种方式

DRF(Django REST Framework)不支持带前缀的资源。 所有资源,默认都应该在URL的根路径下(/)。

这里提供了一种支持前缀的解决方案,并且支持带参数的前缀。

URL配置前缀

ROUTER = DefaultRouter()
...

urlpatterns = [
    path('', include(ROUTER.urls)),
    path(r'api/v1/app/', include(ROUTER.urls)),
    path(r'api/v2/<str:app_uid>/', include(ROUTER.urls)),
]

这里三行path,展示了三种定义方式。

  1. ''代表一般的推荐用法,没有前缀。
  2. r'api/v1/app/'代表一种常见的固定前缀。
  3. r'api/v2/<str:app_uid>/'代表在前缀中,包含动态的参数。

第1种定义,没有本文的方案,也能正常使用。 v1v2两种前缀,则需要本文的方案,自定义Field类。

但是,无论使用第2还是第3种方案,都必须加上第1种。 因为,在进行资源关联时,v1v2的API,都会去自动需要''下的资源。 如果它不存在,则会报错。 这一点固然也有办法,但都比较麻烦,不如保留'',在部署的代理层面过滤即可。

自定义带前缀的Field

默认情况下,资源中的url字段,会返回''下的那个链接。 这里通过自定义Field,为它加上了固定前缀。

class PrefixIdField(HyperlinkedIdentityField):
    """Customize `url` field with prefix.

    Example:
        Set `LEVELS = 3`, then a prefix with 3 levels is supported,
        like `/api/v1/app`.
    """

    LEVELS = 3

    def get_url(self, obj, view_name, request, *args, **kwargs):
        url = super().get_url(obj, view_name, request, *args, **kwargs)

        splits = request.path.split('/')
        prefix = '/'.join(splits[:self.LEVELS + 1])
        parsed = urlparse(url)
        path = prefix + parsed.path

        if DEBUG:
            return urljoin(url, path)
        return path

前缀的层级一般是固定的,这里示例时使用了3层,只支持静态配置。 其核心操作,就是在生成关联资源的URL(也即响应JSON中的url字段)时,额外增加前缀。

最后在return时,这里还展示了如何返回完整的URL(if DEBUG),或只返回其path部分。 在本地开发时,使用完整URL可方便点击;上线后,只保留path部分,可减少重复、降低流量,也不影响前端使用。 这部分逻辑,可按需调整。

替换原生的Field

PrefixIdField定义完成后,只需要修改Serializer.serializer_url_field,就可使用。 由于整个DefaultRouter下的所有资源都需要这个调整,因此可以考虑定义一个公共的基类,统一配置。

class BaseSrlz(HyperlinkedModelSerializer):
    serializer_url_field = PrefixIdField
    ...

获取v2前缀中的参数

对以上v2类型的前缀,还有一个示例的动态参数app_uid。 在SerializerViewSet中,均有办法获取。

class BaseSrlz(HyperlinkedModelSerializer):
    serializer_url_field = PrefixIdField

    def create(self, validated_data):
        request = self.context['request']
        app_uid = req.parser_context['kwargs'].get('app_uid')
        ...
        return super().create(validated_data)


class AViewSet(ModelViewSet):

    def retrieve(self, request, *args, **kwargs):
        app_uid = request.parser_context['kwargs'].get('app_uid')
        ...
        return super().retrieve(request, *args, **kwargs)

结论

由于DRF官方不支持,所以以上方案只能算是一种hack。 它把所有关联资源都设为''那组,并且让这组资源按URL前缀,修改关联的url前缀。 但无论如何,这是一个可用的有效方案,可以撑到官方支持出炉。


相关笔记