Статьи

Четыре варианта SPI: лица без гражданства, использующие хэшбэнгсы

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

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

В первом мы объяснили, как создать простой веб-сайт SPI с использованием подхода с отслеживанием состояния (используются веб-сеансы) и метода hashbang. В этой статье / уроке мы собираемся сделать то же самое, но в этом случае используем режим без сохранения состояния (представлен в ItsNat 1.3). 

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

Состояние в клиенте снова сохраняется в hashbangs, несмотря на то, что History API является идеальным подходом, hashbangs практически универсальны, браузеры, такие как старый Internet Explorer 8 (с небольшой, но значительной долей на рынке), не поддерживают History API. История API будет применена в следующих статьях.

Чтобы лучше понять эту статью, прочитанную перед предыдущей, некоторые концепции не будут объясняться снова.

SPI, SEO-совместимость и отношения без гражданства

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

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

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

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

Создайте сервлет, как объяснено в учебнике с хэшбэнгом с отслеживанием состояния.

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

Поскольку наш веб-сайт является SPI, мы хотели бы иметь более симпатичный URL, например  http: // localhost: 8080 / spistlesshashbangtut /

Конфигурация  web.xml такая же , как с учетом состояния учебник hashbang :

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

<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.http.HttpServletWrapper;
import org.itsnat.core.http.ItsNatHttpServlet;
import org.itsnat.core.tmpl.ItsNatDocumentTemplate;
import org.itsnat.spistlesshashbangtut.SPITutGlobalLoadRequestListener;
import org.itsnat.spistlesshashbangtut.SPITutMainLoadRequestListener;

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

        ItsNatHttpServlet itsNatServlet = getItsNatHttpServlet();

        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.addItsNatServletRequestListener(new SPITutGlobalLoadRequestListener());

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

        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");
    }
}

Это очень похоже на версию с состоянием, но с некоторыми тонкими различиями:

  • Нет интереса к сессиям, например, нет необходимости указывать количество таких сессий, как  itsNatCtx.setMaxOpenDocumentsBySession(4);.
  • Ни один глобальный прослушиватель событий не зарегистрирован, этот код  itsNatServlet.addEventListener(new SPITutGlobalEventListener()); отсутствует, потому что его целью  SPITutGlobalEventListener было спросить пользователей, что делать в случае потери сеанса.
  • События «основных» документов явно отключаются при вызове  docTemplate.setEventsEnabled(false);, это необходимо для того, чтобы сделать эту страницу без сохранения состояния (страница / документ загружена, но не сохраняется на сервере синхронно с клиентом), этот вызов необходим, потому что по умолчанию это состояние с состоянием.

In this example the default URL is:

http://localhost:8080/spistlesshashbangtut/

The main page of our web site is loaded as explained in the stateful hashbang version.

Main Page Processing

Returning to the servlet:

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

