- PVSM.RU - https://www.pvsm.ru -
С каждым днем пользователи смартфонов занимают все большую долю интернета. По данным [2] LiveInternet доля российских пользователей OS Android уже превысила долю Windows7. В выходные дни пользователи мобильных платформ пользуются [3] интернетом значительно чаще. Та же тенденция наблюдается [4] и в мире. Все это еще раз доказывает необходимость адаптации сайта для смартфонов и планшетов.
О том, как можно адаптировать ваш Django-проект для мобильных устройств, я расскажу в этой статье. Но сначала давайте разберем, какие есть варианты создания мобильной версии сайта.
В этом случае мы отдаем одинаковое количество данных для большой и мобильной версии сайта. Этот подход самый простой для backend разработки, все решается версткой.
Из плюсов:
Из минусов:
Такой подход хорошо подходит для небольших сайтов. Когда выводимого контента на страницу становится много, простота в реализации создает большую проблему в юзабилити.
По сути, это два отдельных сайта. Такой подход решает проблемы лишнего трафика, дает больше гибкости и возможностей в разработке версии для мобильных устройств. Однако при этом вопрос, какую версию показывать пользователю, решается сервером, а не браузером. Также нужно дать возможность пользователю выбрать, какая версия сайта ему нужна, и «подружить» обе версии сайта редиректами и альтернейтами.
Разные URL одной страницы приводят к недостатку этого способа: относительные ссылки в материалах сайта могут вести на страницы, отсутствующие в мобильной версии. Поэтому приходится указывать абсолютные ссылки на основной домен и потом редиректить пользователя на нужную версию. Решением этого недостатка будет полностью соответствующие большая и мобильная версии, но в этом случае правильнее идти третьим способом.
Это доработка первого подхода и решение его минуса с трафиком и лишней нагрузкой. Реализуется он так же как с поддоменом: вы определяете, какая версия нужна клиенту, и отдаете нужное количество данных в нужный шаблон. Одинаковый URL для обоих версий сайта — безусловно плюс. Хотя проблема организации контента для обоих версий еще остается, но решать ее уже проще, так как ограничения на одинаковые данные уже нет.
В отделе контентных проектов Mail.Ru Group мы используем второй подход, хотя и плавно движемся в сторону третьего. Проекты Дети Mail.Ru и Здоровье Mail.Ru написаны на Django, оба имеют мобильные и/или тач версии. Несмотря на то, что проекты под капотом немного отличаются, механизм создания мобильных версий у них одинаков. Об этом я хочу с вами поделиться.
url(r'^$', views.MainIndexView.as_view(), name='health-main-index')
url(r'^(?P<slug>[-w]+)/$', views.NewsDetailView.as_view(), name='health-news-detail')
И обращаемся к ним всегда по этому имени.
reverse('health-main-index')
Мы никогда не собираем URL'ы сами в контроллерах или шаблонах, они указаны только в urls.py. DRY [5].
Об этой библиотеке [6] уже упоминали на Хабре. Изначально мы ее использовали на «детях» для форума на поддомене, сейчас форум переехал у нас на основной хост, и эта библиотека используется только для мобильный версий.
Вкратце, как она работает: вы подключаете middleware, которая в зависимости от Host заголовка подменяет схему URL'ов. Помимо джанговской функции reverse
, вы можете использовать из этой библиотеки reverse_full
, которая строит абсолютный URL. Подобный тег host_url
можно использовать в шаблонах. Используемые функции reverse_host
, get_host
также взяты из этого приложения.
Несмотря на то, что порой контроллеры большой и мобильной версий сайта отдают в контекст одинаковые данные, мы их разделили на отдельные функции/классы. Да, между ними есть дублирование, но зато код становится понятней без лишних абстракций и проверок, когда какие функции нужны и в каком виде.
Мобильная версия должна решать следующие задачи:
С какого устройства пользователь зашел на наш проект мы определяем с помощью нашего модуля nginx. Выглядит это примерно вот так:
set $mobile $rb_mobile;
if ($cookie_mobile ~ 0) { set $mobile ""; } # discard by cookie
proxy_set_header X-Mobile-Version $mobile;
Модуль опредеяет тип версии, которую нужно показать (m или touch), но если у пользователя стоит кука mobile, мы игнорируем это. Результат передается в виде http заголовка на бэкенд.
Дальнейшая обработка запроса происходит в middleware.
class MobileMiddleware(object):
def process_request(self, request):
if request.method != 'GET': # redirect only GET requests
return
mobile_version = request.META.get(MOBILE_HEADER, '')
if not mobile_version: # redirect only for mobile devices
return
hostname = request.host.name
if hostname in settings.MOBILE_HOSTS: # redirect only for main version
return
if mobile_version == 'm':
host = get_host('mobile-' + hostname)
elif mobile_version == 'touch':
host = get_host('touch-' + hostname)
else:
# wrong header value
return
if not is_valid_path(request.path, host.urlconf):
# url doesn't exist in mobile version
return
redirect_to = u'http://{}{}'.format(reverse_host(host), request.get_full_path())
return http.HttpResponseRedirect(redirect_to)
Редирект пользователя возможен, если:
В общем случае какую версию отдать пользователю, определяется [7] по UserAgent в middleware. Там же нужно проверить значение куки mobile. Сам я не пользовался приложением django-mobile, возможно, есть другие более точные библиотеки для определения типа устройства. Предложите их в комментариях.
На мобильную версию мы отправили пользователя, дадим ему также возможность перейти обратно на большую версию. В подвалах наших проектов содержится ссылка вида /go-health/
, по которой и осуществляется переход.
url(r'^go-health(?P<path>/.*)$', 'health.mobile.views.go')
К сожалению, иногда страницы мобильной версии отличаются от основной. Та информация, которая легко помещается на большой версии, в мобильной разделяется на 3 страницы. Поэтому отбрасывать поддомен и редиректить на тот же URL, было бы неправильно. Мы выбрали следующий алгоритм:
go_view_name
. В этом случае мы редиректим на страницу с этим (другим) именем роута. Это нужно как раз для того случая, когда несколько страниц одной версии соответствуют одной странице большой версии.Таким образом, имя роутов выступает связкой между контроллерами большой и мобильной версии.
@never_use_mobile
def go(request, path):
meta_query_string = request.META.get('QUERY_STRING', '')
query_string = '?' + iri_to_uri(meta_query_string) if meta_query_string else ''
main_host = get_main_host(request.host)
try:
resolver_match = resolve(path)
except Resolver404:
pass
else:
if hasattr(resolver_match.func, 'go_view_name'):
redirect_to = 'http:%s%s' % (reverse_full(
main_host.name, resolver_match.func.go_view_name,
view_args=resolver_match.args, view_kwargs=resolver_match.kwargs),
query_string)
return HttpResponseRedirect(redirect_to)
# path matches url patterns, otherwise 404
resolver_match = resolve(path, main_host.urlconf)
redirect_to = 'http:%s%s' % (reverse_full(
main_host.name, resolver_match.view_name,
view_args=resolver_match.args, view_kwargs=resolver_match.kwargs),
query_string)
return HttpResponseRedirect(redirect_to)
Атрибут go_url_name
назначается через декоратор
def go(url_name):
def decorator(view_func):
@wraps(view_func, assigned=available_attrs(view_func))
def _wrapped_view_func(*func_args, **func_kwargs):
return view_func(*func_args, **func_kwargs)
_wrapped_view_func.go_url_name = url_name
return _wrapped_view_func
return decorator
@go('health-news-index')
def rubric_list(request):
...
А декоратор never_use_mobile
ставит куку mobile для отмены автоматического редиректа
def never_use_mobile(view_func):
@wraps(view_func, assigned=available_attrs(view_func))
def _wrapped_view_func(request, *args, **kwargs):
response = view_func(request, *args, **kwargs)
set_mobile_cookie(response, 0)
return response
return _wrapped_view_func
К сожалению, тач версии развиваются после основных разделов и не всегда соответствуют страницам на большой версии, поэтому такой код приходится держать.
Атрибут go_view_name
просто подменяет имя роута для страницы-аналога. Это довольно ограниченное решение, но его пока хватает.
Вы можете не просто сообщать пользователю о том, что страница не найдена, но и указать, что такая страница есть в полной версии сайта. При этом проверить URL по схеме URL'ов недостаточно: запрос /news/foo/
соответствует схеме URL'ов, а новости такой нет. Поэтому надо попытаться выполнить функцию контроллера в основной схеме урлов. Есть еще одна тонкость: надо подменять текущую схему URL'ов для большой версии, так как она нужна функциям reverse
и тегу url. Иначе вы будете рендерить страницу большой версии в схеме URL'ов мобильной.
def page_not_found(request):
current_host = request.host
hostname = current_host.name
main_host = get_host(hostname.replace('mobile-', ''))
try:
# path matches url patterns
resolver_match = resolve(request.path, urlconf=main_host.urlconf)
except Resolver404:
return mobile_404(request)
set_urlconf(main_host.urlconf)
try:
# function returns not 404 with passed arguments
resolver_match.func(request, *resolver_match.args, **resolver_match.kwargs)
except Http404:
set_urlconf(current_host.urlconf)
return mobile_404(request)
set_urlconf(current_host.urlconf)
meta_query_string = request.META.get('QUERY_STRING', '')
query_string = '?' + iri_to_uri(meta_query_string) if meta_query_string else ''
redirect_to = 'http:%s%s' % (reverse_full(
main_host.name, resolver_match.view_name,
view_args=resolver_match.args, view_kwargs=resolver_match.kwargs),
query_string)
return mobile_fallback404(request, redirect_to)
Эти URL'ы строятся с помощью функций или шаблонных тегов приложения django-host.
context['canonical'] = build_canonical(reverse_full('www', 'health-news-index'))
context['alternate'] = {
'touch': build_canonical(reverse_full('touch-www', 'health-news-index'))
}
Хочется повторить, что основные трудности реализации вызваны расхождением основной и мобильной версий. Пока не получается развивать мобильную версию одновременно с большой, приходится идти этим путем и держать в коде эти проверки. Возможно, в скором времени мы перейдем на мобильную версию на том же домене и отдельно напишем об этом способе.
Автор: bekbulatov
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/71076
Ссылки в тексте:
[1] Image: http://habrahabr.ru/company/mailru/blog/239343/
[2] данным: http://www.liveinternet.ru/stat/ru/oses.gif?slice=ru;period=week;graph=yes
[3] пользуются: http://www.liveinternet.ru/stat/ru/oses.gif?slice=ru;graph=yes
[4] наблюдается: http://www.liveinternet.ru/stat/ru/oses.gif?period=week;graph=yes
[5] DRY: https://ru.wikipedia.org/wiki/Don%E2%80%99t_repeat_yourself
[6] библиотеке: https://github.com/jezdez/django-hosts
[7] определяется: https://github.com/gregmuellegger/django-mobile/blob/master/django_mobile/middleware.py
[8] Источник: http://habrahabr.ru/post/239343/
Нажмите здесь для печати.