https://habrahabr.ru/post/342280/Доброго времени суток!
Относительно недавно появившийся html5 HistoryAPI уже стал довольно популярным. В интернете можно найти много статей о том, как поднять у себя работу HistoryAPI, но при этом они по большей части однообразны и есть два нюанса:
- Они организованы так, что обрабатывают одинаково все ссылки;
- Можно отстрелить себе ногу и не понять — почему.
В данной статье рассматривается способ организации работы HistoryAPI так, чтобы потом не продавать душу дьяволу, чтобы всё работало.
Итак, что же предлагают большинство статей:
Предположим, что у нас есть самый стартовый вариант разметки — несколько навигационных ссылок и блок контента:
<!DOCTYPE html>
<html>
<head>
<...>
</head>
<body>
<nav>
<a href="//<?=$_SERVER['HTTP_HOST']?>">Главная</a>
<a href="/about">О проекте</a>
<a href="/contact">Обратная связь</a>
</nav>
<div id="content">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Voluptatibus, odio.
</div>
</body>
</html>
И такой же усреднённый минимальный предлагаемый js:
$(document).ready(function(){
$('a').click(function(e){
e.preventDefault();
var url = $(this).attr('href');
$.ajax({
url: url,
data: 'ajax=true',
success: function(data){
//Самый усреднённый вариант, но можно и передать полученную информацию в собственный обновлятель страницы
$('#content').html(data.content);
}
});
window.history.pushState(null, null, url);
return false;
});
$(window).bind('popstate', function(){
$.ajax({
url: history.location,
data: 'ajax=true',
success: function(data){
$('#content').html(data.content);
}
});
});
});
Что здесь уже плохо:
- Этот js одинаково обрабатывает ВСЕ ссылки: внешние, локальные и даже ссылки, которые никуда не перенаправляют (допустим, вы сделали ссылку, открывающую модальное окно). А на дворе, на минуточку, 2017 год. Ссылок, которые никуда не ведут, на разных сайтах очень много; внешние ссылки и вовсе принято (хороший тон) открывать в новой вкладке.
- Как только у вас в изменяемом блоке появится локальная ссылка — вы обречены. Потому что она не будет обрабатываться вашим скриптом, и вы даже не будете понимать — почему.
Что же делать?
Проблема 1 довольно легко решается: нужно отлавливать только ссылки с атрибутом href (я для ссылок на модальные окна, например, использую конструкцию вида <a modal_url="/url" modal_header=«Header» title=«Title»>) и с помощью регулярки понять, локальная ссылка или нет; и в зависимости от этого обрабатывать её по-разному.
С проблемой 2 я столкнулся, когда занимался поднятием HistoryAPI на одном из сайтов. К слову, это online-радио, поэтому путешествие по сайту без обновления страницы — принципиально важная задача. В чем заключалась проблема: Первый переход по ссылке после обновления страницы работал прекрасно. А вот дальше начиналась какая-то дьявольщина: одни ссылки продолжали работать как надо, а другие приводили к перезагрузке страницы. Только спустя 2 месяца поисков решения я наконец обнаружил, что сатанеют только те ссылки, которые не были на странице изначально, а оказались там после подгрузки страницы. И сатанеют они, потому что на них не вешается прослушка события клика.
//Вот, где проблема:
$(document).ready(function(){
$('a[href]').click(function(e){
Как работает этот код:
1. Вы зашли на сайт
2. Скрипт ждёт, когда прогрузится страница
3. Сканирует её и вешает на все ссылки с атрибутом href обработчик события клика
4. ВСЁ. На этом его работа окончена, и он умирает.
Внезапно, правда? Но ведь нам нужно обрабатывать ВСЕ ссылки ВСЕГДА.
Первая идея — повесить в функцию-обновлятель-страницы повторное сканирование — но это ни к чему не приводит, да и дополнительный код.
Вторая идея — вернуться к идее onclick=«foo(this)», но мы уже решили, что не стоит так делать.
Быстрое свидание с гуглом, и у нас появляется
решение:
$(document).ready(function(){
$(document).on('click', 'a[href]', function(e){
Этот код работает чуть-чуть по-другому: здесь скрипт ищет в документе… документ. И дальше будет отлавливать все клики, реагируя только на клики по сслыкам с атрибутом href. Звучит странно и необъяснимо похоже на первый вариант, но оно работает.
Итоговый вариант:
Данное решение позволит вам 1 раз написать обработчик ссылок и забыть о необходимости добавления атрибутов ссылкам при создании новых постов; и от необходимости объяснять всем, кто может создавать новые записи на сайте, как правильно вставлять ссылки.
$(document).ready(function(){
var pattern = new RegExp("^(https:\/\/"+location.host+"\/|http:\/\/"+location.host+"\/|\/\/"+location.host+"\/|"+location.host+"\/|\/(?!\/))"), // "^\/(?!\/)" - "начинается с /, но дальше - не /"
pattern_protocol = new RegExp("^(http:\/\/|https:\/\/|\/\/)"), // да, "просто двойной слеш" тоже здесь
pattern_lochost = new RegExp("^("+location.host+")");
$(document).on('click', 'a[href]', function(e){
e.preventDefault();
if(!$(this).attr('href')){console.log('no href'); return false;}
var url = $(this).attr('href'),
isLocal = (pattern.test(url)) ? true : false;
if(isLocal){
console.log('Local link: '+url);
if(pattern_protocol.test(url)){url = url.replace(pattern_protocol, '');}
if(pattern_lochost.test(url)){url = url.replace(pattern_lochost, '');}
//На выходе получаем ссылку без протокола, двойного слеша и домена. Т.е., например, "https://domain.com/page" -> "/page".
//Это нужо делать, ибо если у нас сылка вида domain.com/page, то она честно отдаёт isLocal,
//но открывается через пятую точку - domain.com/domain.com/page
$.ajax({
//У меня логика построена так: я получаю с сервера объект
//string data.title
//string data.url
// bool data.isErrorPage
// bool data.hideSidebar
//и отправляю его в обновлятель страницы, который с помощью $('selector').load(data.url+' selector') меняет содержимое нескольких элементов.
//Но вы можете и по-другому организовать работу.
url: url,
data: 'ajax=true',
success: function(data){
reload_page($.parseJSON(data));
}
});
window.history.pushState(null, null, url);
return false;
}else{
console.log('External link: '+url);
//Если нет протокола или хотя бы двойного слеша, то нужно обязательно добавить, иначе откроется в новом окне, но как location.href/url (например, domain.com/google.com)
//Добавляем http://. В последствие вторая сторона, если имеет https:// - сама перенаправит.
url = (pattern_protocol.test(url)) ? url : 'http://'+url;
window.open(url, '_blank');
}
});
$(window).bind('popstate', function(){
$.ajax({
url: location.pathname+location.search,
data: 'ajax=true',
success: function(data){reload_page(data)}
});
});
});
Если у вас могут попасться ссылки на ftp:// или ssh:// и т.д. — нужно позаботиться об обработке этих ссылок. Я не заботился, т.к. они у меня попадаться не будут.
На этом всё. Надеюсь, было полезно.