This call registers with the name «main» the page template file main.html (saved in 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 STATELESS 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(state_name,state_secondary_name)
    {
        if (typeof document.getItsNatDoc == "undefined") return; // Too soon, page is not fully loaded
        var userEvt = document.getItsNatDoc().createEventStateless();
        userEvt.setExtraParam('itsnat_doc_name',"main");
        userEvt.setExtraParam('state_name',state_name);
        if (state_secondary_name) userEvt.setExtraParam('state_secondary_name',state_secondary_name);
        document.getItsNatDoc().dispatchEventStateless(userEvt, 3 /*XHR_ASYNC_HOLD*/, 1000000);
    }
    window.spiSite.onBackForward = setState;

    function removeById(id)
    {
        var elem = document.getElementById(id);
        if (!elem) return;
        elem.parentNode.removeChild(elem);
    }
    </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 STATELESS <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>

This template is very similar to the stateful example, the main difference is the JavaScript code:

function setState(state_name,state_secondary_name)
{
    if (typeof document.getItsNatDoc == "undefined") return; // Too soon, page is not fully loaded
    var userEvt = document.getItsNatDoc().createEventStateless();
    userEvt.setExtraParam('itsnat_doc_name',"main");
    userEvt.setExtraParam('state_name',state_name);
    if (state_secondary_name) userEvt.setExtraParam('state_secondary_name',state_secondary_name);
    document.getItsNatDoc().dispatchEventStateless(userEvt, 3 /*XHR_ASYNC_HOLD*/, 1000000);
}
window.spiSite.onBackForward = setState;

function removeById(id)
{
    var elem = document.getElementById(id);
    if (!elem) return;
    elem.parentNode.removeChild(elem);
}

The setState method sends an «ItsNat stateless event» to server (take a look to the call dispatchEventStateless(...) call) providing the necessary info to generate a new state in server to propagate back to client. A stateless event is an extension of W3C DOM Events defined by ItsNat and is fired calling some public ItsNat methods from JavaScript like dispatchEventStateless(...).

Because the stateless nature of this example, the server does not remember any data sent by client, just generate the according Javascript code and markup to change the state in client and forget the request. The result is the same as the stateful hashbang example but with a different (stateless) approach based on the stateless capabilities of ItsNat.

Stateless events are used in this tutorial to notify the server about the next fundamental state to be loaded (in the stateful example stateful custom user events were used), any received stateless event in server will load a document specified by itsnat_doc_name parameter and will be processed by an event listener in server previously registered in the just loaded document.

Back to the servlet we also register a load listener for the main template:

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

The SPITutMainLoadRequestListener class is the same as the stateful hashbang example.

In stateless the SPITutMainLoadRequestListener.processRequest(…) method will be called in two different scenarios:

  1. When the servlet receives a new load request of this template that is when initially loading the page, conventional use
  2. When a stateless event has been received in server specifying the template of this document (load phase of stateless event processing)

In this tutorial the same template is going to be used for the initial load and for stateless event processing, this is the simplest (not mandatory) option, in this case is appropriated because the template is very simple, everything is in the same position when the page is loaded and when processing stateless events, there is no need of locById attributes in this tutorial (please read first about how ItsNat support stateless apps in Manual, a tutorial about using stateless mode exists).

A new SPITutMainDocument instance is created per call (load request), this instance holds an ItsNatHTMLDocument object which represents the client document (page) being loaded as usual.

Unlike the stateful mode, SPITutMainDocument objects are not going to be retained in server because the page template was registered «stateless» calling docTemplate.setEventsEnabled(false);, and when a SPITutMainDocument is created to dispatch a stateless event, this document object is ever stateless (not retained in server) because the nature of stateless event processing.

The class SPITutMainDocument inherits from SPIStlessHashbangMainDocument the main class of the mini-framework to create SPI SEO compatible stateless based on hashbang web sites under the package org.itsnat.spistlesshashbang.

Again we are using a mini-framework in this tutorial:

  1. org.itsnat.spi: generic classes, same as all other SPI web site tutorialsClasses: SPIMainDocumentConfigSPIMainDocumentSPIStateSPIStateDescriptor 
  2. org.itsnat.spistless: stateless specificClass: SPIStlessMainDocument 
  3. org.itsnat.spistlesshashbang: stateless and hashbang processing specificClasses: SPIStlessHashbangMainDocument 

We are not going to explain the classes of the package org.itsnat.spi because they are the same as seen in stateful hashbang tutorial. Note there is no SPIStlessState class (not needed).

Main Page Processing Stateless

Now is time to step down to describe how the mini-framework manages a stateless web site.

There is only one class into the package org.itsnat.spistlessSPIStlessMainDocument.

package org.itsnat.spistless;

import org.itsnat.spi.SPIMainDocumentConfig;
import org.itsnat.spi.SPIStateDescriptor;
import org.itsnat.core.ClientDocument;
import org.itsnat.core.event.ItsNatEventDOMStateless;
import org.itsnat.core.http.ItsNatHttpServletRequest;
import org.itsnat.core.http.ItsNatHttpServletResponse;
import org.itsnat.spi.SPIMainDocument;
import org.itsnat.spi.SPIState;
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 SPIStlessMainDocument extends SPIMainDocument
{
    public SPIStlessMainDocument(ItsNatHttpServletRequest request, ItsNatHttpServletResponse response,SPIMainDocumentConfig config)
    {
        super(request, response,config);

        if (itsNatDoc.isCreatedByStatelessEvent())
        {
            EventListener listener = new EventListener()
            {
                @Override
                public void handleEvent(Event evt)
                {
                    ItsNatEventDOMStateless itsNatEvt = (ItsNatEventDOMStateless)evt;

                    String stateName = (String)itsNatEvt.getExtraParam("state_name");

                    changeState(stateName,itsNatEvt);
                }
            };

            itsNatDoc.addEventListener(listener);
        }
    }

    public SPIState changeState(String stateName)
    {
        return changeState(stateName,null);
    }

    public SPIState changeState(String stateName,ItsNatEventDOMStateless itsNatEvt)
    {
        SPIStateDescriptor stateDesc = config.getSPIStateDescriptor(stateName);
        if (stateDesc == null)
        {
            return changeState(config.getNotFoundStateName(),itsNatEvt);
        }

        // Cleaning previous state:
        if (!itsNatDoc.isLoading())
        {
            ClientDocument clientDoc = itsNatDoc.getClientDocumentOwner();
            String contentParentRef = clientDoc.getScriptUtil().getNodeReference(config.getContentParentElement());
            clientDoc.addCodeToSend("window.spiSite.removeChildren(" + contentParentRef + ");");  // ".innerHTML = '';"
        }

        // Setting new state:
        changeActiveMenu(stateName);

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

        return createSPIState(stateDesc,itsNatEvt);
    }

    public abstract SPIState createSPIState(SPIStateDescriptor stateDesc,ItsNatEventDOMStateless itsNatEvt);


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

        Element currentMenuItemElem = config.getMenuElement(mainMenuItemName);
        for(Element menuItemElem : config.getMenuElementMap().values())
        {
            onChangeActiveMenu(menuItemElem,(currentMenuItemElem == menuItemElem));
        }
    }

    public abstract void onChangeActiveMenu(Element menuItemElem,boolean active);
}

