Мобильная версия для Django-проекта
С каждым днем пользователи смартфонов занимают все большую долю интернета. По данным LiveInternet доля российских пользователей OS Android уже превысила долю Windows7. В выходные дни пользователи мобильных платформ пользуются интернетом значительно чаще. Та же тенденция наблюдается и в мире. Все это еще раз доказывает необходимость адаптации сайта для смартфонов и планшетов.
О том, как можно адаптировать ваш Django-проект для мобильных устройств, я расскажу в этой статье. Но сначала давайте разберем, какие есть варианты создания мобильной версии сайта.
1.1 Адаптивная верстка В этом случае мы отдаем одинаковое количество данных для большой и мобильной версии сайта. Этот подход самый простой для backend разработки, все решается версткой.Из плюсов:
не требует редиректов; не нужно отдельно сообщать поисковым роботам о наличии мобильной версии (мета теги alternate, сайтмапы и прочее). Из минусов: поскольку на мобильной версии приходится отдавать все и скрывать лишнее, создается лишний трафик и нагрузка на сервер; версия для мобильных устройств всегда должна создаваться с большой версией сайта; так как данные на обоих версиях одинаковы, может быть сложно удобно организовать обе версии, скорее всего мобильной версией придется пожертвовать; к примеру, я не представляю, как можно сделать удобный форум при таком подходе. Такой подход хорошо подходит для небольших сайтов. Когда выводимого контента на страницу становится много, простота в реализации создает большую проблему в юзабилити.1.2 Мобильная версия на поддомене По сути, это два отдельных сайта. Такой подход решает проблемы лишнего трафика, дает больше гибкости и возможностей в разработке версии для мобильных устройств. Однако при этом вопрос, какую версию показывать пользователю, решается сервером, а не браузером. Также нужно дать возможность пользователю выбрать, какая версия сайта ему нужна, и «подружить» обе версии сайта редиректами и альтернейтами.Разные URL одной страницы приводят к недостатку этого способа: относительные ссылки в материалах сайта могут вести на страницы, отсутствующие в мобильной версии. Поэтому приходится указывать абсолютные ссылки на основной домен и потом редиректить пользователя на нужную версию. Решением этого недостатка будет полностью соответствующие большая и мобильная версии, но в этом случае правильнее идти третьим способом.
1.3 Мобильная версия на том же домене Это доработка первого подхода и решение его минуса с трафиком и лишней нагрузкой. Реализуется он так же как с поддоменом: вы определяете, какая версия нужна клиенту, и отдаете нужное количество данных в нужный шаблон. Одинаковый URL для обоих версий сайта — безусловно плюс. Хотя проблема организации контента для обоих версий еще остается, но решать ее уже проще, так как ограничения на одинаковые данные уже нет.1.4 Наш опыт В отделе контентных проектов Mail.Ru Group мы используем второй подход, хотя и плавно движемся в сторону третьего. Проекты Дети Mail.Ru и Здоровье Mail.Ru написаны на Django, оба имеют мобильные и/или тач версии. Несмотря на то, что проекты под капотом немного отличаются, механизм создания мобильных версий у них одинаков. Об этом я хочу с вами поделиться.
2.1 Все роуты мы именуем
url (r'^$', views.MainIndexView.as_view (), name='health-main-index')
url (r'^(? P
2.3 Отдельные контроллеры для большой и мобильной версии Несмотря на то, что порой контроллеры большой и мобильной версий сайта отдают в контекст одинаковые данные, мы их разделили на отдельные функции/классы. Да, между ними есть дублирование, но зато код становится понятней без лишних абстракций и проверок, когда какие функции нужны и в каком виде. Мобильная версия должна решать следующие задачи: Определение версии сайта и редирект для соответствующих устройств. Возможность пользователя отказаться от мобильной версии и использовать основную. Редиректы должны перенаправлять на соответствующую мобильную версию и наоборот. Нехорошо кидать пользователя на главную и заставлять искать с начала. Если пользователь попал на отсутствующую страницу в мобильной версии, для которой есть аналог в большой версии, ему нужно явно сообщать об этом. Этой задачи не было, если бы обе версии соответствовали друг другу. Необходимо указывать мета теги alternate и canonical, если для страницы доступен мобильный аналог. 3.1 Определение версии сайта С какого устройства пользователь зашел на наш проект мы определяем с помощью нашего модуля 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)
Редирект пользователя возможен, если: пришел GET запрос;
запрос пришел на основную версию сайта (если пользователь явно набрал адрес мобильной версии — оставим его на ней);
проверим, есть ли такая страница в мобильной версии (не редиректить же его на 404).
В общем случае какую версию отдать пользователю, определяется по UserAgent в middleware. Там же нужно проверить значение куки mobile. Сам я не пользовался приложением django-mobile, возможно, есть другие более точные библиотеки для определения типа устройства. Предложите их в комментариях.3.2 Переход на большую версию сайта
На мобильную версию мы отправили пользователя, дадим ему также возможность перейти обратно на большую версию. В подвалах наших проектов содержится ссылка вида /go-health/, по которой и осуществляется переход.
url (r'^go-health (? P
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 просто подменяет имя роута для страницы-аналога. Это довольно ограниченное решение, но его пока хватает.3.3 404 для мобильной версии Вы можете не просто сообщать пользователю о том, что страница не найдена, но и указать, что такая страница есть в полной версии сайта. При этом проверить 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) 3.4 Мета теги alternate и canonical Эти 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')) } Хочется повторить, что основные трудности реализации вызваны расхождением основной и мобильной версий. Пока не получается развивать мобильную версию одновременно с большой, приходится идти этим путем и держать в коде эти проверки. Возможно, в скором времени мы перейдем на мобильную версию на том же домене и отдельно напишем об этом способе.