Статьи

Четыре SPI-аромата: использование хешбангов с сохранением состояния

Это первая из четырех статей серии о том, как создать общедоступные веб-сайты с одностраничным интерфейсом (SPI), совместимые с SEO и построенные на основе веб-фреймворка ItsNat на основе Java. 

Мы собираемся описать в этих статьях четыре различных метода:

1. Состояние с использованием Hashbangs

2. Безгражданство с использованием Hashbangs

3. Состояние с использованием History API

4. Без сохранения состояния с помощью History API

Первая статья объяснит многие концепции впервые, следующая статья будет легче, потому что многие вещи уже были объяснены ранее и так далее.

Предполагается его Нат 1.4 или выше.

Давайте перейдем к первому подходу: 

Как создать общедоступный веб-сайт с одностраничным интерфейсом (SPI), совместимый с SEO, используя State-Hashbangs .

Вступление

Парадигма одностраничного интерфейса (SPI) не нова, в наши дни SPI очень популярен в «веб-приложениях», потому что современные веб-фреймворки с поддержкой AJAX делают такие приложения проще, чем когда-либо.

Однако ItsNat — это веб-фреймворк, сильно ориентированный на веб-сайты с одностраничным интерфейсом   (а также приложения).

В этом руководстве показано, как создавать  веб-сайты SPI , веб-сайты на основе страниц можно превратить в SPI, избегая ненужной перезагрузки без потери преимуществ веб-сайтов на основе страниц, таких как закладки, поисковая оптимизация (SEO) или экстремальная доступность (JavaScript отключен) в соответствии с  Одностраничный манифест интерфейса .