Some parts of this class are very similar to the stateful example, others are very different.

For instance:

if (itsNatDoc.isCreatedByStatelessEvent())
{
    EventListener listener = new EventListener()
    {
        @Override
        public void handleEvent(Event evt)
        {
            ItsNatEventDOMStateless itsNatEvt = (ItsNatEventDOMStateless)evt;

            String stateName = (String)itsNatEvt.getExtraParam("state_name");

            changeState(stateName,itsNatEvt);
        }
    };
    itsNatDoc.addEventListener(listener);
}

The method isCreatedByStatelessEvent() is used to distinguish conventional load page from stateless event processing, in case of document load for stateless event processing we must register a listener going to be called inmediatelly after the load phase of the document created by the stateless event, because a new special stateless event is again (and automatically) fired.

Some things are different when processing a stateless event, the client page is loaded to be modified generating JavaScript in the event listener dispatch phase but the document loaded may differ from the client state, for instance, this is the reason of this DOM cleaning in client to prepare the client to receive the new inserted markup of the new state, take a look to the code of changeState():

public SPIState changeState(String stateName,ItsNatEventDOMStateless itsNatEvt)
{
    SPIStateDescriptor stateDesc = config.getSPIStateDescriptor(stateName);
    if (stateDesc == null)
    {
        return changeState(config.getNotFoundStateName(),itsNatEvt);
    }

    // Cleaning previous state:
    if (!itsNatDoc.isLoading())
    {
        ClientDocument clientDoc = itsNatDoc.getClientDocumentOwner();
        String contentParentRef = clientDoc.getScriptUtil().getNodeReference(config.getContentParentElement());
        clientDoc.addCodeToSend("window.spiSite.removeChildren(" + contentParentRef + ");");  // ".innerHTML = '';"
    }

    // Setting new state:
    changeActiveMenu(stateName);

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

    return createSPIState(stateDesc,itsNatEvt);
}

Some explanation is needed: the method changeState is going to be called in three different phases:

  1. When the initial page loads (isLoading() is true): markup of the fragment content is inserted as markup (because fast-load mode is enabled)
  2. When the document for processing a stateless event loads (isLoading() is true): markup of the fragment content is inserted as DOM, no JS is generated in this load phase (normal behavior when processing a stateless event)
  3. When the stateless event is being dispatched by the registered event listener ((isLoading() is false): modified DOM generates JavaScript to be sent to client, this is the reason of custom code to clean the current content in client, because the no initial content is defined in the document loaded to process the stateless event.

This event ItsNatEventDOMStateless itsNatEvt is dispatched by the event listener that was registered on load time when the document is loaded by a stateless event, and executed just after the load phase, this stateless event transports the necessary data sent by the client to change the state in client, because this is the stateless event in dispatching phase any change to the DOM will generate JavaScript DOM code to do the same in client to bring the just loaded document to the required state in client.

Main Page Processing and Hashbangs

Time to another step down to know how to manage hashbangs.

The package org.itsnat.spistlesshashbang has only one class SPIStlessHashbangMainDocument, yes this class inherits from SPIStlessMainDocument and specifies how to manage hashbangs in this stateless context.

package org.itsnat.spistlesshashbang;

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.spistless.SPIStlessMainDocument;

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

        if (!itsNatDoc.isCreatedByStatelessEvent())
        {
            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);
        }

    }
}

