Изменение URL без перезагрузки страницы
1 ноября 2013 HTML5 66 комментариев 68481 просмотр
С развитием WEB понятие о странице немного изменилось. Для конечного пользователя улучшением производительности и комфорта путешествия по сайту, для разработчиков - немного иной реализацией. Понятно, что страница целиком состоит из множества элементов, большая часть которых не изменяется при переходе на новую страницу. Зачем заставлять пользователя смотреть по 100 раз как рендерится один и тот же документ?

Конечно, было бы правильней изменять только ту часть, которая действительно изменятся. Это стало чуть ли не стандартом в WEB >= 2.0. А именно асинхронные переходы. Теперь можно делать различную анимацию между переходами, начиная от простых gif-loading, заканчивая 3D-transition.

Проблема

Ссылки являются основной точкой возврата к определенному состоянию сайта. И было бы хорошо, чтобы после открытия страницы асинхронно менялась ссылка, которую человек может сохранить, отправить другу и т.д. Но когда-то не было возможности просто так из JS менять ссылку без полной перезагрузки страницы.

Решение 1

Которое я использовал около года. Это хэши. В JS легко можно установить hash страницы, написать функцию, которая его обработает и т.д. Примерно было так:

function handleHash() {
    var h = window.location.hash.substring(1).replace(/&/, '');

    switch (h) {
        case 'profile':
            // ...
        case 'dashboard':
            // ...
    }
}

document.getElementById('profile-link').onclick = function() {
    window.location.hash = 'profile';
    handleHash();
}

Но почему-то мне это никогда не нравилось. Наверное, потому что используются вещи, которые по сути для этого не предназначены. И не работали браузерные кнопки “Назад” и “Вперед”. А потом я увидел сайт, который открывает страницы и меняет физический URL. Сразу я думал, что это как-то связано с технологиями. Но только недавно я понял в чем дело и нашел решение.

Решение 2. Правильное.

Первая часть с асинхронной загрузкой остается такой же. А вот для изменения URL нам нужен HTML5 History API. Это путь-стандарт для управления историей браузера из JS. На самом деле, это всего лишь несколько методов в JS. Появился этот стандарт не так давно, но уже смело можно его использовать, кроме IE.

Firefox4.0 +
Safari5.0 +
Chrome8.0 +
Opera11.50 +
iPhone4.2.1 +
IE-
Android-


Для браузеров, которые не поддерживают History API можно написать костыль с хэшами. Проверить, работает ли в вашем браузере эта фича можно так:

function isHhistoryApiAvailable() {
    return !!(window.history && history.pushState);
}

А теперь сделаем небольшое Демо. У нас будет страница с общим шаблоном (index.php):


<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <script src="/js/jquery-2.0.3.min.js"></script>
</head>
<body>
    <ul>
        <li><a href="/page1.php">page 1</a></li>
        <li><a href="/page2.php">page 2</a></li>
        <li><a href="/page3.php">page 3</a></li>
    </ul>
    Content:
    <div id="content">
    </div>
</body>
</html>

Это общий шаблон, динамическое содержимое будет находиться в #content. Соответственно, страницы pageN.php не будут содержать в себе всего шаблона, а только контент. jQuery я подключил только для того, чтобы было меньше кода, так как будет ajax. Ниже приведен JS, который получает при клике на ссылку содержимое и вставляет его в #content.

$(document).ready(function() {
    $('a').click(function() {
        var url = $(this).attr('href');

        $.ajax({
            url:     url + '?ajax=1',
            success: function(data){
                $('#content').html(data);
            }
        });

        // А вот так просто меняется ссылка
        if(url != window.location){
            window.history.pushState(null, null, url);
        }

        // Предотвращаем дефолтное поведение
        return false;
    });
});

Параметр ajax=1 передается для того, чтобы сервер определил, что нам нужно. При начальной загрузке страницы этот параметр не будет отправлен, в этом случае возвращаем меню и скрипт. Если же параметр передан, то возвращаем только контент.

И еще небольшой кусочек кода, который делает рабочими кнопки “Вперед” и “Назад”.

