Full-Stack PHP/JS developer
0,2
рейтинг
30 октября 2013 в 02:13

Разработка → Прекратите использовать location.hash, да здравствует HTML5 History API!

Много лет location.hash был способом в AJAX-приложении заставить работать кнопки «Назад» / «Вперёд» и, например, добавить определенное состояние страницы в избранное и вернуться к нему позже.

Сейчас, когда HTML5 считается нормой, пора обратить свое внимание на History API и забыть про location.hash. HTML5 History API проще для понимания и позволяет сделать URL чуточку красивее (без кракозябры # или #! если вы имеете дело с индексированием ajax приложения).

Поддержка браузерами


image

Конечно, IE догнал хорошие браузеры только к 10й версии. Лично я в своих проектах не поддерживаю пользователей IE<=9, но это не значит что у вас тоже хорошие заказчики или вы сам себе заказчик :) В любом случае добавить поддержку старых браузеров не составляет никаких проблем, есть большое число библиотек с hash-fallback, например HTML5 History API, представленная автором на Хабрахабре.

Принцип работы


Всю суть History API я могу изложить в одном прокомментированном куске кода:
// Обработчик back/forward событий
window.onpopstate = function(event) {
  console.log("location: " + location.href + ", state: " + JSON.stringify(event.state));
};

// добавить состояние истории
history.pushState({page: 1}, "title 1", "?page=1");
history.pushState({page: 2}, "title 2", "?page=2");

// заменить текущее состояние
history.replaceState({page: 3}, "title 3", "?page=3");

history.back(); // location: http://example.com/example.html?page=1, state: {"page":1}
history.back(); // location: http://example.com/example.html, state: null
history.go(2);  // location: http://example.com/example.html?page=3, state: {"page":3}

console.log(history.state) // Object {page: 3}