Code is basically the same as stateful but the sentence if (!itsNatDoc.isCreatedByStatelessEvent()), because this code only apply to normal page loading, not for stateless event processing (even in load phase).

Everything explained in stateful hashbang tutorial about Google AJAX Crawling specification is also applied here.

Because this tutorial uses hashbangs, the JavaScript library spi_hashbang.js is the same as the stateful version.

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.

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

package org.itsnat.spistlesshashbangtut;

import org.itsnat.spistlesshashbang.SPIStlessHashbangMainDocument;
import org.itsnat.core.event.ItsNatEventDOMStateless;
import org.itsnat.core.http.ItsNatHttpServletRequest;
import org.itsnat.core.http.ItsNatHttpServletResponse;
import org.itsnat.spi.SPIMainDocumentConfig;
import org.itsnat.spi.SPIState;
import org.itsnat.spi.SPIStateDescriptor;
import org.w3c.dom.Element;

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

    @Override
    public SPIState changeState(String stateName,ItsNatEventDOMStateless itsNatEvt)
    {
        SPIState state = super.changeState(stateName,itsNatEvt);

        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 SPIState createSPIState(SPIStateDescriptor stateDesc,ItsNatEventDOMStateless itsNatEvt)
    {
        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"))
        {
            String stateSecondaryName = itsNatEvt != null? (String)itsNatEvt.getExtraParam("state_secondary_name") : null;
            return new SPITutStateDetail(this,stateDesc,stateSecondaryName);
        }
        else
            return null;
    }

    @Override
    public void onChangeActiveMenu(Element menuItemElem,boolean active)
    {
        if (active)
        {
            menuItemElem.setAttribute("class","menuOpSelected");
        }
        else
        {
            menuItemElem.setAttribute("class","foo");
            menuItemElem.removeAttribute("class");
        }
    }
}

Code is similar to the stateful version but not the same, in stateless we need to provide the necessary data from client to recreate in server the client state and perform the required operations to change the client to a new state.

For instance:

...
else if (stateName.equals("detail"))
{
    String stateSecondaryName = itsNatEvt != null? (String)itsNatEvt.getExtraParam("state_secondary_name") : null;
    return new SPITutStateDetail(this,stateDesc,stateSecondaryName);
}
...

State definition is very different in stateless, in stateful we can easily remember in server the current state in client, in stateless is not possible unless you make some custom use of session, we are not going to use sessions in no way, so we need the necessary data from client, this is the reason of thestate_secondary_name parameter received for instance to insert and manage the «detail» state and «more detail» substate, whether «more detail» must be «shown» or «hidden».

This is the detail.html link used to change from «show» to «hide» and viceversa:

<a itsnat:nocache="true" id="detailMoreId" href="javascript:;" onclick="setState('detail',this.getAttribute('action')); return false;">Hide|More Detail</a><br>

The attribute action is defined to save in client the current state of the page, and used to send to the server the current state to change accordingly. In this example we only need a second parameter, more complex seb sites will require a more complex context captured to send to server.

@Override
public void onChangeActiveMenu(Element menuItemElem,boolean active)
{
    if (active)
    {
        menuItemElem.setAttribute("class","menuOpSelected");
    }
    else
    {
        menuItemElem.setAttribute("class","foo");
        menuItemElem.removeAttribute("class");
    }
}

This code is different to the stateful version, we know the new menu item to activate but we do not know what is the current selected (remember it is stateless), therefore we clear all menu items and select the new active (this method is called by the base class for all menu items).

This is the SPITutStateDetail:

package org.itsnat.spistlesshashbangtut;

import org.itsnat.core.ClientDocument;
import org.itsnat.core.domutil.ItsNatTreeWalker;
import org.itsnat.spi.SPIState;
import org.itsnat.spi.SPIStateDescriptor;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.html.HTMLDocument;