$(window).bind('popstate', function() {
    $.ajax({
        url:     location.pathname + '?ajax=1',
        success: function(data) {
            $('#content').html(data);
        }
    });
});

Как это может выглядеть? Демо
По просьбам комментирующих выкладываю Исходники
66 комментариев
SEO

Еще плюс такого подхода перед location.hash в том, что ссылки получаются семантические и нигде с ними не будет проблем, они будут также определяться поисковыми индексаторами.

Александр

Спасибо большое за статью, очень полезная. Я проделал все действия, описанные во втором решении (через Jquery) т.е в index.php залил вышеуказанный код+скрипт и создал 3 тестовых файла page1.php page2.php page3.php с текстовым содержимым Но при загрузке скрипта по http://адрес/index.php контент страницы дублируется и начинается нормально работать только после первого клика. page 1 page 2 page 3 Content: page 1 page 2 page 3 Content:

Подскажите пожалуйста что я сделал не так?

@Александр, когда вы заходите на страницу, то у вас изначально возвращается HTML из PHP, а потом еще срабатывает событие popstate. Правильно будет разделить запросы, которые отвечают за начальную загрузку и за загрузку контента. Это можно сделать передачей параметра ajax=1 из JS. А на сервере проверять: если есть этот параметр, то возвращать только контент. В противном случае возвращать только меню. Я отредактировал статью, чтобы было более понятно.

Александр

Уважаемый Александр.
Если Вас не затруднит, могли бы разместить под разделом "Как это может выглядеть? Демо" сам демо скрипт на скачку.
К сожалению у меня так и не получилось въехать как именно должно все быть.
На данный момент я имею файл index.php в котором находится ваш шаблонный код, а также два скрипта которые вы описали выше.
и три файла page1.php page2.php page3.php в которых нет скриптов, просто размещен текст.
Но когда я открываю index - происходит дублирование как и писал выше.

Я так понимаю вторую часть скрипта popstate нужно подгружать не в index.php а в контенте? Выложите пожалуйста ваш демо пример на скачку.

Tim

Что-то с вашими исходниками ничего не ясно. Демо работает замечательно, а локально просто отображаются все три страницы одна под другой, первая в блоке content. Нажатие на page1, page2 просто открывает эти страницы...

Поддерживает ли HTML5 History API в вашем браузере? В статье указан способ проверки.

Tony

Будут ли новые загруженные элементы добавлены в DOM при подгрузке , для дальнейшей роботы с ними?

Tony, конечно, разве это не делается в строке "$('#content').html(data);"?

reel push mower

Thanks for your personal marvelous posting! I quite enjoyed reading it, you may be a great author.I will be sure to bookmark your blog and may come back in the foreseeable future. I want to encourage youu to definitely continue your great work, have a nice holiday weekend!

Dmitry

У части пользователей такой подход не работает из-за noscript. Подход раздутый. Юзается ява, которая конечно есть айс, но не необходимость при серфинге.

Dmitry, вы в каменном веке застряли? Разработчикам уже давно пора забыть о том, что есть браузеры без JavaScript (да, называйте это так, Java - это совсем другое). И если подход раздутый, то почему тогда VK его использует, почему его использует Яндекс?

Владислав

Задумка весьма интересная, но как ее реализовать если сайт динамический и имеет адреса index?page= , поможет кто-нибудь?

Дмитрий

Добрый день! Всё сделал, как Вы описали, но при клики на меню, все пункты меню дублируются. Как это можно исправить? Спасибо!

Дмитрий, скорее всего ваш AJAX запрос возвращает страницу целиком. Попробуйте мои исходники и разберитесь с ними. Страница целиком не должна отдаваться, должен отдаваться шаблон, а в него уже при клике или на событие popstate должен грузиться контент.

Дмитрий

Надпись "Hello. It's History API" Отображается только в опере, остальные браузеры её не выводят. Что можно сделать, чтобы выводилось во всех браузерах?

@Дмитрий, я не понимаю о какой надписи идет речь.

Александр

Хотелось бы видеть как выглядит костыль для осла! Спасибо!

