javascript

HistoryAPI: Как написать один раз, и чтобы голова не болела

  • вторник, 14 ноября 2017 г. в 03:12:42
https://habrahabr.ru/post/342280/
  • jQuery
  • JavaScript
  • HTML


Доброго времени суток!

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

  1. Они организованы так, что обрабатывают одинаково все ссылки;
  2. Можно отстрелить себе ногу и не понять — почему.

В данной статье рассматривается способ организации работы 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);
   }
  });
 });
});

Что здесь уже плохо:


  1. Этот js одинаково обрабатывает ВСЕ ссылки: внешние, локальные и даже ссылки, которые никуда не перенаправляют (допустим, вы сделали ссылку, открывающую модальное окно). А на дворе, на минуточку, 2017 год. Ссылок, которые никуда не ведут, на разных сайтах очень много; внешние ссылки и вовсе принято (хороший тон) открывать в новой вкладке.
  2. Как только у вас в изменяемом блоке появится локальная ссылка — вы обречены. Потому что она не будет обрабатываться вашим скриптом, и вы даже не будете понимать — почему.

Что же делать?


Проблема 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:// и т.д. — нужно позаботиться об обработке этих ссылок. Я не заботился, т.к. они у меня попадаться не будут.

На этом всё. Надеюсь, было полезно.