public class SPITutStateDetail extends SPIState
{
    public SPITutStateDetail(SPITutMainDocument spiTutDoc,SPIStateDescriptor stateDesc,String stateSecondaryName)
    {
        super(spiTutDoc,stateDesc,true);

        HTMLDocument doc = getItsNatHTMLDocument().getHTMLDocument();
        Element detailMoreLink = doc.getElementById("detailMoreId");

        if ("moreDetail".equals(stateSecondaryName))
        {
            DocumentFragment frag = spiTutDoc.loadDocumentFragment("detail.more");
            Element detailMoreElem = ItsNatTreeWalker.getFirstChildElement(frag);
            detailMoreElem.setAttribute("id", "detailContentId");

            Element contentParentElem = spiTutDoc.getContentParentElement();
            contentParentElem.appendChild(detailMoreElem);
            detailMoreLink.setTextContent("Hide");
            detailMoreLink.setAttribute("action","lessDetail");
        }
        else
        {
            ClientDocument clientDoc = getItsNatHTMLDocument().getClientDocumentOwner();
            clientDoc.addCodeToSend("removeById('detailContentId');");

            detailMoreLink.setTextContent("More Detail");
            detailMoreLink.setAttribute("action","moreDetail");
        }
    }
}

States are inherited from org.itsnat.spi.SPIState, no need of dispose method (necessary in stateful).

In stateless there is less automatic management of client state and some custom JavaScript is convenient, for instance:

ClientDocument clientDoc = getItsNatHTMLDocument().getClientDocumentOwner();
clientDoc.addCodeToSend("removeById('detailContentId');");

This code calls the auxiliary JavaScript method removeById defined in client to remove the «more detail» content, this is needed because by default our stateless document do not include a «more detail» fragment DOM, in practice you usually do not need automatically generated JavaScript from server to remove code in client because removing code is a very easy task in client, ItsNat helps you a lot in stateless for inserting big and complex parts of markup in client usually mixed with data loaded in server, but removing client DOM is trivial with custom JavaScript.

The state «overview» including a popup («overview.popup») is an example of how we can make a limited use of ItsNat components also in stateless:

package org.itsnat.spistlesshashbangtut;

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

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

        if (showPopup) showOverviewPopup();
        else cleanOverviewPopup();
    }

    public void showOverviewPopup()
    {
        new SPITutStateOverviewPopup(this);
    }

    public void cleanOverviewPopup()
    {
        SPITutStateOverviewPopup.dispose(this);
    }

}
package org.itsnat.spistlesshashbangtut;

import org.itsnat.comp.ItsNatComponentManager;
import org.itsnat.comp.layer.ItsNatModalLayer;
import org.itsnat.core.ClientDocument;
import org.itsnat.core.domutil.ItsNatTreeWalker;
import org.itsnat.core.html.ItsNatHTMLDocument;
import org.itsnat.spi.SPIMainDocument;
import org.itsnat.spi.SPIState;
import org.itsnat.spi.SPIStateDescriptor;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.html.HTMLBodyElement;
import org.w3c.dom.html.HTMLDocument;

public class SPITutStateOverviewPopup extends SPIState
{
    protected SPITutStateOverview parent;
    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();
        ItsNatModalLayer layer = compMgr.createItsNatModalLayer(null,false,1,0.5f,"black",null);
        Element parentLayer = layer.getElement();
        parentLayer.setAttribute("id","overviewPopupLayerContainerId");

        HTMLBodyElement body = (HTMLBodyElement)doc.getBody();

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

        container.setAttribute("id","overviewPopupContentContainerId");

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

    public static void dispose(SPITutStateOverview parent)
    {
        ClientDocument clientDoc = parent.getItsNatHTMLDocument().getClientDocumentOwner();
        clientDoc.addCodeToSend("removeById('overviewPopupLayerContainerId');");
        clientDoc.addCodeToSend("removeById('overviewPopupContentContainerId');");
    }
}

Remaining classes SPITutGlobalLoadRequestListenerSPITutMainLoadRequestListener are the same as stateful hashbang counterparts. In stateful has no sense a SPITutGlobalEventListener as explained before.

Conclusion

This tutorial has shown a generic example of how to build SPI stateless 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. Use the mini-framework to build your SPI sites using this approach if you want.

Download, Online Demo and Links

See the demo running online (try to clear cookies to check there is no need of keep client state in server).

Download the source code in GitHub.