Так тут и костылей никаких не надо. Это, конечно, больше вопрос субъективный. Но я для осла (точнее для браузеров, не поддерживающий HTML5 history API) просто не включаю этот обработчик клика, соответственно происходит обычное перенаправление на страницу. Не вижу ничего плохого.

Александр

Спасибо! Я уже понял. Скажите, а такую технику можно применять к статичным сайтам, не использующим php? Хотелось бы менять не только блок с контентом, но и подключаемые скрипты для каждой страницы.

Загрузил для наглядности ваше демо на хостинг - при переходе по страницам адрес меняется, а вот содержимое одно и тоже - запись, что в файле index.php прописана: "Hello. It\'s History API". Оно все время висит.

Спасибо, еще раз!:-)

@Александр, я же не знаю, что у вас на хостинге? Если бы вы дали доступ, то я бы посмотрел. А технику можно использовать для статики тоже, она же реализована на JS, просто контент подгружать не с PHP скриптов, а из html страниц. Тут небольшая доработка нужна, но только в пути для загрузки контента.

Александр

Спасибо! Саш, а не могли бы Вы написать эти самые изменения в скрипте? Как должно быть? Я на эту тему очень много информации нашел - целая папка с закладками в Safari, но лучше, чем у Вас, ни у кого не написано. На Ruseller есть одна статья, но там нет изменеий в url, да, и датируется она 2010 годом. На хостинг выгружу папку с Вашим пример, ссылку скину.

@Александр. Пример такой. У вас есть index.html, contacts.html. На этих страницах содержится общий шаблон страницы с JS кодом (чтобы не дублировать, лучше сделать инклудами). В каждом есть div #content. Если мы запрашиваем напрямую эту страницу, то вернется сразу div с контентом. Если нажимаем на ссылку, то нужно только поменять контент. Сам контент можно хранить, например, в файлах index-content.html и contacts-content.html. Они же инклудятся в index.html, contacts.html. Изменения в JS будут небольшие, нужно только поменять URL, чтобы при нажатии на ссылку, получался только контент. Поменяем параметр url в ajax методе. url: 'index-content.html' или url: 'contacts-content.html'. Тем самым в div мы загрузим только контент. Ссылка поменяется, например на contacts.html. Обновляем страницу, загружается обычный contacts.html с уже вставленным контентом..

bukkake

Encore un magnifique poste, j'espère en parler dans la semaine avec certains de mes amis

webman1988

Спасибо за пример! Все работает как надо. Но если ссылки открывать в новом окне или в новой вкладке, то контент не будет подгружен. Чтобы исправить это, необходимо в скрипте где проверка на наличие GET['ajax'], в логике else перед include шаблона сделать запрос в бд и поместить результат например в переменную cont. А в скрипте шаблона в теге div#content вывести переменную cont. Может, кому пригодится.

Александр

Хорошо. Все, в принципе, понятно. Но можно как-нибудь обыграть ситуацию с тем, что не использовать отдельные html-документы с "вставляемыми" блоками, а работать с полноценными страницами, из которой бы "брался" нужный блок? Много времени у меня уйдет на создание "отдельных" страниц. Вот, как здесь. Спасибо!

webman1988, спасибо, конечно пригодится.

Александр, можно как в примере по вашей ссылке. Но мне не нравится тот вариант тем, что там весь контент грузится изначально и меняется только opacity у контейнера. По мне так это bad practice.

Александр

Просто мне не понятно: как все это будет выглядеть в плане SЕО? Страницы с общим каркасом, страницы - с контентом. Для статики все сделал. Единственная проблема - не работают скрипты. Ни один. Пробовал их оставлять - безрезультатно. Не могу понять что я делаю не так? И еще вопрос: а как же быть с заголовками и описанием страницы?

Александр, к сожалению, не могу о ваших скриптах ничего сказать, так как их не видел. А по поводу заголовка страницы, его тоже можно менять динамически меняя document.title

Александр

Извиняюсь за назойливость, но в Интернете по этой теме ничего дельного нет, кроме Вашей статьи. И никто однозначного ответа дать не может. Немного переделал:

$(function($){
  $('.menu-tabs a').attr('onclick','return false;').click(function(){
    var href = $(this).attr('href');
    $('.wrapper').load(href+' .wrapper-inner', function(){
      history.pushState(null, null, href);
    });
  });
});
А вот как активировать кнопки "Вперед", "Назад". Не получается. Видимо, знаний не хватает. Перед прописал ссылки на скрипты, при переходе на другую страницу, они уже не работают. С заголовками разобрался.

Вот теперь я понял, как вы делаете. При обычном заходе вы грузите страницу целиком, а при запросе выдергиваете только определнный див из этой страницы? Гипотетически, вам нужно работать над popstate:

$(window).bind('popstate', function() {
  var href = location.pathname;
  $('.wrapper').load(href+' .wrapper-inner');
});

Александр

Да, все верно! Все никак не освою php. Мне он каким-то замороченным кажется. Построил сайт чисто на html, но в скором будущем собираюсь перейти на динамику. А почему не работают скрипты? Пробовал их во .wrapper-inner вставлять и просто оставлять - не работают. Видимо, не происходит переинициализация, так как при обновлении страницы Cmb+R все сразу активируется.

Александр, я очень Вам благодарен. Ваш блог для меня настоящая находка - изначально добавил его в закладки.

Денис

Здравствуйте, не могу понять, почему в условии if(url != window.location) мы сравниваем строку (url) с объектом (window.location)?

Александр

Перечитал массу статей в интернете, связанных с темой отказа обработки событий для элементов DOM, загруженных через AJAX. Кто-то советует для обработчиков прописывать .live(), кто-то - повторно инициализировать плагины для появившихся объектов. Хотелось бы знать Ваше мнение? Третий день бьюсь - пока результатов мало.

@Денис, спасибо, что обратили внимание. Да, на самом деле нужно было бы сравнить с window.location.href. Благодаря вашему комментарию я даже задумался и убрал эту проверку из статьи, читателей она может только сбить с толку, а действия от нее никакого. Так что спасибо.

@Александр, нужно смотреть конкретный пример. Что быстрее работает, то и использовать.

irakli

zdrastvuite, mojete padskazat kak sdelat tak shtobi informacia vivodilas iz db

@irakli, извините конечно, но какое это имеет отношение к топику?

irakli

Плютов Саша, mne interesna iesli vazzmojna pri zamene stranic shtobi informacia vivadilas iz msql tablic i isho adna prablema ia ustanovil kod no kagda zagrujaiu stranicu vivoditsia page1 page 2 page 3 a sam kontent net ono paevliaetsia posle najatie page1 a posle perezagruzki apiat ischezaiet iesli pamojete budu blagadaren

Katya

Здравствуйте, большое спасибо за статью! webman1988 писал что что бы по открытию в новом окне или по ссылке подгружался контент нужно поместить результат в переменную и затем вывести в теге div#content. Можете пожалуйста показать как именно это сделать?

Katya, думаю, что решение webman1988 излишне. Посмотрите моё демо, оно полностью повторяет код из статьи. Если нажать на page2 (открыть в новой вкладке, то коне=тент подгрузится).

Katya

Демо работает отлично, но вот исходники, как не меняй при открытии новой вкладки не подгружают контент. Все что я могла увидеть из кода страницы демо я сравнила, но это мне не помогло. Может стоит изменить что то в коде подгружаемых страничек?

Katya, вы правы, в исходниках я это не предусмотрел. Я их обновил, скачайте еще раз архив. Спасибо за то, что заметили!

Костярин

А с этой темой будет работать Disqus или Hypercomments? Или их нужно перезагружать вместе с контентом. Есть информация?

webman1988

Интересно - будут ли гугл и яндекс видеть глубину просмотра пользователей без перезагрузки страницы? И как это скажется на поведенческих факторах?

Михаил

Как сделать, что бы если страница отсутсвует, то оно либо alert показывало, либо на мою страницу 404 отправляло?

Статья интересная! Спасибо.