Ну правда же легко? :) Не мучайте больше себя и своих коллег работой с location.hash. У него появился достойный преемник — HTML5 History API.
@limonte
карма
69,7
рейтинг 0,2
Full-Stack PHP/JS developer
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (38)

  • +11
    Завидую вам с вашими заказчиками.
    Мне около полугода тому назад пришлось опять IETester ставить. Некоторые заказчики такие заказчики.
  • 0
    Откуда взялось
    location: example.com/example.html, state: null
    и куда делось
    history.pushState({page: 2}, «title 2», "?page=2");
    ?
    • 0
      1. после двух history.back() вы вернетесь к самому первому состоянию, когда сайт был загружен.
      2. Оно было заменено состоянием ({page: 3}, «title 3», "?page=3");

      history.pushState({page: 2}, "title 2", "?page=2");
      
      // заменить текущее состояние
      history.replaceState({page: 3}, "title 3", "?page=3");
      
      • 0
        Нумерация страниц путает ужасно.

        Поясните, пожалуйста, когда мы делаем back(), то переход идёт к первому запушенному состоянию?
        Если так, то я всё ещё не понимаю, куда делась page 2?

        И когда go(2) почему попадаем на page3?
        • +2
          back() — переход к предыдущему состоянию.

          изначально имеем страницу «example.com/example.html». стек состояний пуст.

          history.pushState({page: 1}, "title 1", "?page=1"); // location: http://example.com/example.html?page=1, state: {"page":1}
          // теперь стек сотояний [null, {page: 1}]
          history.pushState({page: 2}, "title 2", "?page=2"); // location: http://example.com/example.html?page=2, state: {"page":2}
          // теперь стек сотояний [null, {page: 1}, {page: 2}]
          history.replaceState({page: 3}, "title 3", "?page=3"); // location: http://example.com/example.html?page=3, state: {"page":3}
          // здесь мы заменили текущее состояние, а не добавили его. поэтому грубо стек состояний таков:
          [null, {page: 1}, {page:3}]
          
          // Переходим назад по истории, получаем состояние {page: 1}
          history.back(); // location: http://example.com/example.html?page=1, state: {"page":1}
          // Переходим назад по истории, получаем состояние null
          history.back(); // location: http://example.com/example.html, state: null
          // Переходим на два состояния вперед, получаем состояние {page: 3}
          history.go(2);  // location: http://example.com/example.html?page=3, state: {"page":3}
          
          • 0
            Теперь всё понятно (:
            Спасибо большое за разжёвывание.
          • +1
            [null] — это не пустой стек
            null это тоже значение
  • +5
    Я для себя давно написал маленький универсальный объект для работы с url.
    Если браузер тянет History api — работаем с ним, если нет — то с hash.

    Вот, может кому-то еще пригодится. Использовать просто:
    Hash.add('key1', 'var1'); // добавляем значение в url
    Hash.remove('key1'); // удаляем 
    
    Hash.set({'key1':'var1'}); // заменяем все значения своим массивом
    Hash.get(); // получаем данные в url массивом
    Hash.clear(); // удаляем все значения в url без перезагрузки
    

    Рабочий пример — просто покликайте по фильтру.
    • +4
      Странно кликаю по фильтру норм и ссылка меняется и содержимое, щелкаю назад меняется только ссылка содержимое как было так и осталось.
      • 0
        Видимо это оттого, что у меня нет обработки back/forward
        window.onpopstate = function(event) {
          console.log("location: " + location.href + ", state: " + JSON.stringify(event.state));
        };
        

        Постараюсь разобраться с этим, как появится немного времени.
    • 0
      Есть готовые полифилы HTML5-History-API от devote, мой форк, которые делают тоже самое, но только прозрачно для пользователя. У себя в форке я ещё добавил событие pagechange которое срабатывает и на методы pushState/replaceState, и на событие popstate
  • 0
    Если вы пушите событие (первый параметр), то могли бы пояснить, как он может использоваться, благо работает он как положено. А вот второй параметр (title), не работает для большинства браузеров, title страницы надо вручную менять через document.title чтобы работало как положено — опять же не помешает добавить в статью.
    • 0
      А вот второй параметр (title), не работает для большинства браузеров, title страницы надо вручную менять через document.title чтобы работало как положено


      Ну это не проблема… Исходя из примера:

      // Обработчик back/forward событий
      window.onpopstate = function(event) {
        console.log("location: " + location.href + ", state: " + JSON.stringify(event.state));
        // меняем title
        document.title = event.state.title;
      };
      
      // добавить состояние истории
      history.pushState({page: 1, title: "title 1"}, "", "?page=1");
      history.pushState({page: 2, title: "title 2"}, "", "?page=2");
      
      // заменить текущее состояние
      history.replaceState({page: 3, title: "title 3"}, "", "?page=3");
      
      • 0
        Все верно, осталось только дописать как хранить динамический контент в event.state, чтобы не перезагружать страницу при нажатиях back и forward, потом убедить автора добавить это в статью и лично я буду абсолютно доволен :)
        • 0
          осталось только дописать как хранить динамический контент в event.state


          Не самый лучший вариант, но тоже имеет право на жизнь :)

          <html>
          <head>
            <title>Some Title</title>
          </head>
          <body>
            <a href="?page=1" title="Title 1">Page 1</a>
            <a href="?page=2" title="Title 2">Page 2</a>
            <a href="?page=3">Page 3</a>
          
            <script>
              window.onpopstate = function (event) {
                document.title = event.state.title;
              };
              var a = document.querySelectorAll('a');
              for (var i=0; i < a.length; i++) {
                a[i].onclick = function() {
                  var title = document.title = this.title ? this.title : this.innerHTML;
                  history.pushState({title: title}, "", this.href);
                    return false;
                  }
                }
              </script>
          </body>
          </html>
          
          • 0
            Вы либо меня не поняли, либо упростили пример так, что потерялась сама идея, хранить в первом параметре состояние\контент динамического блока\блоков (того что вы аяксом грузите н-р).

            И к слову вместо ?page=1 и ?page=2, можно сделать красивые /page1 и /page2 (нужно будет чутка только в .htaccess подшаманить, чтоб букмарки открывались так, как ожидает пользователь), главная идея статьи избавиться от hash, так что почему не сделать симпатичный URL, вообще без параметров сходу.

            Я может придираюсь через чур, но как заметили ниже, статья введение на хабре уже была, раз уж это уже вторая о том же, можно было бы довести до полезного на практике примера.
            • 0
              Вы либо меня не поняли


              И к слову вместо ?page=1 и ?page=2, можно сделать красивые /page1 и /page2
              Это понятно… ?page=1 и ?page=2 взяты из примера самой статьи

              нужно будет чутка только в .htaccess подшаманить, чтоб букмарки открывались так, как ожидает пользователь
              Пишу на python/django по-этому ничего шаманить не надо

              Я может придираюсь через чур, но как заметили ниже, статья введение на хабре уже была, раз уж это уже вторая о том же, можно было бы довести до полезного на практике примера.
              с этим я согласен… но акцент был на поддержку браузеров (то бишь даже ie поддерживает)
    • +1
      А вот второй параметр (title), не работает для большинства браузеров, title страницы надо вручную менять через document.title чтобы работало как положено

      Если внимательно прочесть спецификацию, второй параметр никак не относиться к title страницы. И говорить о том что он не работает, не совсем правильно.

      Суть данного параметра в том, что многие привыкли читать название параметра как title и воспринимать его как title-страницы (грубо сопоставлять с document.title), что является ошибочным. Этот параметр не имеет ничего общего с document.title. Он нужен для того что бы менять имя страницы в истории. Для примера это имя вы можете увидеть при продолжительном нажатии на кнопки back/forward, то есть попробуйте нажать левой клавишей мыши на одну из этих стрелок с небольшой задержкой, откроется список, вот в нем и будут отображаться те самые имена что были занесены во второй параметр.
      • 0
        Для примера это имя вы можете увидеть при продолжительном нажатии на кнопки back/forward, то есть попробуйте нажать левой клавишей мыши на одну из этих стрелок с небольшой задержкой, откроется список, вот в нем и будут отображаться те самые имена что были занесены во второй параметр.

        Параметр title никак к этому не относится!
        В историю вписывается то что в: <title>Some Title</title>
        или в случае с javascript'ом: document.title = "Some Title";
        • 0
          вы так утверждаете лишь потому что браузеры в реальности пишут туда заголовок из document.title, хотя в реальности так работать не должно (то есть должно но не в случае с pushState/replaceState). Хотя например Opera 12 (если моя память не изменяет) пишет то что попадает вторым параметром в pushState/replaceState, то есть поступает правильно в отличии от других браузеров.

          попробуйте открыть в Опере 12 консоль и вбить:
          history.pushState(null, 'Новый заголовок');
          history.pushState(null, 'Новый заголовок 2', '/urlurl');
          

          После проделайте то о чем я писал в посте ранее
          • 0
            Сорри… Да, в Опере 12 работает.
            До этого тестил в Хроме, там это не работает, по этому дал такой ответ.
  • 0
    Стоит отметить что onpopstate в хроме может вызывается при подписываниии (может тк если обернуть в setTimeout, то результат может быть не предсказуем).

    А так для старых браузеров я просто перегружал страницу через window.location = '/url' и имел отличную возможность использовать hash для всякого рода сообщений и выделений внутри страницы.
  • 0
    • 0
      В этой статье нет ни слова о onpopstate, который является неотъемлемой частью History API.
      Но в любом случае хорошо, что она упомянута в комментариях.
      • 0
        Простите, но:
        // Обработчик back/forward событий
        window.onpopstate = function(event) {
        
  • +2
    В статьи не озвучено:
    * полифилы: HTML5-History-API от devote, мой форк (используется у нас в компании)
    * подводные камни при использовании History-API:
    1. разное срабатывание onpopstate при первой загрузке страницы в Webkit/Blink и FF/Opera
    2. забагованые location.[pathname/href] в старых Chrome/Safari (в том числе на старых Android/iPhone) и Opera 12-: эти браузеры разэкранируют спец символы, при получении значений этих свойств, в отличии от последних версий браузеров
    3. Баги Opera 12- с релативными ссылками при использовании History-API: после второго применения History-API node.href указывает на урл, сформированный из предыдущего установленного через History-API значения и относительной ссылки самого элемента a
    4. Баг в последнем Webkit/Blink с History-API + Fullscreen API: Chromium/Safari выходят из полноэкранного режима (активированного через Fullscreen API) при использовании History-API

    В своём форке я исправляю все эти проблемы.

    В общем, использование History-API это не панацея и нужно было ещё допиливать API браузера для реального использования в крупном проекте.
    • 0
      5. Забыл добавить, что в общем-то, использование History-API не такое удобное как хотелось бы. Т.к. при использовании методов pushState/replaceState не происходит никакого события, то остальная часть приложения ничего не знает о том, что урл сменился. Поэтому в своём форке я добавил событие
      pagechange = {
        newUrl: string
        , oldUrl: string
        , popstate: boolean
      }
      

      диспатчится оно на window. Срабатывает и на методы pushState/replaceState и на событие popstate
  • 0
    Вот только с popstate у Chrome до сих пор проблемы. Приходится изобретать разнообразные велосипеды, чтобы оно работало как следует.
    • +3
      Проблем хватает у многих браузерах, пока еще ни один браузер идеально не поддерживает History-API поэтому приходиться прикручивать костыли для тех или иных случаев. В моей реализации библиотеки, о которой упомянул termi я стараюсь избавиться от подобных багов и стабилизировать работу API для всех браузеров одинаково.
      • +1
        Внезапный вывод. «Пользуйтесь современной технологией, давно пора! Правда придётся костыли ваять на каждый браузер...»
  • 0
    У каждой технологии — свои предназначения, и если на сайтах используется ajax + поддержка IE9+ only, то в данном случае статья верная, но если разрабатывается приложение, ну, допустим под VK, под FB, под мэил.ру или одноклассники уж на худой конец, то тут Вы ошибаетесь просто катастрофически, просто потому, что History API просто не работает в айфреймах, и не важно какой браузер.
    • 0
      Никто и не говорит о приложениях под соцсети, поэтому фраза «тут Вы ошибаетесь» не думаю что уместна. Там свои причуды которые нужно выносить в отдельную статью и обсуждения.
      • 0
        В заголовке статьи и в самой статье описано, что location.hash для AJAX — на свалку истории, хистори апи уже ворвался в нашу жизнь — используйте его. Это если в кратце. Я уточнил, что да, это всё хорошо, но не стоит быть столь категоричным.

        Может немного резковато получилось, да.
    • +2
      Один я не понимаю зачем History API использовать в фреймах?
      • –1
        Хотя бы как минимум что бы можно было ходить по истории. Хотя конечно для этих целей можно использовать и location.hash. Но все же на мой взгляд лучше использовать более новые технологии
        • 0
          Так ведь вперед-назад во фреймах все равно работают непонятно как.
          • 0
            Проблем с этим не замечал, вполне нормально работают
            • 0
              Так ведь непонятно (с точки зрения пользователя) когда должен отрабатывать верхний уровень, а когда нижний.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.