Это первая из четырех статей серии о том, как создать общедоступные веб-сайты с одностраничным интерфейсом (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 с сохранением состояния на основе хеш-бэнгов, созданных для этого урока.
Этот мини-фреймворк имеет три пакета, содержащие следующие классы:
org.itsnat.spi
: Общие classesClasses:SPIMainDocumentConfig
,SPIMainDocument
,SPIState
,SPIStateDescriptor
org.itsnat.spistful
: Сохраняющее состояние specificClasses:SPIStfulMainDocument
,SPIStfulState
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.spistful
: SPIStfulMainDocument
и 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 SPITutStateOverview
, SPITutStateOverviewPopup
, SPITutStateDetail
.
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
Source code in GitHub