А можно как-то это способ применить для конкретных ссылок, а не для всех? Ну, например, группа ссылок заключённая в div или каждая ссылка будет иметь идентификатор. Как-то так

Андрей

Доброго времени суток! Все работает благодарю за примеры! Вопрос: каким способом заставить работать меню на сайте. Сайт сделан по вашему "рецепту", Меню в шаблоне, контент в отдельных страницах. Задача подсветить или как то выделить пункт меню в месте пребывания (меню в одну строчку). Какой способ лучше взять.

Ну это по-моему несложно. Если открывается страница изначально без ajax запроса, то вы можете передать какую ссылку подсвечивать. И нужно будет добавить JS, который при клике на ссылку меняет подсветку на текущий пункт меню.

Andrey

это все замечательно, но у меня ajax. Затем здесь и пишу.

Тогда если есть AJAX, то есть и JS, при клике на ссылку выделяйте ее. Думаю, это не касается этой статьи.

ZICK

скриптик реально рульный, но у меня проблема, в зоне контента загружается повторно весь шаблон сайта, как этого избежать? лопатить весь движок чтобы подставить значение параметра GET[ajax] не вариант, может есть другой костыль?

Ответ для Ивана - чтобы сделать группу ссылок, достаточно их запихнуть в спан, чтобы диз не поплыл, я сделал так - <a.....>ссылка в JS надо заменить $('a').click(function() на $('#linkid a').click(function()

Михаил

ZICK, как вариант использовать функцию load ... при помощи же нее загружать со страницы определнный контент, помеченный id`om

ZICK спасибо, но я уже разобрался. Проблема была в том, что идентификатор можно использовать только один раз, я об это забыл. Стал ссылки помечать через class, а не id и стало всё на свои места. Да способ изложенный здесь просто замечательный, немного доработав его под свои нужды я получил то, что хотел.

ZICK

Михаил, у меня все страницы подгружаются инклудом, тобишь нет уникальных айди для контента

ZICK

Люди добрые, дайте ответ пожалуйста

ZICK я сделал так, исключил из скрипта тот контент, который не нужен таким вот способом if (!$SERVER["HTTPXREQUESTEDWITH"] == "XMLHttpRequest") { .. }

Спасибо Вам большое за дельную статью! Сейчас буду разбираться в технологии и повышать свою карму)

Николай

Александр Здравствуйте. Я начинающий программист, воспользовался вашим скриптом, но возникла проблема, когда я возвращаюсь на главную, сайт дублируется в контент, как бы это обойти? Я так понял выше это описывалось, хотелось бы по подробней, "для тупых". Заранее СПАСИБО.

Владислав

Александр, спасибо за скрипт. Сделал тоже самое с минимальными изменениями. Один вопрос: как это всё поднять на дивах, а не на гиперссылках? Как заставить js реагировать на div-ы?

Дмитрий

Тяжело всё это делать когда сталкиваешься с этим в первый раз. Описание есть - а что куда вставить к себе на сайт или прописать НЕ ПОНЯТНО. Буду признателен за подробные подсказки :)

akucher

Чтобы не было двойного инклуда, в самом начале index.php добавьте что-то вроде ~~~ ~~~

Чтобы работали якоря, в функции ПОСЛЕ кода ~~~ $('a').click(function() { var url = $(this).attr('href'); ~~~ добавьте ~~~ if(url.charAt(0) == "#") return; ~~~ т.е. чтобы к ссылкам вида "#home" не добавлялось '?ajax=1'

Alex

Привет! А возможность видоизменять текущую ссылку в зависимости от того активна она или нет Вы видите? Иными словами при нажатии на определённую ссылку прикручивать к ней или к li, класс, к примеру active, с отличными от defoultных css свойствами и чтобы при reload page, класс active присутствовал у текущей ссылки?

А как поменять title у страниц при ajax навигации? )

Andrew

akucher Чтобы не было двойного инклуда, в самом начале index.php добавьте что-то вроде ~~~ ~~~

Тоже интересует решение проблемы с двойным инклюдом. akucher, напишите пожалуйста, еще раз, что надо написать, а то ваш ответ заменился на ~~~ ~~~