Существует несколько вариантов создания веб-сайта SPI с ItsNat, он может быть с состоянием или без состояния, основываясь на хэш-бангах (#!) Или с помощью History API.

Из этого туториала вы узнаете, как сделать веб-сайт ItsNat SPI с  сохранением состояния  с помощью  hashbangs .

Мы сможем создавать веб-сайты SPI с SEO, закладками, кнопками «назад» и «вперед» (навигация по истории), а с некоторыми хитростями мы все еще можем использовать «счетчики посещений», например, Google Analytics, несмотря на пользовательскую навигацию по интерфейсу с одной страницей. Это будет объяснено с помощью кода.

Чтобы понять этот урок, необходимы некоторые базовые знания ItsNat.

Как веб-сайт может быть SPI и страница основана одновременно?

Несмотря на недавнюю поддержку JavaScript поисковой системой Google при  сканировании веб-сайтов , лучший способ получить высокий рейтинг в поисковых роботов и убедиться в отсутствии проблем с SEO — это игнорировать JavaScript. Если JavaScript игнорируется, никакие запросы AJAX не выполняются, тогда ваш сайт не является SPI, это хорошо для веб-сканеров. ItsNat — это технология, которая позволяет веб-сайтам перемещаться по страницам, когда JavaScript игнорируется, и по одной странице (без перезагрузки, без навигации по страницам, другими словами, имитация навигации по страницам), когда выполняется JavaScript.

Типичное поведение ItsNat следующее: при изменении DOM на сервере код JavaScript автоматически генерируется и отправляется клиенту для соответствующего обновления DOM клиента. Обычно на веб-сайтах разделяются многие элементы, такие как верхний и нижний колонтитулы, стили, библиотеки JavaScript и т. Д., А область контента — это почти единственная полностью изменяемая зона, в SPI эту «область контента» можно изменить с помощью новых фрагментов HTML, фрагмента HTML динамически вставляется в документ DOM на сервере с использованием W3C DOM Java API, в то же время этот новый HTML-код также автоматически вставляется с помощью кода JavaScript в клиенте (обычно с использованием innerHTML), к сожалению, этот подход не подходит для SEO, так как сканеры могут игнорировать JavaScript ,

Чтобы обеспечить имитацию страницы, ключом является функция ItsNat:  режим быстрой загрузки .

Если ItsNat сконфигурирован в режиме быстрой загрузки (режим по умолчанию), при загрузке начальной страницы (на основе чистого HTML-шаблона) любое изменение DOM, выполненное на сервере для исходного шаблона кодом разработчика, не отправляется как JavaScript, DOM сериализуется для генерации исходной HTML-страницы, отправляемой клиенту. Следовательно, один и тот же пользовательский код, манипулирующий DOM, может генерировать JavaScript или обычный HTML в зависимости от выполняемой фазы, JavaScript при получении события AJAX или HTML во время загрузки начальной страницы, вставляя требуемый фрагмент HTML в зону содержимого, избегая типичного » подход «два сайта» (сайт для конечных пользователей и сайт для сканеров) или что-то вроде кэша страниц сайта.

Когда режим быстрой загрузки отключен, используемый шаблон представляет собой разметку, отправляемую клиенту, а изменения DOM на сервере отправляются как операции DOM JavaScript, это не подходит для SEO и не рекомендуется для веб-сайтов, совместимых с SPI SEO. В этом уроке мы будем использовать режим быстрой загрузки.

На веб-сайте SPI ваши клики обычно заменяют части веб-страницы новыми фрагментами с использованием AJAX (ItsNat чрезвычайно эффективен для выполнения этой задачи с использованием чистых фрагментов HTML), когда JavaScript игнорируется, щелкающие ссылки выполняются после  href атрибута. Как href правило, определяют URL-адрес «страницы», ItsNat будет загружать эту страницу как обычный сайт на основе страниц, таким образом, сканер веб-страниц видит сайт «постраничным». И альтернатива  onclick присутствует для одностраничной навигации (на основе AJAX).

Если  href ссылка (компонент навигации в целом) определяет URL-адрес страницы, на которую можно добавить закладку, эта страница является «фундаментальным состоянием» в соответствии с терминологией Манифеста SPI. Фундаментальное состояние может быть просканировано как страница, и в то же время вы можете перейти к этому состоянию без перезагрузки страницы, когда JavaScript не игнорируется (выполняется onclick), в обоих случаях при навигации по странице игнорируется навигация по JavaScript или AJAX, Конечное состояние страницы браузера будет таким же. Это искусство и магия одностраничных веб-сайтов, совместимых с SEO-интерфейсом.

В этом уроке мы собираемся использовать параметр в части запроса URL-адреса в ссылках, чтобы указать основное состояние / страницу,  ?st=state и в то же время хэш-банг (#! St = состояние) для обеспечения навигации по состоянию без перезагрузки при выполнении JavaScript. Как вы знаете, мы можем изменить URL-адрес вручную или с помощью window.location добавления  # части к URL-адресу без перезагрузки страницы, этот метод позволяет нам делать закладки, а сканеры также распознают их для создания закладок.

Настройка веб-приложения

ItsNat не требует специальной настройки или серверов приложений, достаточно любого контейнера сервлетов, поддерживающего Java 1.6 или выше. Используйте предпочитаемую среду IDE для создания пустого веб-приложения ItsNat, в этом руководстве  spistfulhashbangtut в качестве имени веб-приложения используется порт 8080.

Создание сервлета ItsNat

ItsNat не требует начальной загрузки, сервлета ItsNat по умолчанию не существует, фактически вы можете использовать несколько сервлетов с помощью ItsNat в своем веб-приложении. Используя вашу IDE, добавьте новый класс сервлета с именем  servlet (это имя не обязательно) в пакет по умолчанию (опять же, не обязательно).

Регистрация сервлета по умолчанию в  web.xml действительна, этот пример не требует специальных параметров инициализации или фильтров  web.xml.

В соответствии с этой настройкой URL-адрес нашего сервлета (предполагается 8080):

HTTP: // локальный: 8080 / spistfulhashbangtut / сервлет

Поскольку наш веб-сайт является SPI, мы хотели бы иметь более красивый URL, например

HTTP: // локальный: 8080 / spistfulhashbangtut /

У нас есть два варианта:

1. Добавьте сервлет в качестве файла приветствия в  web.xml

<welcome-file-list>
    <welcome-file>servlet</welcome-file>
</welcome-file-list>

2. Добавьте простое  index.jsp (этот файл обычно по умолчанию является «файлом приветствия») с таким содержанием: 

<jsp:forward page="/servlet" />

в  web.xml

<welcome-file-list>
    <welcome-file>index.jsp</welcome-file>
</welcome-file-list>

Теперь замените сгенерированный код  servlet класса следующим кодом:

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import org.itsnat.core.ItsNatServletConfig;
import org.itsnat.core.ItsNatServletContext;
import org.itsnat.core.http.HttpServletWrapper;
import org.itsnat.core.http.ItsNatHttpServlet;
import org.itsnat.core.tmpl.ItsNatDocumentTemplate;
import org.itsnat.spistfulhashbangtut.SPITutGlobalEventListener;
import org.itsnat.spistfulhashbangtut.SPITutGlobalLoadRequestListener;
import org.itsnat.spistfulhashbangtut.SPITutMainLoadRequestListener;

public class servlet extends HttpServletWrapper
{
    public void init(ServletConfig config) throws ServletException
    {
        super.init(config);

        ItsNatHttpServlet itsNatServlet = getItsNatHttpServlet();

        ItsNatServletContext itsNatCtx = itsNatServlet.getItsNatServletContext();
        itsNatCtx.setMaxOpenDocumentsBySession(4); // To avoid abusive users

        ItsNatServletConfig itsNatConfig = itsNatServlet.getItsNatServletConfig();
        itsNatConfig.setFastLoadMode(true); // Not really needed, is the same as default

        String pathBase = getServletContext().getRealPath("/");
        String pathPages =     pathBase + "/WEB-INF/pages/";
        String pathFragments = pathBase + "/WEB-INF/fragments/";

        itsNatServlet.addEventListener(new SPITutGlobalEventListener());
        itsNatServlet.addItsNatServletRequestListener(new SPITutGlobalLoadRequestListener());

        ItsNatDocumentTemplate docTemplate;
        docTemplate = itsNatServlet.registerItsNatDocumentTemplate("main","text/html",pathPages + "main.html");
        docTemplate.addItsNatServletRequestListener(new SPITutMainLoadRequestListener());

        docTemplate = itsNatServlet.registerItsNatDocumentTemplate("google_analytics","text/html",pathPages + "google_analytics.html");
        docTemplate.setScriptingEnabled(false);

        // Fragments
        itsNatServlet.registerItsNatDocFragmentTemplate("not_found","text/html",pathFragments + "not_found.html");
        itsNatServlet.registerItsNatDocFragmentTemplate("overview","text/html",pathFragments + "overview.html");
        itsNatServlet.registerItsNatDocFragmentTemplate("overview.popup","text/html",pathFragments + "overview_popup.html");
        itsNatServlet.registerItsNatDocFragmentTemplate("detail","text/html",pathFragments + "detail.html");
        itsNatServlet.registerItsNatDocFragmentTemplate("detail.more","text/html",pathFragments + "detail_more.html");
    }
}

Как вы можете видеть, наш сервлет наследуется от  HttpServletWrapper:

Этот класс ItsNat перенаправляет любой запрос к  ItsNatHttpServlet объекту, обертывающему экземпляр сервлета.

Конфигурация веб-приложения выполняется стандартным  init(ServletConfig) методом, конфигурация в ItsNat является «классической», обязательной, вызывающей методы конфигурации.

itsNatCtx.setMaxOpenDocumentsBySession(4); // To avoid abusive users

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

itsNatConfig.setFastLoadMode(true); // Not really needed, is the same as default

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

String pathBase = getServletContext().getRealPath("/");
String pathPages =     pathBase + "/WEB-INF/pages/";
String pathFragments = pathBase + "/WEB-INF/fragments/";

Папка  WEB-INF/pages будет использоваться для сохранения «шаблонов страниц ItsNat», поскольку наше веб-приложение SPI требует только одного шаблона страницы, позже будет добавлена ​​еще одна очень простая страница для отслеживания посещений «страниц». В папку  WEB-INF/fragments будут сохранены «фрагменты страницы», фрагменты страницы — это чистые HTML-страницы, где используется только содержимое <body> (или <head>).

itsNatServlet.addEventListener(new SPITutGlobalEventListener());

SPITutGlobalEventListener является глобальным  org.w3c.dom.events.EventListener, все запросы AJAX, полученные этим сервлетом (для любого документа, загруженного этим сервлетом), сначала отправляются этому слушателю. Это код:

package org.itsnat.spistfulhashbangtut;

import org.itsnat.core.ClientDocument;
import org.itsnat.core.event.ItsNatEvent;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventListener;

public class SPITutGlobalEventListener implements EventListener
{
    public SPITutGlobalEventListener()
    {
    }

    @Override
    public void handleEvent(Event evt)
    {
        ItsNatEvent itsNatEvt = (ItsNatEvent)evt;
        if (itsNatEvt.getItsNatDocument() == null)
        {
            StringBuilder code = new StringBuilder();
            code.append("if (confirm('Expired session. Reload?'))");
            code.append("  window.location.reload(true);");
            ClientDocument clientDoc = itsNatEvt.getClientDocument();
            clientDoc.addCodeToSend(code.toString());
            itsNatEvt.getItsNatEventListenerChain().stop();
        }
    }
}

Когда веб-браузер конечного пользователя отправляет событие из документа клиента, потерянного на сервере (обычно из-за истечения срока действия сеанса пользователя), метод ItsNatEvent.getItsNatDocument() возвращает значение NULL, поскольку это событие « потерянное», на сервере  нет документа, подключенного к клиенту документ (истек и удален на сервере). В этом случае мы просим конечного пользователя перезагрузить страницу, вы можете перезагрузить, не спрашивая.

Звонок:

itsNatEvt.getItsNatEventListenerChain().stop();

на самом деле не нужен, он предотвращает вызов других слушателей глобальных событий (в этом уроке больше нет).

Этот  SPITutGlobalEventListener класс на самом деле не нужен, потому что стандартное поведение ItsNat, обрабатывающего событие-сирота, заключается в автоматической перезагрузке страницы (единственное отличие заключается в том, что конечный пользователь не запрашивается).

Следующий в сервлете:

itsNatServlet.addItsNatServletRequestListener(new SPITutGlobalLoadRequestListener());

Регистрирует слушателя глобального документа (страницы). Этот слушатель вызывается, когда запрашивается загрузка страницы (= document) или любой другой неизвестный запрос страницы (не события AJAX). Исходный код:

package org.itsnat.spistfulhashbangtut;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.itsnat.core.ItsNatDocument;
import org.itsnat.core.ItsNatServletRequest;
import org.itsnat.core.ItsNatServletResponse;
import org.itsnat.core.event.ItsNatServletRequestListener;

public class SPITutGlobalLoadRequestListener implements ItsNatServletRequestListener
{
    @Override
    public void processRequest(ItsNatServletRequest request, ItsNatServletResponse response)
    {
        ItsNatDocument itsNatDoc = request.getItsNatDocument();
        if (itsNatDoc == null)
        {
            // Requested with a custom URL, not ItsNat standard format,
            // for instance servlet without params or Google AJAX crawling.
            // Internal redirection specifying the target template page:
            ServletRequest servRequest = request.getServletRequest();
            String gAnalytState = servRequest.getParameter("ganalyt_st");
            if (gAnalytState != null)
                servRequest.setAttribute("itsnat_doc_name","google_analytics");
            else
                servRequest.setAttribute("itsnat_doc_name","main");
            ServletResponse servResponse = response.getServletResponse();
            request.getItsNatServlet().processRequest(servRequest,servResponse);
        }
    }
}

Если запрос включает параметр по умолчанию  itsnat_doc_name и указанный шаблон страницы существует, метод  ItsNatServletRequest.getItsNatDocument() возвращает ненулевой  ItsNatDocument объект, если запрошенный URL-адрес не включает  itsnat_doc_name в себя «пользовательский» запрос и getItsNatDocument() возвращает ноль. В последнем случае речь идет о симпатичных URL, этот учебник не используется  itsnat_doc_name в публичных URL.

Сначала мы проверяем, присутствует ли параметр  ganalyt_st , этот параметр будет использоваться позже для мониторинга состояния с помощью Google Analytics. Если этот параметр отсутствует, это ожидаемый случай запроса, подобного этому:

HTTP: // локальный: 8080 / spistfulhashbangtut /

В этом случае нам необходимо загрузить главную страницу нашего веб-сайта, поэтому  в качестве атрибута запроса itsnat_doc_name указывается значение main, значением mainявляется имя шаблона главной страницы нашего веб-сайта SPI, сначала его ItsNat проверяет, itsnat_doc_name был ли  указан атрибут запроса а затем в качестве параметра запроса. Теперь мы готовы повторно отправить запрос на вызов ItsNat:

request.getItsNatServlet().processRequest(servRequest,servResponse);

Этот новый запрос внутренне имеет эффект, аналогичный явному URL:

HTTP: // локальный: 8080 / spistfulhashbangtut / сервлет itsnat_doc_name = основной

Теперь целевой шаблон страницы указан и найден и  request.getItsNatDocument(); возвращает ненулевое значение, и в этом случае ничего не делается, потому что этот глобальный прослушиватель не является типичным местом для управления обычными запросами загрузки страницы.

Обработка главной страницы

Возвращаясь к сервлету:

ItsNatDocumentTemplate docTemplate;
docTemplate = itsNatServlet.registerItsNatDocumentTemplate("main","text/html",pathPages + "main.html");

Этот вызов регистрируется с именем mainфайла шаблона страницы  main.html (сохраненного в  WEB-INF/pages/):

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:itsnat="http://itsnat.org/itsnat">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta http-equiv="Pragma" content="no-cache" />
    <meta http-equiv="expires" content="Wed, 1 Dec 1997 03:01:00 GMT" />
    <meta http-equiv="Cache-Control" content="no-cache, must-revalidate" />
    <title id="titleId" itsnat:nocache="true">Tutorial: Single Page Interface SEO Compatible Web Site With ItsNat STATEFUL Using Hashbangs
    <link rel="stylesheet" type="text/css" href="css/style.css" />
    <script type="text/javascript" src="js/spi_hashbang.js?timestamp=2015-08-18_01"></script>
    <script type="text/javascript">
    function setState(name)
    {
        if (typeof document.getItsNatDoc == "undefined") return; // Too soon, page is not fully loaded
        var itsNatDoc = document.getItsNatDoc();
        var evt = itsNatDoc.createUserEvent("setState");
        evt.setExtraParam("name",name);
        itsNatDoc.dispatchUserEvent(null,evt);
    }
    window.spiSite.onBackForward = setState;
    </script>
</head>
<body>

<div class="main">
    <table style="width:100%; height:100%; padding:0; margin:0;" border="0px" cellpadding="0" cellspacing="0">
    <tbody>
    <tr style="height:50px;">
        <td>
            <h2 style="text-align:center;">Tutorial: Single Page Interface SEO Compatible Web Site With ItsNat STATEFUL <br> Using Hashbangs</h2>
        </td>
    </tr>
    <tr style="height:40px;">
        <td>
            <table style="width:100%; margin:0; padding:0; border: #ED752A solid; border-width: 0 0 2px 0; ">
                <tbody>
                <tr class="mainMenu" itsnat:nocache="true">
                    <td id="menuOpOverviewId">
                        <a onclick="setState('overview'); return false;" class="menuLink" href="?st=overview">Overview</a>
                    </td>
                    <td id="menuOpDetailId">
                        <a onclick="setState('detail'); return false;" class="menuLink" href="?st=detail">Detail</a>
                    </td>
                    <td> 
                </tr>
                </tbody>
            </table>
        </td>
    </tr>
    <tr style="height:70%; /* For MSIE */">
        <td id="contentParentId" itsnat:nocache="true" style="padding:20px; vertical-align:super;" >



        </td>
    </tr>
    <tr style="height:50px">
        <td style="border-top: 1px solid black; text-align:center;">
            SOME FOOTER
        </td>
    </tr>
    </tbody>
    </table>

</div>

<iframe id="googleAnalyticsId" itsnat:nocache="true" src="?ganalyt_st=" style="display:none;" ></iframe>

<!-- For Google Analytics, needed to know how the site is reached by users -->
<script>
  (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
  (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
  m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
  })(window,document,'script','//www.google-analytics.com/analytics.js','ga');

  ga('create', 'UA-2924757-6', 'auto');
  ga('send', 'pageview');

</script>

</body>
</html>

Как вы можете видеть, шаблоны ItsNat являются чистыми файлами HTML, поскольку логика представления кодируется с помощью Java API W3C DOM. По умолчанию шаблоны кэшируются, то есть узлы DOM внутренне сериализуются как обычный HTML и используются пользователями совместно; Любое кэшированное поддерево DOM заменяется на сервере текстовым узлом, содержащим специальную метку, когда это поддерево отправляется клиенту ItsNat автоматически отправляет кэшированную разметку. Кэширование DOM интересно для «статических» (с точки зрения сервера) частей шаблона, кеширования можно полностью избежать с помощью флага конфигурации, но не рекомендуется, если вы хотите сэкономить память сервера и повысить производительность.

Поскольку мы хотим изменить некоторые части шаблона страницы, эти части считаются не статичными (поскольку нам необходимо получить к ним программный доступ и, возможно, изменить их) и должны быть помечены как «не кэшированные» специальным атрибутом ItsNat,  itsnat:nocache="true" где  itsnat префикс объявлен в <html> as  xmlns:itsnat="http://itsnat.org/itsnat", yes ItsNat требует пространства имен XHTML, но ваши шаблоны обычно будут HTML 5 (взгляните на объявление DOCTYPE) и обслуживаются text / html MIME.

Например:

<title id="titleId" itsnat:nocache="true">Tutorial: Single Page Interface SEO Compatible Web Site With ItsNat Using Hashbangs</title>

Название веб-сайта SPI будет меняться при загрузке нового основного состояния (время загрузки страницы или событие AJAX).

<tr class="mainMenu" itsnat:nocache="true">

Эта строка содержит главное меню веб-сайта, не кэшируется, потому что нам нужно получить доступ к пунктам меню на сервере, чтобы изменить цвет текущей выбранной опции.

<td id="contentParentId" itsnat:nocache="true" style="padding:20px; vertical-align:super;" >

Эта ячейка таблицы является родительской для «области содержимого», когда конечный пользователь нажимает какую-либо опцию меню, эта зона изменится соответственно.

В заключение:

<iframe id="googleAnalyticsId" itsnat:nocache="true" src="?ganalyt_st=" style="display:none;" ></iframe>

URL-адрес этого <iframe> будет изменен, чтобы отслеживать основные состояния при выполнении внутристраничной одностраничной навигации, просто добавляя имя состояния в конец  src URL-адреса, затем содержимое <iframe> перезагружается (поскольку URL-адрес изменился) и Google Аналитика обнаруживает эту перезагрузку. Изучая различные параметры (состояния), мы можем отслеживать, как конечный пользователь перемещается по сайту, несмотря на характер SPI. На практике URL-адрес <iframe> будет изменен,  location.reload() чтобы избежать новых ненужных записей истории.

И:

<!-- For Google Analytics, needed to know how the site is reached by users -->
<script>
  (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
  (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
  m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
  })(window,document,'script','//www.google-analytics.com/analytics.js','ga');

  ga('create', 'UA-2924757-6', 'auto');
  ga('send', 'pageview');

</script>

Последний скрипт для Google Analytics в конце страницы необходим для того, чтобы узнать, как пользователи попадают на сайт (это тот же сценарий, который загружен в iframe), позже iframe отслеживает внутристраничную навигацию внутри страницы.

Следующий код:

<script type="text/javascript">
function setState(name)
{
    if (typeof document.getItsNatDoc == "undefined") return; // Too soon, page is not fully loaded

    var itsNatDoc = document.getItsNatDoc();
    var evt = itsNatDoc.createUserEvent("setState");
    evt.setExtraParam("name",name);
    itsNatDoc.dispatchUserEvent(null,evt);
}
window.spiSite.onBackForward = setState;
</script>

Используется для отправки ItsNat «пользовательских событий», пользовательское событие является расширением ItsNat для W3C DOM Events и вызывается некоторыми общедоступными методами ItsNat из JavaScript. Пользовательские события используются в этом руководстве для уведомления сервера о следующем базовом состоянии, которое должно быть загружено, эти пользовательские события принимаются прослушивателем пользовательских событий на ранее зарегистрированном сервере. В этом примере мы используем «пользовательские события» для навигации между основными состояниями, потому что этот учебник навигации по состоянию может быть выполнен также с обычными слушателями событий ItsNat на основе AJAX.

Пример того, как меняются основные состояния, — это двойная ссылка (пункт меню):

<a href="?st=overview" onclick="setState('overview'); return false;"
   class="menuLink">Overview</a>

Когда конечный пользователь щелкает эту ссылку,  onclick вызывается встроенный обработчик, вызов  setState('overview'); отправляет пользовательское событие на командный сервер для загрузки нового основного состояния overview, поскольку  onclick возвращает false, поведение ссылки (процесса href) по умолчанию  отменяется, следовательно  href="?st=overview" , игнорируется, а URL остается то же самое, но страница была частично изменена с новым состоянием. Это не относится к сканерам поисковых систем, эти боты обычно игнорируют JavaScript и следуют по ссылке, загружающей новую страницу overviewв качестве исходного состояния в соответствии с этим примером. Это пример двойного соединения AJAX / Normal.

Теперь настало время регистрации загрузчика для  main шаблона (обратно к сервлету):

ItsNatDocumentTemplate docTemplate;
docTemplate = itsNatServlet.registerItsNatDocumentTemplate("main","text/html",pathPages + "main.html");
docTemplate.addItsNatServletRequestListener(new SPITutMainLoadRequestListener());

SPITutMainLoadRequestListener.processRequest(…) Метод будет вызываться , когда сервлет получает новый запрос загрузки этого шаблона, один вызов для каждого запроса нагрузки. Исходный код:

package org.itsnat.spistfulhashbangtut;

import org.itsnat.core.ItsNatServletRequest;
import org.itsnat.core.ItsNatServletResponse;
import org.itsnat.core.event.ItsNatServletRequestListener;
import org.itsnat.core.http.ItsNatHttpServletRequest;
import org.itsnat.core.http.ItsNatHttpServletResponse;
import org.itsnat.spi.SPIMainDocumentConfig;
import org.itsnat.spi.SPIStateDescriptor;
import org.w3c.dom.Document;
import org.w3c.dom.html.HTMLTitleElement;

public class SPITutMainLoadRequestListener implements ItsNatServletRequestListener
{
    @Override
    public void processRequest(ItsNatServletRequest request, ItsNatServletResponse response)
    {
        Document doc = request.getItsNatDocument().getDocument();

        SPIMainDocumentConfig config = new SPIMainDocumentConfig();
        config.setStateNameSeparator('.')
              .setTitleElement((HTMLTitleElement)doc.getElementById("titleId"))
              .setContentParentElement(doc.getElementById("contentParentId"))
              .setGoogleAnalyticsElement(doc.getElementById("googleAnalyticsId"))
              .addMenuElement("overview",doc.getElementById("menuOpOverviewId"))
              .addMenuElement("detail",doc.getElementById("menuOpDetailId"))
              .addSPIStateDescriptor(new SPIStateDescriptor("overview","Overview",true))
              .addSPIStateDescriptor(new SPIStateDescriptor("overview.popup","Overview Popup",false))
              .addSPIStateDescriptor(new SPIStateDescriptor("detail","Detail",true))
              .addSPIStateDescriptor(new SPIStateDescriptor("not_found","Not Found",true))
              .setDefaultStateName("overview")
              .setNotFoundStateName("not_found");


        new SPITutMainDocument((ItsNatHttpServletRequest)request,(ItsNatHttpServletResponse)response,config);
    }
}

Новый  SPITutMainDocument экземпляр создается для каждого вызова (запрос на загрузку), этот экземпляр содержит  ItsNatHTMLDocument объект, представляющий загружаемый клиентский документ (страницу), этот экземпляр будет таким же и синхронизируется с клиентом, если страница изменит свое состояние без загрузки страницы (это учебник с состоянием, возможна альтернатива без состояния).

Этот объект не будет подвергаться сборке мусора, поскольку в нем будет зарегистрирован какой-либо прослушиватель событий,  ItsNatHTMLDocument и этот экземпляр документа автоматически удерживается ItsNat в соответствии с жизненным циклом клиентского документа: когда конечный пользователь покидает страницу, документ на сервере автоматически отбрасываются. Если событие unload не получено, ItsNat автоматически отбрасывает документы на сервере, если событие не получено в течение длительного времени (время истечения сеанса используется) или когда сеанс пользователя истекает (несмотря на то, что разработчики ItsNat не используют сеансы, в режиме с состоянием ItsNat Сеансы сервлетов являются основным механизмом для идентификации страниц конечных пользователей), в любом случае существует ограничение на количество живых страниц (документов), чтобы избежать злоупотреблений со стороны пользователей, когда этот предел превышается, самые старые документы на сервере отбрасываются.

Экземпляр  SPIMainDocumentConfig — это объект конфигурации нашей мини-инфраструктуры SPI ItsNat с сохранением состояния на основе хеш-бэнгов, созданных для этого урока.

Этот мини-фреймворк имеет три пакета, содержащие следующие классы:

  1. org.itsnat.spi: Общие classesClasses:  SPIMainDocumentConfigSPIMainDocumentSPIStateSPIStateDescriptor 
  2. org.itsnat.spistful: Сохраняющее состояние specificClasses:  SPIStfulMainDocumentSPIStfulState 
  3. org.itsnat.spistfulhashbang: stateful and hashbang processing specific

    Классы: SPIStfulHashbangMainDocument 

Вы можете использовать эту мини-инфраструктуру для создания других подобных веб-сайтов SPI (с сохранением состояния с использованием hashbangs) с минимальным кодом, мини-инфраструктура разработана для повторного использования и расширения.

Пакет  org.itsnat.spi содержит классы, которые будут использоваться для всех учебных пособий, совместимых с SPI SEO (с сохранением состояния и без сохранения состояния, hashbang и истории API).

Давайте объясним подробно

Класс  SPIMainDocumentConfig очень прост, это объект конфигуратора для менеджера документов. Это код:

package org.itsnat.spi;

import java.util.HashMap;
import java.util.Map;
import org.w3c.dom.Element;
import org.w3c.dom.html.HTMLTitleElement;

/**
 *
 * @author jmarranz
 */
public class SPIMainDocumentConfig
{
    protected char stateNameSeparator = 0;
    protected HTMLTitleElement titleElem; // HTMLTitleElement
    protected Element contentParentElem;
    protected Element googleAnalyticsElem;
    protected Map<String,SPIStateDescriptor> stateMap = new HashMap<String,SPIStateDescriptor>();
    protected Map<String,Element> menuElemMap = new HashMap<String,Element>();
    protected String defaultStateName;
    protected String notFoundStateName;

    public char getStateNameSeparator()
    {
        return stateNameSeparator;
    }

    public SPIMainDocumentConfig setStateNameSeparator(char stateNameSeparator)
    {
        this.stateNameSeparator = stateNameSeparator;
        return this;
    }

    public HTMLTitleElement getTitleElement()
    {
        return titleElem;
    }

    public SPIMainDocumentConfig setTitleElement(Element titleElem)
    {
        this.titleElem = (HTMLTitleElement)titleElem;
        return this;
    }

    public Element getContentParentElement()
    {
        return contentParentElem;
    }

    public SPIMainDocumentConfig setContentParentElement(Element contentParentElem)
    {
        this.contentParentElem = contentParentElem;
        return this;
    }

    public Element getGoogleAnalyticsElement()
    {
        return googleAnalyticsElem;
    }

    public SPIMainDocumentConfig setGoogleAnalyticsElement(Element googleAnalyticsElem)
    {
        this.googleAnalyticsElem = googleAnalyticsElem;
        return this;
    }

    public SPIStateDescriptor getSPIStateDescriptor(String stateName)
    {
        return stateMap.get(stateName);
    }

    public SPIMainDocumentConfig addSPIStateDescriptor(SPIStateDescriptor stateDesc)
    {
        SPIStateDescriptor old = stateMap.put(stateDesc.getStateName(),stateDesc);
        if (old != null) throw new RuntimeException("Already registered a state with name: " + stateDesc.getStateName());
        return this;
    }

    public Element getMenuElement(String stateName)
    {
        return menuElemMap.get(stateName);
    }

    public Map<String,Element> getMenuElementMap()
    {
        return menuElemMap;
    }

    public SPIMainDocumentConfig addMenuElement(String stateName,Element menuElem)
    {
        menuElemMap.put(stateName,menuElem);
        return this;
    }

    public String getDefaultStateName()
    {
        return defaultStateName;
    }

    public SPIMainDocumentConfig setDefaultStateName(String defaultStateName)
    {
        this.defaultStateName = defaultStateName;
        return this;
    }

    public String getNotFoundStateName()
    {
        return notFoundStateName;
    }

    public SPIMainDocumentConfig setNotFoundStateName(String notFoundStateName)
    {
        this.notFoundStateName = notFoundStateName;
        return this;
    }

    public SPIMainDocumentConfig check()
    {
        if (stateNameSeparator == 0) throw new RuntimeException("State name list separator is not defined, use any char if you don't need it");
        if (titleElem == null) throw new RuntimeException("Missing titleElement");
        if (contentParentElem == null) throw new RuntimeException("Missing contentParentElement");
        if (googleAnalyticsElem == null) throw new RuntimeException("Missing googleAnalyticsElement");
        for(Map.Entry<String,Element> entry : menuElemMap.entrySet())
        {
            Element menuElem = entry.getValue();
            if (menuElem == null) throw new RuntimeException("Menu element of " + entry.getKey() + " is null");
        }
        // menuElemMap puede estar vacío (?)
        if (defaultStateName == null) throw new RuntimeException("Missing defaultStateName");
        if (notFoundStateName == null) throw new RuntimeException("Missing notFoundStateName");

        if (stateMap.get(defaultStateName) == null) throw new RuntimeException("Missing state declaration for default state: " + defaultStateName);
        if (stateMap.get(notFoundStateName) == null) throw new RuntimeException("Missing state declaration for not found state: " + notFoundStateName);

        return this;
    }
}

В этом объекте конфигурации мы настраиваем необходимые элементы DOM для изменения заголовка, активного меню, Google Analytics и краткого описания основных состояний.

Например,  menuElemMap коллекция отображает имена состояний с элементами DOM пунктов меню, нам нужно, чтобы эти элементы меняли свой внешний вид при выборе опции меню, выбор меню изменяет текущее отображаемое основное состояние.

contentParentElem Полезно вставить / удалить разметку «области контента»,  googleAnalyticsElem используется для мониторинга состояния посещений с помощью Google Analytics.

Самый интересный класс мини-фреймворков это  SPIMainDocument. Некоторые объяснения интересны, чтобы показать вам, как управлять состояниями и как поддерживать сканирование Google AJAX на основе hashbangs ( прочитайте это  и  это ).

package org.itsnat.spi;

import org.itsnat.core.ItsNatServlet;
import org.itsnat.core.html.ItsNatHTMLDocument;
import org.itsnat.core.http.ItsNatHttpServletRequest;
import org.itsnat.core.http.ItsNatHttpServletResponse;
import org.itsnat.core.tmpl.ItsNatDocFragmentTemplate;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;

public abstract class SPIMainDocument
{
    protected ItsNatHTMLDocument itsNatDoc;
    protected SPIMainDocumentConfig config;
    protected String title;
    protected String googleAnalyticsIFrameURL;


    public SPIMainDocument(ItsNatHttpServletRequest request, ItsNatHttpServletResponse response,SPIMainDocumentConfig config)
    {
        config.check();

        this.config = config;
        this.itsNatDoc = (ItsNatHTMLDocument)request.getItsNatDocument();

        this.title = config.getTitleElement().getText(); // Initial value
        this.googleAnalyticsIFrameURL = config.getGoogleAnalyticsElement().getAttribute("src");  // Initial value
    }

    public ItsNatHTMLDocument getItsNatHTMLDocument()
    {
        return itsNatDoc;
    }

    public SPIStateDescriptor getSPIStateDescriptor(String stateName)
    {
        return config.getSPIStateDescriptor(stateName);
    }

    public void setStateTitle(String stateTitle)
    {
        String pageTitle = stateTitle + " - " + title;
        if (itsNatDoc.isLoading())
            config.getTitleElement().setText(pageTitle);
        else
            itsNatDoc.addCodeToSend("document.title = \"" + pageTitle + "\";\n");
    }

    public Element getContentParentElement()
    {
        return config.getContentParentElement();
    }

    public ItsNatDocFragmentTemplate getFragmentTemplate(String name)
    {
        ItsNatServlet servlet = itsNatDoc.getItsNatDocumentTemplate().getItsNatServlet();
        return servlet.getItsNatDocFragmentTemplate(name);
    }

    public DocumentFragment loadDocumentFragment(String name)
    {
        ItsNatDocFragmentTemplate template = getFragmentTemplate(name);
        if (template == null)
            throw new RuntimeException("There is no template registered for state or fragment name: " + name);
        return template.loadDocumentFragment(itsNatDoc);
    }

    public String getFirstLevelStateName(String stateName)
    {
        String firstLevelName = stateName;
        int pos = stateName.indexOf(config.getStateNameSeparator());
        if (pos != -1) firstLevelName = stateName.substring(0, pos); // Case "overview.popup"
        return firstLevelName;
    }

    public void registerState(SPIState state)
    {
        setStateTitle(state.getStateTitle());
        String stateName = state.getStateName();
        itsNatDoc.addCodeToSend("spiSite.setStateInURL(\"" + stateName + "\");");
        // googleAnalyticsElem.setAttribute("src",googleAnalyticsIFrameURL + stateName);
        // http://stackoverflow.com/questions/24407573/how-can-i-make-an-iframe-not-save-to-history-in-chrome
        String jsIFrameRef = itsNatDoc.getScriptUtil().getNodeReference(config.getGoogleAnalyticsElement());
        itsNatDoc.addCodeToSend("var elem = " + jsIFrameRef + "; try{ elem.contentWindow.location.replace('" + googleAnalyticsIFrameURL + stateName + "'); } catch(e) {}");
    }
}

Этот класс является ядром веб-сайта, тесно связан с основным шаблоном и отвечает за (фундаментальное) управление состояниями, состояния напрямую зависят от главного меню.

Первое предложение:

this.itsNatDoc = (ItsNatHTMLDocument)request.getItsNatDocument();

Сохраняет объект документа ItsNat в атрибуте, поскольку  SPIMainDocument является оберткой (и менеджером) объекта  ItsNatHTMLDocument.

Особенно интересен метод,  SPIMainDocument.registerState(SPIState) который фиксирует текущее состояние в клиенте:

public void registerState(SPIState state)
{
    setStateTitle(state.getStateTitle());
    String stateName = state.getStateName();
    itsNatDoc.addCodeToSend("spiSite.setStateInURL(\"" + stateName + "\");");
    // googleAnalyticsElem.setAttribute("src",googleAnalyticsIFrameURL + stateName);
    // http://stackoverflow.com/questions/24407573/how-can-i-make-an-iframe-not-save-to-history-in-chrome
    String jsIFrameRef = itsNatDoc.getScriptUtil().getNodeReference(config.getGoogleAnalyticsElement());
    itsNatDoc.addCodeToSend("var elem = " + jsIFrameRef + "; try{ elem.contentWindow.location.replace('" + googleAnalyticsIFrameURL + stateName + "'); } catch(e) {}");
}

Звонок:

setStateTitle(state.getStateTitle());

изменяет заголовок страницы, добавляя описание состояния.

itsNatDoc.addCodeToSend("spiSite.setStateInURL(\"" + stateName + "\");");

Этот код отправляет клиенту некоторый код JavaScript для указания основного состояния, загружаемого в клиент, в URL-адресе страницы. Метод JavaScript  setStateInURL(stateName) включен в файл  spi_hashbang.jsдля веб-сайтов на основе hashbang и  spi_hsapi.js для API истории, один из этих файлов должен быть включен в основной шаблон и всегда присутствовать на нашем веб-сайте SPI.

Класс  SPIStateDescriptor является просто дескриптором состояния:

package org.itsnat.spi;

/**
 *
 * @author jmarranz
 */
public class SPIStateDescriptor
{
    protected final String stateName;
    protected final String stateTitle;
    protected final boolean mainLevel;

    public SPIStateDescriptor(String stateName,String stateTitle,boolean mainLevel)
    {
        this.stateName = stateName;
        this.stateTitle = stateTitle;
        this.mainLevel = mainLevel;
    }

    public String getStateName()
    {
        return stateName;
    }

    public String getStateTitle()
    {
        return stateTitle;
    }

    public boolean isMainLevel()
    {
        return mainLevel;
    }
}

Класс  SPIState является базовым классом государственных классов.

Логический параметр указывает, является ли состояние «основным уровнем», в данном случае overview.popupне «основным уровнем», является ли состояние второго уровня зависимым от overviewсостояния, при выборе состояния выбирается overview.popupпункт меню «Обзор», поскольку используется только overviewчасть для активации меню.

Все государства наследуют от  SPIState:

package org.itsnat.spi;

import org.itsnat.core.html.ItsNatHTMLDocument;

public abstract class SPIState
{
    protected SPIMainDocument spiDoc;
    protected SPIStateDescriptor stateDesc;

    public SPIState(SPIMainDocument spiDoc,SPIStateDescriptor stateDesc,boolean register)
    {
        this.spiDoc = spiDoc;
        this.stateDesc = stateDesc;

        if (register)
            spiDoc.registerState(this);
    }

    public SPIMainDocument getSPIMainDocument()
    {
        return spiDoc;
    }

    public ItsNatHTMLDocument getItsNatHTMLDocument()
    {
        return spiDoc.getItsNatHTMLDocument();
    }

    public String getStateName()
    {
        return stateDesc.getStateName();
    }

    public String getStateTitle()
    {
        return stateDesc.getStateTitle();
    }

}

Звонок:

spiDoc.registerState(this);

Особенно интересно, этот метод вызывает,  SPIMainDocument.registerState(SPIState) который устанавливает текущее состояние в клиенте.

Главная страница Обработка Stateful

Теперь пришло время уйти в отставку, чтобы описать, как мини-фреймворк управляет веб-сайтом с отслеживанием состояния.

В пакете только два класса  org.itsnat.spistfulSPIStfulMainDocument и  SPIStfulState.

package org.itsnat.spistful;

import org.itsnat.spi.SPIMainDocumentConfig;
import org.itsnat.spi.SPIStateDescriptor;
import org.itsnat.core.domutil.ItsNatDOMUtil;
import org.itsnat.core.event.ItsNatUserEvent;
import org.itsnat.core.http.ItsNatHttpServletRequest;
import org.itsnat.core.http.ItsNatHttpServletResponse;
import org.itsnat.spi.SPIMainDocument;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventListener;

public abstract class SPIStfulMainDocument extends SPIMainDocument
{
    protected Element currentMenuItemElem;
    protected SPIStfulState currentState;

    public SPIStfulMainDocument(ItsNatHttpServletRequest request, ItsNatHttpServletResponse response,SPIMainDocumentConfig config)
    {
        super(request, response,config);

        EventListener listener = new EventListener()
        {
            @Override
            public void handleEvent(Event evt)
            {
                ItsNatUserEvent itsNatEvt = (ItsNatUserEvent)evt;
                ItsNatHttpServletRequest request = (ItsNatHttpServletRequest)itsNatEvt.getItsNatServletRequest();
                ItsNatHttpServletResponse response = (ItsNatHttpServletResponse)itsNatEvt.getItsNatServletResponse();
                String name = (String)itsNatEvt.getExtraParam("name");
                changeState(name,request,response);
            }
        };
        itsNatDoc.addUserEventListener(null,"setState", listener);
    }

    public SPIStfulState changeState(String stateName,ItsNatHttpServletRequest request,ItsNatHttpServletResponse response)
    {
        SPIStateDescriptor stateDesc = config.getSPIStateDescriptor(stateName);
        if (stateDesc == null)
        {
            return changeState(config.getNotFoundStateName(),request,response);
        }

        // Cleaning previous state:
        if (currentState != null)
        {
            currentState.dispose();
            this.currentState = null;
        }

        ItsNatDOMUtil.removeAllChildren(config.getContentParentElement());

        // Setting new state:
        changeActiveMenu(stateName);

        String fragmentName = stateDesc.isMainLevel() ? stateName : getFirstLevelStateName(stateName);
        DocumentFragment frag = loadDocumentFragment(fragmentName);
        config.getContentParentElement().appendChild(frag);

        this.currentState = createSPIState(stateDesc,request,response);

        return currentState;
    }

    public abstract SPIStfulState createSPIState(SPIStateDescriptor stateDesc,ItsNatHttpServletRequest request,ItsNatHttpServletResponse response);


    public void changeActiveMenu(String stateName)
    {
        String mainMenuItemName = getFirstLevelStateName(stateName);

        Element prevActiveMenuItemElem = this.currentMenuItemElem;

        this.currentMenuItemElem = config.getMenuElement(mainMenuItemName);

        Element currActiveMenuItemElem = this.currentMenuItemElem;

        onChangeActiveMenu(prevActiveMenuItemElem,currActiveMenuItemElem);
    }

    public abstract void onChangeActiveMenu(Element prevActiveMenuItemElem, Element currActiveMenuItemElem);
}

The class SPIStfulMainDocument extends SPIMainDocument and concretes how states are managed in a stateful web site.

Let’s to explain:

EventListener listener = new EventListener()
{
    @Override
    public void handleEvent(Event evt)
    {
        ItsNatUserEvent itsNatEvt = (ItsNatUserEvent)evt;
        ItsNatHttpServletRequest request = (ItsNatHttpServletRequest)itsNatEvt.getItsNatServletRequest();
        ItsNatHttpServletResponse response = (ItsNatHttpServletResponse)itsNatEvt.getItsNatServletResponse();
        String name = (String)itsNatEvt.getExtraParam("name");
        changeState(name,request,response);
    }
};
itsNatDoc.addUserEventListener(null,"setState", listener);

This code registers the user event listener listening state changes when some navigation link is clicked, for instance when the end user click a menu option. User events are received by the handleEvent(Event) method of this class. JavaScript client code will fire these user events to change the fundamental state, managed in server and propagated to client.

The method changeState(String) is the responsible of state change management. This method clears the old fundamental state with code like currentState.dispose() and ItsNatDOMUtil.removeAllChildren(config.getContentParentElement()), loads the specified fundamental state inserting the new markup into the content area, changes the appearance of the new active menu option and delegates further state processing to the appropriated SPIState class.

The class SPIStfulState just add one abstract method required to dispose the state because is required by SPIStfulMainDocument.

package org.itsnat.spistful;

import org.itsnat.spi.SPIMainDocument;
import org.itsnat.spi.SPIStateDescriptor;
import org.itsnat.spi.SPIState;

public abstract class SPIStfulState extends SPIState
{
    public SPIStfulState(SPIMainDocument spiDoc,SPIStateDescriptor stateDesc,boolean register)
    {
        super(spiDoc,stateDesc,register);
    }

    public abstract void dispose();
}

Main Page Processing and Hashbangs

Time to another step down to know how the mini-framework is able to manage hashbangs.

The package org.itsnat.spistfulhashbang has only one class SPIStfulHashbangMainDocument, yes this class inherits from SPIStfulMainDocument and specifies how to manage hashbangs.

package org.itsnat.spistfulhashbang;

import org.itsnat.spi.SPIMainDocumentConfig;
import javax.servlet.http.HttpServletRequest;
import org.itsnat.core.http.ItsNatHttpServletRequest;
import org.itsnat.core.http.ItsNatHttpServletResponse;
import org.itsnat.spistful.SPIStfulMainDocument;

public abstract class SPIStfulHashbangMainDocument extends SPIStfulMainDocument
{
    public SPIStfulHashbangMainDocument(ItsNatHttpServletRequest request, ItsNatHttpServletResponse response,SPIMainDocumentConfig config)
    {
        super(request, response,config);

        HttpServletRequest servReq = request.getHttpServletRequest();
        String stateName = servReq.getParameter("_escaped_fragment_"); // Google bot, has priority, its value is based on the hash fragment
        if (stateName != null)
        {
            if (stateName.startsWith("st=")) // st means "state"
                stateName = stateName.substring("st=".length(), stateName.length());
            else // Wrong format
                stateName = config.getDefaultStateName();
        }
        else
        {
            stateName = servReq.getParameter("st");
            if (stateName == null)
                stateName = config.getDefaultStateName();
        }


        changeState(stateName,request,response);
    }
}

This demo application is prepared to be crawled by Google Search bots following its AJAX Crawling Specification, links ending with #! are also followed by Google bots, in this case the target site is accessed replacing #! with a parameter _escaped_fragment= followed by the text following #!. For instance this link is crawled by Google bots requesting with this link. Other web crawlers like Bing also support Google AJAX Crawling Specification.

When navigation the SPI web site, automatically #!st=somestate is added to the URL accordingly, you can copy this URL and use it to link the state/page in your web sites, when Google bots traverse your web site, Google knows that a URL ending in a hashbang must be accessed using the_escaped_fragment convention.

In this tutorial we are using another kind of state navigation based on a conventional st=somestate parameter in dual links, when Google traverses a dual link, the href, containing a st=somestate parameter, is used to navigate to the target page. This parameter is not shown when end users navigate using a conventional browser with AJAX enabled because the onclick is executed instead. This normal parameter is interesting for crawlers with no support of _escaped_fragment convention and to provide navigation with JavaScript disabled. This approach is optional, if you just want to support crawlers with _escaped_fragment, a hashbang href="#!st=somestate" is enough for Google Search to index your site if you provide the _escaped_fragment convention. This tutorial will use the st=somestate parameter in dual links.

When the st parameter is present when loading the main page, the value is the initial state. When no st parameter is provided, for instance when loading with this URL http://localhost:8080/spistfulhashbangtut, the default state is loaded (overview in this tutorial) and the default menu option is selected («Overview»).

Hashbang management in client happens in the JavaScript library spi_hashbang.js, this script is part of the mini-framework for SPI web sites based on hashbangs (stateless or stateful).

This is the source code of spi_hashbang.js:

function LocationState()
{
    this.getURL = getURL;
    this.setURL = setURL;
    this.getStateName = getStateName;
    this.setStateName = setStateName;
    this.isStateNameChanged = isStateNameChanged;

    this.url = window.location.href;

    function getURL() { return window.location.href; }
    function setURL(url) { window.location.href = url; }

    function getStateName()
    {
        var url = this.getURL();
        var posR = url.lastIndexOf("#!st=");
        if (posR == -1) return null;
        var stateName = url.substring(posR + "#!st=".length);
        if (stateName == "") return null;
        return stateName;
    }

    function setStateName(stateName)
    {
        var url = this.getURL();
        var posR = url.lastIndexOf("#");
        var url2;
        if (posR != -1) url2 = url.substring(0,posR);
        else url2 = url;
        url2 = url2 + "#!st=" + stateName;
        if (url == url2) return;

        window.location.href = url2;
    }

    function isStateNameChanged(newUrl)
    {
        var url = this.getURL();
        if (newUrl == url) return false;
        var posR = url.lastIndexOf("#!st=");
        if (posR == -1) return false;
        var posR2 = newUrl.lastIndexOf("#!st=");
        if (posR2 == -1) return false;
        if (posR != posR2) return false;
        var stateName = url.substring(posR + "#!st=".length);
        var newStateName = newUrl.substring(posR + "#!st=".length);
        if (stateName == newStateName) return false;
        return true;
    }
}

function SPISite()
{
    this.load = load;
    this.detectURLStateChange = detectURLStateChange;
    this.detectURLStateChangeCB = detectURLStateChangeCB;
    this.setStateInURL = setStateInURL;
    this.removeChildren = removeChildren;
    this.onBackForward = null; // Public, user defined


    this.firstTime = true;
    this.initialURLWithState = null;
    this.url = null;
    this.disabled = false;

    this.load();

    function load() // page load phase
    {
        if (this.disabled) return;

        var currLoc = new LocationState();
        var stateName = currLoc.getStateName();
        if (stateName == null) return;
        this.initialURLWithState = currLoc.getURL();
    }

    function setStateInURL(stateName)
    {
        if (this.disabled) return;

        var currLoc = new LocationState();
        currLoc.setStateName(stateName);

        this.url = currLoc.getURL();

        if (!this.firstTime) return;
        this.firstTime = false;

        if (this.initialURLWithState != null)
        {
            // Loads the initial state in URL if different to default
            currLoc.setURL( this.initialURLWithState );
            this.initialURLWithState = null;
        }

        this.detectURLStateChange();
    }

    function detectURLStateChange()
    {
        var onhashchangeSupport = ("onhashchange" in window); // Supported in IE 8
        if (onhashchangeSupport)
        {
            var func = function()
            {
                arguments.callee.spiSite.detectURLStateChangeCB();
            };
            func.spiSite = this;
            if (window.addEventListener) window.addEventListener("hashchange", func, false);
            else window.attachEvent("onhashchange", func); // IE 8  https://msdn.microsoft.com/en-us/library/cc288209(v=vs.85).aspx
        }
        else
        {
            var time = 200;
            var func = function()
            {
                arguments.callee.spiSite.detectURLStateChangeCB();
                window.setTimeout(arguments.callee,time);
            };
            func.spiSite = this;
            window.setTimeout(func,time);
        }
    }

    function detectURLStateChangeCB()
    {
        // Detecting when only the state of the reference part of the URL changes
        var currLoc = new LocationState();
        if (!currLoc.isStateNameChanged(this.url)) return;

        // Only changed the state in reference part
        this.url = currLoc.getURL();

        var stateName = currLoc.getStateName();
        if (this.onBackForward) this.onBackForward(stateName);
        else try { window.location.reload(true); }
             catch(ex) { window.location = window.location; }
    }

    function removeChildren(node) // used by spistless
    {
        while(node.firstChild) { var child = node.firstChild; node.removeChild(child); }; // Altnernative: node.innerHTML = ""
    }
}

window.spiSite = new SPISite();

Some code is interesting and needs explanation:

   ...
    this.load();

    function load() // page load phase
    {
        if (this.disabled) return;

        var currLoc = new LocationState();
        var stateName = currLoc.getStateName();
        if (stateName == null) return;
        this.initialURLWithState = currLoc.getURL();
    }

    function setStateInURL(stateName)
    {
        if (this.disabled) return;

        var currLoc = new LocationState();
        currLoc.setStateName(stateName);

        this.url = currLoc.getURL();

        if (!this.firstTime) return;
        this.firstTime = false;

        if (this.initialURLWithState != null)
        {
            // Loads the initial state in URL if different to default
            currLoc.setURL( this.initialURLWithState );
            this.initialURLWithState = null;
        }

        this.detectURLStateChange();
    }
    ...
}

When you explicitly write down in your browser a URL like this http://localhost:8080/spistfulhashbangtut/#!st=somestate, the server only receives http://localhost:8080/spistfulhashbangtut/ without the fragment/hashbang, this is normal and expected, the hashbang is not a normal parameter, is not sent to server by web browsers. The server returns and sets the default state (overview in this tutorial), but the first time the JavaScript code automatically detects this case (initialURLWithState attribute) and sets the user specified initial state. Web crawlers have not problems because links are traversed using ?st=somestate or when a URL with #!st=somestate is detected the _escaped_fragment_ is used to fetch the page.

In a conventional web site users are used to page navigation by clicking Back/Forward buttons, most of the time this action is «automatic», ask to some user not to click the back button, is near impossible. A SPI web site MUST support history navigation by user actions, fortunately history navigation managed/simulated by the JavaScript code of our mini-framework.

In our example Back/Forward buttons are supported with no reload because window.spiSite.onBackForward was registered in main.html with the call:

<script>
function setState(name)
{
    ...
}

window.spiSite.onBackForward = setState;
</script>

When clicking Back/Forward buttons is detected, the state change in the hashbang of the URL is forwarded to the server by calling the method registered in window.spiSite.onBackForward with the name of the state obtained from URL. In this tutorial the user defined method setName sends a user event to server to set the new state name. By this way our Single Page Interface remains pure SPI including Back/Forward behavior (manual history navigation in general). If arbitrary state change transitions are too complex without reloading the page, use the default approach (page reload) leavingwindow.spiSite.onBackForward undefined.

Infrastructure of Fundamental States

Now is the time to deep inside a concrete example using our mini-framework. We must to define the fundamental states being used as examples in this SPI web site.

Back to the servlet:

// Fragments
itsNatServlet.registerItsNatDocFragmentTemplate("not_found","text/html",pathFragments + "not_found.html");
itsNatServlet.registerItsNatDocFragmentTemplate("overview","text/html",pathFragments + "overview.html");
itsNatServlet.registerItsNatDocFragmentTemplate("overview.popup","text/html",pathFragments + "overview_popup.html");
itsNatServlet.registerItsNatDocFragmentTemplate("detail","text/html",pathFragments + "detail.html");
itsNatServlet.registerItsNatDocFragmentTemplate("detail.more","text/html",pathFragments + "detail_more.html");

The fragment overview contains the markup in <body> being included in the content area of the main page when user selects the «Overview» menu option or the URL loading our web site specifies overview as the initial state. The same for detail.

The fragment overview.popup is going to be inserted into the overview fragment, the same for detail.more. There is a big difference between overview.popup and detail.more is that overview.popup is registered as a fundamental state, that is, it is bookmarkable, detail.more is just a substate no bookmarkable (is not a fundamental state).

This is why overview.popup is registered and detail.more is not, do you remeber SPITutMainLoadRequestListener?

.addSPIStateDescriptor(new SPIStateDescriptor("overview","Overview",true))
.addSPIStateDescriptor(new SPIStateDescriptor("overview.popup","Overview Popup",false))
.addSPIStateDescriptor(new SPIStateDescriptor("detail","Detail",true))
.addSPIStateDescriptor(new SPIStateDescriptor("not_found","Not Found",true))

The substate detail.more is not registered as a fundamental state.

The boolean parameter indicates whether the state is «main level», in this case overview.popup is not «main level», is a second level state dependent of overview state, when selected the state overview.popup the menu item «Overview» is selected because only the overview part is used for menu activation (this is the reason of why we specify the separator character).

This is the source code of the concrete SPITutMainDocument inherited from SPIStfulHashbangMainDocument:

package org.itsnat.spistfulhashbangtut;

import org.itsnat.spistful.SPIStfulState;
import org.itsnat.core.http.ItsNatHttpServletRequest;
import org.itsnat.core.http.ItsNatHttpServletResponse;
import org.itsnat.spistfulhashbang.SPIStfulHashbangMainDocument;
import org.itsnat.spi.SPIMainDocumentConfig;
import org.itsnat.spi.SPIStateDescriptor;
import org.w3c.dom.Element;

public class SPITutMainDocument extends SPIStfulHashbangMainDocument
{
    public SPITutMainDocument(ItsNatHttpServletRequest request, ItsNatHttpServletResponse response,SPIMainDocumentConfig config)
    {
        super(request,response,config);
    }

    @Override
    public SPIStfulState changeState(String stateName,ItsNatHttpServletRequest request,ItsNatHttpServletResponse response)
    {
        SPIStfulState state = super.changeState(stateName,request,response);

        itsNatDoc.addCodeToSend("try{ window.scroll(0,-5000); }catch(ex){}");
        // try/catch is used to avoid exceptions when some (mobile) browser does not support window.scroll()

        return state;
    }

    @Override
    public SPIStfulState createSPIState(SPIStateDescriptor stateDesc,ItsNatHttpServletRequest request,ItsNatHttpServletResponse response)
    {
        String stateName = stateDesc.getStateName();
        if (stateName.equals("overview")||stateName.equals("overview.popup"))
        {
            boolean popup = false;
            if (stateName.equals("overview.popup"))
            {
                popup = true;
                stateDesc = getSPIStateDescriptor("overview");
            }
            return new SPITutStateOverview(this,stateDesc,popup);
        }
        else if (stateName.equals("detail"))
            return new SPITutStateDetail(this,stateDesc);
        else
            return null;
    }


    @Override
    public void onChangeActiveMenu(Element prevActiveMenuItemElem,Element currActiveMenuItemElem)
    {
        if (prevActiveMenuItemElem != null)
            prevActiveMenuItemElem.removeAttribute("class");
        if (currActiveMenuItemElem != null)
            currActiveMenuItemElem.setAttribute("class","menuOpSelected");
    }
}

Later we will explain the SPITutState*, all of them inherit from SPIStfulState.

Support of Google Analytics to Monitor Fundamental States

The last two lines of SPIMainDocument.registerState(...) are:

String jsIFrameRef = itsNatDoc.getScriptUtil().getNodeReference(config.getGoogleAnalyticsElement());
itsNatDoc.addCodeToSend("var elem = " + jsIFrameRef + "; try{ elem.contentWindow.location.replace('" + googleAnalyticsIFrameURL + stateName + "'); } catch(e) {}");

Changes the URL of the current page of the <iframe> being used for Google Analytics adding the name of the new fundamental state, because location.replace() is used, the page is reloaded registering in Google Analytics the changed state, but according to replace() behavior, no new entry in the history is added (avoiding unnecessary history entries). By this way you can monitor how many times (and who and how) end users have been visiting the fundamental states of your SPI web site.

The parameter ganalyt_st is detected in SPITutGlobalLoadRequestListener and redirected the request to load the google_analytics template registered in servlet with the following call:

docTemplate = itsNatServlet.registerItsNatDocumentTemplate("google_analytics","text/html", pathPages + "google_analytics.html");
docTemplate.setScriptingEnabled(false);

This template (google_analytics.html) only contains scripts of Google Analytics (of course the concrete token value is not valid for you):

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta http-equiv="Pragma" content="no-cache" />
    <meta http-equiv="expires" content="Wed, 1 Dec 1997 03:01:00 GMT" />
    <meta http-equiv="Cache-Control" content="no-cache, must-revalidate" />
    <title>Google Analytics
</head>
<body style="margin:0; padding:0;">

<script>
  (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
  (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
  m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
  })(window,document,'script','//www.google-analytics.com/analytics.js','ga');

  ga('create', 'UA-2924757-6', 'auto');
  ga('send', 'pageview');

</script>

</body>
</html>

This file could be a simple html (or JSP) file outside ItsNat control, but ItsNat ever adds response headers to disable page caching in browsers (a good thing to force and ensure fresh reload and script execution).

Note the call:

docTemplate.setScriptingEnabled(false);

This configuration call sets this page template as non-scriptable, ItsNat does not add JavaScript code to the page for client-server synchronization purposes hence there is no AJAX, furthermore, the ItsNat document in server only is created and used in load time then is discarded because this page is stateless (server point of view).

Fundamental States: overview and overview.popup

Concrete fundamental states (inherited from SPIStfulState) are SPITutStateOverviewSPITutStateOverviewPopupSPITutStateDetail.

SPITutStateOverview and SPITutStateDetail are directly launched by SPITutMainDocument because both are the main menu options. SPITutStateOverviewPopup is very interesting because in spite of it is a sub-state of SPITutStateOverview we want this state («Overview showing a popup window») to be a fundamental state, that is bookmarkable, content reached by search engine crawlers and monitored by Google Analytics, so it is inherited from SPIStfulState because the SPIMainDocument.registerState(SPIState) method must be called.

Now we are ready to show SPITutStateOverview:

package org.itsnat.spistfulhashbangtut;

import org.itsnat.spistful.SPIStfulState;
import org.itsnat.spi.SPIStateDescriptor;
import org.w3c.dom.Element;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.events.EventTarget;
import org.w3c.dom.html.HTMLDocument;

public class SPITutStateOverview extends SPIStfulState implements EventListener
{
    protected Element popupElem;
    protected SPITutStateOverviewPopup popup;

    public SPITutStateOverview(SPITutMainDocument spiTutDoc,SPIStateDescriptor stateDesc,boolean showPopup)
    {
        super(spiTutDoc,stateDesc,!showPopup);

        HTMLDocument doc = getItsNatHTMLDocument().getHTMLDocument();
        this.popupElem = doc.getElementById("popupId");
        ((EventTarget)popupElem).addEventListener("click",this,false);

        if (showPopup) showOverviewPopup();
    }

    @Override
    public void dispose()
    {
        if (popup != null) popup.dispose();
        ((EventTarget)popupElem).removeEventListener("click",this,false);
    }

    @Override
    public void handleEvent(Event evt)
    {
        showOverviewPopup();
    }

    public void showOverviewPopup()
    {
        ((EventTarget)popupElem).removeEventListener("click",this,false); // Avoids two consecutive clicks
        this.popup = new SPITutStateOverviewPopup(this);
    }

    public void onDisposeOverviewPopup()
    {
        this.popup = null;
        ((EventTarget)popupElem).addEventListener("click",this,false); // Restores
        spiDoc.registerState(this);
    }

}

To understand what this class is doing we need the overview.html fragment template, the markup contained in <body> is inserted into the «content area» of our web site when overview state is selected (the markup in <head> is not inserted):

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:itsnat="http://itsnat.org/itsnat">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Overview
</head>
<body>

    <div>
        <h2>Overview</h2>
        <p>This "fundamental" state is processed by crawlers of search engines (Google, Yahoo, Bing...)</p>
        <p>To understant how this SPI web site is "seen" by search engines, disable
            JavaScript in your browser.
        </p>
        <p>You can load a new text like a pop-up window, the link
           used to open this pop-up contains a URL specifying as fundamental
           state this state with the popup already loaded, this URL is used
           by crawlers because "return false" is not executed.
           By this way text contained in pop-up is also processed by crawlers.
        </p>
        <a itsnat:nocache="true" id="popupId" href="?st=overview.popup" onclick="return false;">Show popup</a>
    </div>
</body>
</html>

Because «Overview» is the default state the following picture shows this state as the initial state of our web application:

Pay attention to the link:

<a id="popupId" href="?st=overview.popup" onclick="return false;">Show popup</a>

This link is dual, AJAX and normal, in this case we directly bind an event listener in server with the call:

((EventTarget)popupElem).addEventListener("click",this,false);

When clicked, the sub-state overview.popup (also a fundamental state) is instanced, and registered as the current state, showing a popup modal window with some text.

When this link is reached by web crawlers is also processed trying to load the linked web site with the initial state overview.popup because onclick handler is ignored. In SPITutMainDocument we know this fundamental state is a sub-state of overview so one SPITutStateOverview object is created withpopup parameter set to true:

public SPIState createSPIState(SPIStateDescriptor stateDesc,ItsNatHttpServletRequest request,ItsNatHttpServletResponse response)
{
    String stateName = stateDesc.getStateName();
    if (stateName.equals("overview")||stateName.equals("overview.popup"))
    {
        boolean popup = false;
        if (stateName.equals("overview.popup"))
        {
            popup = true;
            stateDesc = getSPIStateDescriptor("overview");
        }
        return new SPITutStateOverview(this,stateDesc,popup);
    }
    ...
}

Then the popup window is automatically shown in load time, and because ItsNat is in fast-load mode the markup in the popup is rendered and sent to the client as markup, hence reached by search engine crawlers.

The class SPITutStateOverviewPopup loads the fragment with some markup and insert this markup to the page on top of a «modal layer»:

package org.itsnat.spistfulhashbangtut;

import org.itsnat.spistful.SPIStfulState;
import org.itsnat.comp.ItsNatComponentManager;
import org.itsnat.comp.layer.ItsNatModalLayer;
import org.itsnat.core.domutil.ItsNatTreeWalker;
import org.itsnat.core.html.ItsNatHTMLDocument;
import org.itsnat.spi.SPIMainDocument;
import org.itsnat.spi.SPIStateDescriptor;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.events.EventTarget;
import org.w3c.dom.html.HTMLBodyElement;
import org.w3c.dom.html.HTMLDocument;

public class SPITutStateOverviewPopup extends SPIStfulState implements EventListener
{
    protected SPITutStateOverview parent;
    protected Element container;
    protected ItsNatModalLayer layer;

    public SPITutStateOverviewPopup(SPITutStateOverview parent)
    {
        this(parent,parent.getSPIMainDocument().getSPIStateDescriptor("overview.popup"));
    }

    public SPITutStateOverviewPopup(SPITutStateOverview parent,SPIStateDescriptor stateDesc)
    {
        super(parent.getSPIMainDocument(),stateDesc,true);
        this.parent = parent;

        SPIMainDocument spiTutDoc = getSPIMainDocument();
        ItsNatHTMLDocument itsNatDoc = getItsNatHTMLDocument();
        HTMLDocument doc = itsNatDoc.getHTMLDocument();
        ItsNatComponentManager compMgr = itsNatDoc.getItsNatComponentManager();
        this.layer = compMgr.createItsNatModalLayer(null,false,1,0.5f,"black",null);
        HTMLBodyElement body = (HTMLBodyElement)doc.getBody();

        DocumentFragment frag = spiTutDoc.loadDocumentFragment("overview.popup");
        this.container = ItsNatTreeWalker.getFirstChildElement(frag);
        body.appendChild(container);

        ((EventTarget)container).addEventListener("click", this, false);

        //itsNatDoc.addCodeToSend("try{ window.scroll(0,-1000); }catch(ex){}");
        // try/catch is used to prevent some mobile browser does not support it
    }

    @Override
    public void handleEvent(Event evt)
    {
        dispose();
    }

    @Override
    public void dispose()
    {
        ((EventTarget)container).removeEventListener("click",this, false);
        container.getParentNode().removeChild(container);
        layer.dispose();

        parent.onDisposeOverviewPopup();
    }
}

This is the template file (overview_popup.html) associated to the name overview.popup:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:itsnat="http://itsnat.org/itsnat">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Overview Popup
</head>
<body>

    <div style="position:absolute; z-index:1; background:white; width:80%; height:80%; left:10%;  top:5%; padding:30px;">
        <h2>Overview Popup</h2>

        <p>Overview + popup is also a fundamental state so this text is processed by
            search engine crawlers (Google,Yahoo,Bing...), because you can reach this
            state on load time with a URL <a href="?st=overview.popup">like this</a>
            (you can find this text in the end of the page loaded).
        </p>

        <p><a onclick="setState('overview'); return false;" itsnat:nocache="true" href="?st=overview">Click to exit</a></p>

    </div>

</body>
</html>

Fundamental State detail and Secondary State «More detail»

SPITutStateDetail is another fundamental state and also contains a sub-state («More Detail»), however in this case this sub-state is not fundamental, not bookmarkable, hence there is no new class inherited from SPIStfulState and there is no call to SPIMainDocument.registerState(SPIState) when this sub-state is reached.

Source code of SPITutStateDetail:

package org.itsnat.spistfulhashbangtut;

import org.itsnat.spistful.SPIStfulState;
import org.itsnat.core.domutil.ItsNatTreeWalker;
import org.itsnat.spi.SPIStateDescriptor;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.events.EventTarget;
import org.w3c.dom.html.HTMLDocument;

public class SPITutStateDetail extends SPIStfulState implements EventListener
{
    protected Element detailMoreLink;
    protected Element detailMoreElem;
    protected boolean inserted = false;

    public SPITutStateDetail(SPITutMainDocument spiTutDoc,SPIStateDescriptor stateDesc)
    {
        super(spiTutDoc,stateDesc,true);

        HTMLDocument doc = getItsNatHTMLDocument().getHTMLDocument();
        this.detailMoreLink = doc.getElementById("detailMoreId");
        ((EventTarget)detailMoreLink).addEventListener("click",this,false);
    }

    @Override
    public void dispose()
    {
        ((EventTarget)detailMoreLink).removeEventListener("click",this,false);
    }

    @Override
    public void handleEvent(Event evt)
    {
        if (detailMoreElem == null)
        {
            DocumentFragment frag = spiDoc.loadDocumentFragment("detail.more");
            this.detailMoreElem = ItsNatTreeWalker.getFirstChildElement(frag);
        }

        if (!inserted)
        {
            Element contentParentElem = spiDoc.getContentParentElement();
            contentParentElem.appendChild(detailMoreElem);
            detailMoreLink.setTextContent("Hide");
            this.inserted = true;
        }
        else
        {
            detailMoreElem.getParentNode().removeChild(detailMoreElem);
            detailMoreLink.setTextContent("More Detail");
            this.inserted = false;
        }
    }
}

And the template fragment detail.html:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:itsnat="http://itsnat.org/itsnat">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Detail
</head>
<body>
    <h2>Detail</h2>
    <p>This "fundamental" state is processed by crawlers of search engines (Google, Yahoo, Bing...).
       The link below is used to load some new text, in this case the new
       state is not fundamental (is secondary) and the new text cannot be
       reached by crawlers of search engines.
    </p>
    <a href="javascript:;" id="detailMoreId">More Detail</a><br>
</body>
</html>

Now the link is pure AJAX based and the markup with more info («More Detail») only is inserted when end users click the link, therefore new inserted markup is not reached by web crawlers and there is no call to set this state as bookmarkable, following The Single Page Interface Manifesto this state (showing the «More Detail» text) is a secondary not a fundamental state.

The fragment with name detail.more is the file detail_more.html:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:itsnat="http://itsnat.org/itsnat">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>More Detail
</head>
<body>
    <span>
    <h3>More Detail</h3>
    <p>This text cannot be reached by search engines, because there is no
    fundamental state registered including this fragment on load time.
    </p>
    </span>
</body>
</html>

Fundamental State not_found

Finally we have the state not_found, this state is provided to show a «state of error» when the main page is being loaded with a state name unknown (for instance an old bookmark saved before a web site redesign changing state names). When this not_found state is reached the <body> content ofnot_found.html is inserted:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:itsnat="http://itsnat.org/itsnat">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>State Not Found
</head>
<body>

    <h2>State Not Found</h2>

</body>
</html>

Conclusion

This article has shown a generic example of how to build SPI stateful using hashbangs web sites with ItsNat similar to page based counterparts without sacrificing the typical features of the page paradigm like bookmarking, SEO, JavaScript disabled, Back/Forward buttons (history navigation), page visit counters etc. When a web site is being ported to SPI, pages are usually converted to states, these states can be fundamental and not fundamental (secondary). This tutorial has shown how fundamental states are very similar to pages including templating design based on pure HTML with all of benefits of SPI and how we can set as fundamental state virtually any state thanks to the server-centric nature of ItsNat, The Browser Is The Server approach and the fast-load feature.

Most of the code in this tutorial is infrastructure code, the mini-framework can be reused in many projects, this is the fixed cost of a SPI web site, any new fundamental state just require a new plain HTML template some minimal registration code and a new class inherited from SPIState, this class could be empty or be shared (the same class) for all fundamental states with just static content. In summary, the cost of adding a new fundamental state with only static content is almost the same as adding a simple HTML file like in page based development, with the advantage of no care and no repetition of headers, footers and, in general, static content outside of the content being added when changing states, avoiding the burden of repetition in the case of page based development, improving the end users experience thanks to the SPI open to highly interactive code by using AJAX events.

This is not the only approach, there are other alternatives like stateless and/or using History API, next articles will show these options.

Download, Online Demo and Links

Running online

Source code in GitHub