Прогрессивная загрузка XML страниц — это загрузка с одновременным показом уже загруженных и обработанных частей XML страницы пока XSLT шаблон всё ещё обрабатывает остальные части.
У нас есть очень большой XML. Это статья с очень большим количеством комментариев. На медленном и нестабильном мобильном интернете её загрузки можно и не дождаться. Во время загрузки случается обрыв связи и XML остаётся не догруженным. Казалось бы можно просто обновить страницу и браузер бы просто догрузил недостающую часть. Но нет. Браузер грузит страницу заново и снова это не удаётся и мы видим ошибку вместо страницы.
Но выход из этой ситуации есть. Мы разделим XML на маленькие кусочки которые будут успевать загрузиться на медленном канале и попадут в кеш. Бонусом мы получаем защиту от недогруза и прогрессивную загрузку.
С чем мы будем работать
Статья в XML
Этот файл содержит заголовок, описание, текст статьи и много много комментариев. Если быть точнее, то 5962 комментария.
У каждого комментария есть атрибут 'индекс'. Этот индекс используется другими комментариями в атрибуте 'на' для того чтобы указать что комментарий является ответом на комментарий, индекс которого указан.
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="статья.xslt" type="text/xsl"?>
<статья>
<title xmlns="http://www.w3.org/1999/xhtml">Lorem Ipsum</title>
<meta name="description" content="Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit..." xmlns="http://www.w3.org/1999/xhtml"/>
<текст>Quisque sollicitudin malesuada urna, id tincidunt quam. Pellentesque luctus, sem vel posuere efficitur, est enim pulvinar orci, et vulputate orci dui a libero. Nam eget mauris sed diam pretium fermentum. Proin euismod ultrices justo, non aliquet ipsum bibendum at. Duis eget laoreet mauris. Morbi tristique arcu libero, quis pharetra justo bibendum eget. In at quam sed lacus vestibulum tincidunt quis ut ex.</текст>
<комментарий индекс="1">
Nunc sit amet ligula mauris. Integer vel nisi ac turpis rhoncus suscipit. Fusce elementum ut elit quis rutrum. Nulla bibendum placerat ex pulvinar accumsan. Praesent vestibulum hendrerit accumsan. Sed quis ligula pretium, condimentum enim in, cursus elit. Phasellus quis mauris arcu. Integer congue ex et ante porttitor vestibulum. Donec vel mauris venenatis, ultrices leo in, dapibus metus. Morbi pharetra eleifend libero nec efficitur. Fusce efficitur et ligula quis scelerisque. Curabitur eget nibh at nunc lacinia fringilla et eget quam. Praesent malesuada, odio in pulvinar semper, libero nunc consectetur lorem, vel egestas erat tellus nec ipsum.
</комментарий>
<!-- ... -->
<комментарий на="1" индекс="4">
Praesent varius vitae arcu sed imperdiet. Proin pulvinar a augue blandit scelerisque. Pellentesque lectus erat, gravida lobortis lorem ut, dignissim tempor felis. Nunc porttitor libero quis est sodales, non ornare metus eleifend. Cras orci lacus, auctor non sollicitudin lacinia, ultricies vitae diam. Fusce eu leo varius, interdum sapien ac, vestibulum orci. Sed placerat feugiat odio, vitae tempor tellus volutpat tincidunt. Cras dapibus est et mi euismod ornare. Vestibulum accumsan justo non volutpat dictum. Curabitur porttitor, magna id euismod tincidunt, metus nisl fermentum tellus, id pulvinar leo nibh vel velit. Sed gravida gravida sapien et porttitor. Nam volutpat, ex id faucibus commodo, arcu arcu fringilla mi, et volutpat nunc turpis at ligula. Pellentesque et vulputate ligula, lobortis dignissim ligula. Nulla laoreet auctor ultricies. Interdum et malesuada fames ac ante ipsum primis in faucibus.
</комментарий>
<!-- ...и ещё много много комментариев -->
</статья>
Основной XSLT шаблон
Это небольшой XSLT шаблон, который преобразует XML в полноценный xHTML и собирает из списка ветки комментариев.
<xsl:stylesheet
version="1.0"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:x="http://www.w3.org/1999/xhtml"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" encoding="UTF-8"/>
<!-- ключь который выбирает ответы на комментарий -->
<xsl:key name="ответы" match="комментарий" use="@на"/>
<!-- копирующий шаблон -->
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*" />
</xsl:copy>
</xsl:template>
<!-- основной xHTML код -->
<xsl:template match="статья">
<html>
<head>
<!-- в статье для хранения заголовка и цитаты использованы xHTML теги title и meta -->
<!-- вставляем эти теги на их положенные места -->
<xsl:copy-of select="x:title | x:meta[@name = 'description']"/>
<style>
.комментарий {
padding: 1vw;
min-width: 20em;
border: 1px solid gray;
color: black;
background-color: white;
}
.текст {
max-width: 46em;
margin: auto;
text-align: justify;
}
</style>
</head>
<body>
<!-- копируем атрибуты(они должны идти до дочерних элементов)-->
<xsl:apply-templates select="@*" />
<div class="текст">
<!-- берём содержимое тега 'title' как заголовок статьи -->
<h1><xsl:value-of select="x:title"/></h1>
<!-- берём 'content' тега 'meta' как цитату для статьи -->
<i><xsl:value-of select="x:meta[@name = 'description']/@content"/></i>
<!-- берём содержимое тега 'текст' как текст для статьи -->
<p><xsl:apply-templates select="текст/node()" /></p>
</div>
<!-- вызываем шаблон для блока с комментариями -->
<xsl:apply-templates select="." mode="комментарии"/>
</body>
</html>
</xsl:template>
<!-- блок с комментариями отдельно для возможности его замены -->
<xsl:template match="статья" mode="комментарии">
<details id="комментарии" open=''>
<summary>Комментарии</summary>
<!-- выбираем комментарии которые не являются ответом @на другие комментарии -->
<xsl:apply-templates select="комментарий[not(@на)]"/>
</details>
</xsl:template>
<!-- оформляем комментарий -->
<xsl:template match="комментарий">
<!-- добавляем класс "комментарий" и индекс комментария как идентификатор -->
<div class="комментарий" id="{@индекс}">
<!-- копируем остальные атрибуты комментария -->
<xsl:apply-templates select="@*" />
<!-- показываем индекс комментария и делаем его ссылкой -->
<a href="#{@индекс}"><xsl:value-of select="@индекс"/></a><xsl:text>: </xsl:text>
<div class="текст">
<!-- выводим текст комментария -->
<xsl:apply-templates select="node()" />
</div>
<!-- вызываем шаблон для блока ответов -->
<xsl:apply-templates select="." mode="ответы"/>
</div>
</xsl:template>
<!-- блок с ответами отдельно для возможности его замены -->
<xsl:template match="комментарий" mode="ответы">
<!-- проверяем есть ли ответы на комментарий -->
<xsl:if test="key('ответы', @индекс)">
<details open=''>
<summary>Ответы</summary>
<!-- показываем ответы -->
<xsl:apply-templates select="key('ответы', @индекс)"/>
</details>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Тестируем
Ставим в браузере ограничение скорости GPRS. Открываем страницу и уходим пить чай. На экране будет предыдущая страница, до тех пор, пока не загрузится полностью XML файл и отработает XSLT шаблон.
Полная загрузка: 10,77 мин
Обрабатываем и делим XML файл
Нам надо разделить статью на часть с текстом статьи и несколько частей которые буду содержать комментарии.
В браузере пользователя сразу будет отображена часть с текстом с статьи и по мерере загрузки частей комментариев будут появлятся и они.
Поскольку работать мы будем с XML файлом, то почему бы операции с ним не доверить также XSLT шаблонам.
Задаём межстраничные связи комментариев
Шаблону задаётся количество комментариев на один XML файл и он комментариям дописывает атрибут 'страницы-ответов', в котором перечисляются индексы XML файлов, в которых будут находится ответы на этот комментарий.
xslt\задать-страницы-ответов.xslt:
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" encoding="UTF-8"/>
<xsl:key name="ответы" match="комментарий" use="@на"/>
<!-- количество комментариев на страницу -->
<xsl:param name="комментариев" />
<!-- копирующий шаблон -->
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*" />
</xsl:copy>
</xsl:template>
<!-- обрабатываем все комментарии -->
<xsl:template match="комментарий">
<!-- определяем страницу этого комментария -->
<xsl:param name="текущая-страница" select="floor(@индекс div $комментариев)" />
<!-- находим ответы на других страницах -->
<xsl:param name="ответы-дальше" select="key('ответы', @индекс)[floor(@индекс div $комментариев) > $текущая-страница]"/>
<!-- копируем комментарий -->
<xsl:copy>
<!-- копируем атрибуты "индекс" и "на" -->
<xsl:apply-templates select="@*" />
<!-- если есть ответы на других страницах -->
<xsl:if test="$ответы-дальше">
<!-- добавляем атрибут "страницы-ответов" -->
<xsl:attribute name="страницы-ответов">
<!-- записываем в него номера страниц ответов -->
<xsl:call-template name="страницы-ответов">
<!-- передаём ответы с других страниц -->
<xsl:with-param name="ответы" select="$ответы-дальше" />
</xsl:call-template>
</xsl:attribute>
</xsl:if>
<!-- копируем текст и другие элементы сообщения -->
<xsl:apply-templates select="node()" />
</xsl:copy>
</xsl:template>
<xsl:template name="страницы-ответов">
<!-- ответы с этой и следующих страниц -->
<xsl:param name="ответы"/>
<!-- номер текущей страницы -->
<xsl:param name="номер-страницы" select="floor($ответы[1]/@индекс div $комментариев)"/>
<!-- находим ответы на следующих страницах -->
<xsl:param name="ответы-дальше" select="$ответы[floor(@индекс div $комментариев) > $номер-страницы]"/>
<!-- записываем номер текущей страницы -->
<xsl:value-of select="$номер-страницы"/>
<!-- если есть ответы на следующих страницах -->
<xsl:if test="$ответы-дальше">
<!-- отделяем пробелом номер следующей страницы -->
<xsl:text> </xsl:text>
<!-- вызываем это же шаблон -->
<xsl:call-template name="страницы-ответов">
<!-- передаём ответы со следующих страниц -->
<xsl:with-param name="ответы" select="$ответы-дальше" />
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Определяем индекс последнего XML файла с комментариями
Шаблон просто делит количество комментариев всего на количество комментариев на страницу и выводит целое число меньшее или равное результату.
xslt\индекс-последней-страницы.xslt:
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<!-- в данном случае нам нужно вывести только число поэтому переключаем 'method' на 'text' -->
<xsl:output method="text" encoding="UTF-8"/>
<!-- количество комментариев на один XML файл -->
<xsl:param name="количество" select="100" />
<xsl:template match="/">
<!-- вычисляем и выводим индекс последнего XML файла -->
<xsl:value-of select="floor(count(//комментарий) div $количество)"/>
</xsl:template>
</xsl:stylesheet>
Делим комметарии на диапазоны
Шаблон выбирает из статьи комментарии для заданного индекса('страница') XML файла.
xslt\выделить-диапазон.xslt:
<xsl:stylesheet
version="1.0"
xmlns:x="http://www.w3.org/1999/xhtml"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" encoding="UTF-8"/>
<xsl:param name="путь" />
<!-- количество комментариев на страницу -->
<xsl:param name="комментариев"/>
<!-- номер страницы для которой выбираем комментарии -->
<xsl:param name="страница"/>
<xsl:template match="/">
<!-- задаём ссылку на шаблон который соберёт и загрузит статью полностью -->
<xsl:processing-instruction name="xml-stylesheet">href="../сборка-статьи.xslt" type="text/xsl"</xsl:processing-instruction>
<!-- переходим сразу к статье -->
<xsl:apply-templates select="статья" />
</xsl:template>
<xsl:template match="статья">
<статья путь="{$путь}">
<!-- копируем title и meta теги -->
<xsl:copy-of select="x:title | x:meta[@name = 'description']"/>
<!-- копируем диапазон комментариев для заданной страницы -->
<xsl:copy-of select="комментарий[floor(@индекс div $комментариев) = $страница]" />
</статья>
</xsl:template>
</xsl:stylesheet>
Оставляем только текст статьи
Шаблон удаляет все комментарии, оставляя только текст статьи.
xslt\убрать-комментарии.xslt:
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" encoding="UTF-8"/>
<!-- копирующий шаблон -->
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*" />
</xsl:copy>
</xsl:template>
<xsl:template match="/">
<!-- добавляем шаблон прогрессивной загрузки -->
<xsl:processing-instruction name="xml-stylesheet">href="../прогрессивная-загрузка.xslt" type="text/xsl"</xsl:processing-instruction>
<!-- переходим сразу к статье -->
<xsl:apply-templates select="статья" />
</xsl:template>
<!-- удаляем все комментарии -->
<xsl:template match="комментарий"/>
</xsl:stylesheet>
Скачиваем msxsl.exe
Для использования шаблонов в коммандной строке нам понадобится простая утилита msxsl.exe. Ей мы можем задать XML документ и XSLT шаблон и получить результат в stdout или в заданный файл.
CMD файл
Теперь нам необходимо применить эти все шаблоны к XML файлу статьи.
progressive.cmd lorem-ipsum.xml 100
progressive.cmd:
chcp 65001
rem Создаём дирректорию с именем исходного XML файла без расширения
md "%~dpn1"
rem Задаём межстраничные связи комментариев
msxsl.exe %1 "%~dp0xslt\задать-страницы-ответов.xslt" -xw -o "%~dpn1\index.xml" комментариев=%2
rem Определяем индекс последнего XML файла с комментариями
for /f "delims=" %%a in ('msxsl.exe %1 ^"%~dp0xslt\индекс-последней-страницы.xslt^" комментариев^=%2') do set последняя=%%a
rem Делим комметарии на диапазоны
for /l %%i in (0,1,%последняя%) do msxsl.exe -o "%~dpn1\%%i.xml" "%~dpn1\index.xml" "%~dp0xslt\выделить-диапазон.xslt" комментариев=%2 страница=%%i путь="%~n1"
rem Оставляем только текст статьи
msxsl.exe "%~dpn1\index.xml" "%~dp0xslt\убрать-комментарии.xslt" -o "%~dpn1\index.xml"
Собираем в браузере
Прогрессивная загрузка
Этот шаблон отображает текст статьи а в фрейме запускает загрузку статьи с комментариями. Как только блок комментариев появляется во фрейме он переносится на основную страницу. Второй шаблон в это время продожает подгружать комментарии и они появляются на основной странице сразу как только тот обработает очередную порцию.
прогрессивная-загрузка.xslt
<xsl:stylesheet
version="1.0"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:x="http://www.w3.org/1999/xhtml"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" encoding="UTF-8"/>
<!-- подключаем шаблон "статья.xslt" -->
<xsl:include href="%D1%81%D1%82%D0%B0%D1%82%D1%8C%D1%8F.xslt"/>
<xsl:template match="статья" mode="комментарии">
<!-- в этом фрейме будет загружаться статья вместе с комментариями -->
<iframe style="display:none;" id="фрейм-загрузчик" src="0.xml"/>
<script><![CDATA[
setTimeout(function(){
var фрейм = document.querySelector("#фрейм-загрузчик");
if (фрейм && фрейм.contentDocument)
{
var комментарии = фрейм.contentDocument.getElementById('комментарии');
// ждём пока во фрейме появится блок комментариев
if (комментарии)
{
// добавляем этот блок на эту страницу
document.body.appendChild(комментарии);
return;
}
}
// ставим новый Timeout пока не выполнятся все условия
setTimeout(arguments.callee, 100);
}, 100)]]>
</script>
<noscript>
<!-- Если на странице запрещён JavaScript пользователь может вручную перейти на статью с комментариями -->
<a href="0.xml">Комментарии</a>
</noscript>
</xsl:template>
</xsl:stylesheet>
Сборка статьи с комментариями
Этот шаблон загружает основной текст статьи и подгружает комментарии из всех XML файлов.
сборка-статьи.xslt
<xsl:stylesheet
version="1.0"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:x="http://www.w3.org/1999/xhtml"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" indent="yes" encoding="UTF-8"/>
<!-- подключаем шаблон "статья.xslt" -->
<xsl:include href="%D1%81%D1%82%D0%B0%D1%82%D1%8C%D1%8F.xslt"/>
<!-- создаём ключь который автоматом выберет в текущем документе ответы на комментарий -->
<xsl:key name="ответы" match="комментарий" use="@на"/>
<xsl:param name="путь" select="статья/@путь"/>
<xsl:template match="/">
<!-- даём отработать шаблону "статья.xslt" -->
<xsl:apply-templates select="document(concat($путь, '/index.xml'))/статья"/>
</xsl:template>
<!-- из подключённого шаблона мы вернёмся сюда -->
<xsl:template match="статья" mode="комментарии">
<details id="комментарии" open=''>
<!-- на этом этапе основная страница уже сможет взять блок комментариев из фрейма -->
<summary>Комментарии</summary>
<!-- начинаем загрузку комментариев -->
<xsl:call-template name="загрузить-страницу"/>
</details>
</xsl:template>
<xsl:template name="загрузить-страницу">
<!-- начинаем с XML с номером 0 -->
<xsl:param name="номер" select="0"/>
<!-- загружаем XML -->
<xsl:param name="страница" select="document(concat($путь, '/', $номер, '.xml'))"/>
<!-- если XML загружен -->
<xsl:if test="$страница">
<!-- запускаем обработку комментариев к статье -->
<xsl:apply-templates select="$страница//комментарий[not(@на)]" />
<!-- запускаем шаблон для следующего XML документа -->
<xsl:call-template name="загрузить-страницу">
<!-- задаём следующий номер XML -->
<xsl:with-param name="номер" select="$номер + 1" />
</xsl:call-template>
</xsl:if>
</xsl:template>
<!-- для загрузки ответов на комментарии будет вызван этот шаблон -->
<xsl:template match="комментарий" mode="ответы">
<!-- проверяем есть ли ответы на комментарий в этом XML или на других страницах -->
<xsl:if test="key('ответы', @индекс) or @страницы-ответов">
<details open=''>
<summary>Ответы</summary>
<!-- показываем ответы из этого XML -->
<xsl:apply-templates select="key('ответы', @индекс)"/>
<!-- запускаем загрузку комментариев из других XML -->
<xsl:call-template name="загрузить-ответы"/>
</details>
</xsl:if>
</xsl:template>
<xsl:template name="загрузить-ответы">
<!-- получаем список номеров страниц -->
<xsl:param name="страницы" select="concat(@страницы-ответов, ' ')"/>
<!-- берём первый из списка -->
<xsl:param name="номер" select="substring-before($страницы, ' ')"/>
<!-- проверяем что он не пустой -->
<xsl:if test="$номер">
<!-- загружаем XML -->
<xsl:apply-templates select="document(concat($путь, '/', $номер, '.xml'))" mode="ответы">
<!-- передаём индекс комментария ответы на который надо загрузить -->
<xsl:with-param name="на" select="@индекс"/>
</xsl:apply-templates>
<!-- вызываем снова этот же шаблон -->
<xsl:call-template name="загрузить-ответы">
<!-- передаём список оставшихся страниц -->
<xsl:with-param name="страницы" select="substring-after($страницы, ' ')"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template match="/" mode="ответы">
<!-- это индекс комментария ответы на который надо получить -->
<xsl:param name="на"/>
<!-- отправляем на оформление ответы на комментарий -->
<xsl:apply-templates select="key('ответы', $на)"/>
</xsl:template>
</xsl:stylesheet>
Тестируем
Включаем в браузере ограничение скорости GPRS и запускаем загрузку страницы.
Загрузка текста статьи: 2,28с
Загрузка первых комментариев: 14,41с (только в FireFox)
Полная загрузка: 11,33 мин