Маршрутизация Django с одной страницей VueJS имеет неожиданное поведение, когда в URL-адресе нет косой черты.

У меня есть бэкэнд Django, комбинация внешнего интерфейса VueJS, где я обслуживаю REST API через Django и одностраничное приложение с VueJS и vue-router.

Из этого вопроса я получил совет использовать следующие URL-адреса в моем основном urls.py:

urlpatterns = [
    re_path(r'^(?P<filename>(robots.txt)|(humans.txt))$', views.home_files, name='home-files'),
    path('api/', include('backend.urls', namespace='api')),
    path('auth/', include('auth.urls')),
    path('admin/', admin.site.urls),
    re_path(r'^.*$', views.vue), # VueJS frontend
]

Итак, я хочу, чтобы URL-адреса вели себя так:

{baseDomain}/api/users/1/ -> go to backend.urls  
{baseDomain}/auth/login/ -> go to auth.urls  
{baseDomain}/admin/ -> go to admin page  
{baseDomain}/de/home -> vue-router takes over

Теперь эти URL-адреса работают отлично, однако я ожидаю, что {baseDomain}/api/users/1 (без завершающей косой черты) все равно будет идти на backend.urls, однако происходит то, что я попадаю на страницу Vue.

Добавление APPEND_SLASH = True в settings.py также не помогает, поскольку косая черта добавляется только в том случае, если страница для загрузки не найдена. Но поскольку регулярное выражение для моего внешнего интерфейса соответствует чему угодно, оно всегда перенаправляет на Vue.

Моя попытка состояла в том, чтобы исправить это, добавив:

re_path(r'.*(?<!/)$', views.redirect_with_slash)

со следующим кодом:

def redirect_with_slash(request):
    '''Redirects a requested url with a slash at the end'''
    if request.path == '/':
        return render(request, 'frontend/index.html')
    return redirect(request.path + '/')

Но это не очень элегантно. Также обратите внимание на if request.path == '/':. Как ни странно, Django сопоставлял '/' с регулярным выражением r'.*(?<!/)$', а затем перенаправлял на '//', что является недопустимым URL-адресом и отображало страницу с ошибкой, поэтому мне пришлось включить этот оператор if.

У кого-нибудь есть решение для этого? В указанном вопросе это не казалось проблемой, поэтому мне интересно, почему это в моем проекте.

РЕДАКТИРОВАТЬ: бэкэнд urls.py

"""
backend urls.py
"""
from django.urls import include, path
from rest_framework_nested import routers
from auth.views import UserViewSet, GroupViewSet, ProjectViewSet
from .views import IfcViewSet, IfcFileViewSet

app_name = 'api'

router = routers.DefaultRouter() #pylint: disable=C0103
router.register(r'users', UserViewSet)
router.register(r'groups', GroupViewSet)
router.register(r'projects', ProjectViewSet)
projects_router = routers.NestedSimpleRouter(router, r'projects', lookup='project')
projects_router.register(r'models', IfcFileViewSet, base_name='projects-models')

urlpatterns = [
    path('', include(router.urls)),
    path('', include(projects_router.urls))
]

"""
auth urls.py
"""
from django.urls import path, include
from rest_framework import routers
from rest_framework_jwt.views import obtain_jwt_token, refresh_jwt_token
from .views import RegistrationViewSet

app_name = 'authentication'
router = routers.DefaultRouter()
router.register('register', RegistrationViewSet)

urlpatterns = [
    path('', include(router.urls)),
    path('', include('rest_auth.urls')),
    path('refresh_token/', refresh_jwt_token),
]

person Truning    schedule 21.11.2019    source источник
comment
Пожалуйста, добавьте свой backend.urls.   -  person heemayl    schedule 21.11.2019
comment
@heemayl Добавил их только что. Я использую pypi.org/project/django-rest-framework-nested как вы думаете, это может быть проблемой?   -  person Truning    schedule 21.11.2019
comment
@heemayl, но тогда не должны ли работать URL-адреса в auth urls.py? Я импортирую сюда vanilla DRF, но {baseDomain}/auth/login не ведет себя как {baseDomain}/auth/login/.   -  person Truning    schedule 21.11.2019


Ответы (1)


Проблема в том, что у вас есть уловка в re_path(r'^.*$', views.vue), поэтому, если какой-либо URL-адрес не совпадает точно с более ранними path, это будет активировано.

Django CommonMiddleware на самом деле добавляет завершающую косую черту и перенаправляет, когда находит 404, а путь URL-адреса не заканчивается на / (в зависимости от настройки APPEND_SLASH), но это при ответе.

В вашем случае у вас может быть крошечное промежуточное программное обеспечение запроса, которое добавляет конечную косую черту, если путь запроса не заканчивается на /, например:

from django.shortcuts import redirect

class AppendTrailingSlashOnRequestMiddleware:

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):

        if not request.path.endswith('/'):
            query_string = request.META['QUERY_STRING']
            query_string = f'?{query_string}' if query_string else ''
            to_url = f'{request.path}/{query_string}'
            return redirect(to_url, permanent=True)

        response = self.get_response(request)

        return response

Очевидно, добавьте промежуточное ПО в settings.MIDDLEWARE, предпочтительно поместите его вверху, чтобы предотвратить ненужную обработку от других промежуточных программ, поскольку мы все равно будем перенаправлять, и тогда также потребуется обработка.


Но у этого есть проблема; данные из POST/PUT/PATCH будут потеряны при перенаправлении (здесь мы делаем 301, но аналогично применимо для 302. Там Временное перенаправление 307, которое может помочь нам в этом отношении, и хорошо, что все обычные браузеры, включая IE, поддерживают это. box; поэтому нам нужно реализовать это самостоятельно:

from django.http.response import HttpResponseRedirectBase

class HttpTemporaryResponseRedirect(HttpResponseRedirectBase):
    status_code = 307

Теперь импортируйте это в промежуточное ПО и используйте его вместо redirect:

class AppendTrailingSlashOnRequestMiddleware:

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):

        if not request.path.endswith('/'):
            query_string = request.META['QUERY_STRING']
            query_string = f'?{query_string}' if query_string else ''
            to_url = f'{request.path}/{query_string}'
            return HttpTemporaryResponseRedirect(to_url)  # here

        response = self.get_response(request)

        return response

Примечание. Если вы хотите сохранить средства кэширования браузера для GET, вы можете перенаправить на 301/307 на основе request.method.

person heemayl    schedule 21.11.2019
comment
Работает как шарм! Большое спасибо. Однако два дополнительных вопроса: в чем разница между временным и постоянным перенаправлением (например, HTTP 308)? Должен ли я сделать оператор if, в котором я даю status_code 301 для GET и HEAD и 308 для POST и т. д.? И второй вопрос: почему я не могу найти это больше нигде? Моя установка такая необычная? Есть ли более разумный способ обслуживать одностраничное веб-приложение с помощью Django? Спасибо еще раз - person Truning; 22.11.2019
comment
@Truning Вау. Так много вопросов :) Браузер кэширует все постоянные перенаправления, тогда как временные не кэшируются. То, как я обслуживаю свой SPA с DRF, заключается в том, что Nginx сидит впереди и отправляет все запросы во внешний интерфейс Vue (у которого есть собственная маршрутизация для доступа к различным материалам), кроме маршрутов внутреннего API, которые идут прямо к uWSGI (и, в свою очередь, Django /ДРФ). - person heemayl; 22.11.2019
comment
Да, много вопросов, извините за это :D Отлично, спасибо за ваши ответы. Я думаю, что-то подобное можно сделать и с Apache? - person Truning; 22.11.2019