/spa/webmaster/
Под словом "настоящий" понимается, что страниц как бы много, хотя на самом деле она одна. :)
Т.е. в браузере в адресной строке визуально меняется текущий адрес, без символа #, т.е. по-настоящему. В истории браузера добавляются страницы, по которым можно перемещаться кнопками вперёд-назад истории браузера.
Реализовать это можно благодаря методу
history.pushState(state, title [, url]). Ну и важно продумать структуру сайта.Мне моя реализация нравится, что SEO-friendly. Контент показывается, если отключен Javascript.
Один скрипт отвечает за отдачу контента - index.php:
<?php
// Определяю, есть ли страница для отображения
$pagesArr = file("pages.txt");
$pageToShow = null;
$requestUrl = substr($_SERVER['REQUEST_URI'], strlen("/spa/webmaster"));
for ($i = 0; $i < count($pagesArr); $i ++)
{
$pageArr = explode("\t", $pagesArr[$i]);
if ( $requestUrl === $pageArr[0])
{
$pageToShow = $pageArr[0];
$title = trim($pageArr[2]);
$pageFile = $pageArr[1];
break;
}
}
if ($pageToShow === null)
{
header("HTTP/1.0 404 Not Found");
echo "404 Not Found";
exit;
}
echo "
<!DOCTYPE html>
<html>
<head>
<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">
";
echo"<title>".$title."</title>";
echo "
<link href=\"/spa/webmaster/spa-webmaster.css\" rel=\"stylesheet\" type=\"text/css\">
<link href=\"https://dmitriyzhuravlev.ru/style.css\" rel=\"stylesheet\" type=\"text/css\">
</head>
<body>
";
// Верх страницы
require "header.html";
echo "<h1 class=\"spa\">".$title."</h1>";
echo "<div class=\"spa pageContent\">";
if (file_exists ( "pages/".$pageFile.".html" ))
{
require "pages/".$pageFile.".html";
}
else
{
echo "Ошибка: для этой страницы не найдено файла с контентом.";
}
echo "</div>";
// Низ страницы
require "bottom.html";
?>
Все запросы идут на него, поэтому такой .htaccess в каталоге /spa/webmaster/:
RewriteEngine On RewriteRule ^[a-zA-Z0-9-/]*$ index.phpОбратите внимание, что в regexp нет точки, поэтому запросы с точкой, т.е., например, к файлам *.png пройдут как обычные запросы к файлам.
Скрипт смотрит какую страницу запрашивают и сверяется с файлом pages.txt, где каждая строка - это одна возможная страница, её "технические" значения разделены табуляторами:
/ 000 Что это за SPA? /javascript/operator-precedence/ 001 Приоритет операторов в Javascript /javascript/navigator-mediadevices-getusermedia/ 002 Метод navigator.mediaDevices.getUserMedia() /javascript/truthy-falsy/ 003 Truthy и falsy значения /css/backdrop-filter/ 004 CSS-свойство backdrop-filterДля удобства всё хранится в текстовых файлах, БД не используется.
А Javascript делает POST-запросы к файлу getpagespa.php:
<?php
// Этот PHP скрипт отдаёт JSON-объект с контентом запрошенной страницы после запроса методом POST.
$postBody = file_get_contents('php://input');
if ($postBody === false) { exit; }
$data = json_decode($postBody, true); // второй аргумент (true) означает, что надо вернуть массив (ассоциативный), а не объект
if ( array_key_exists("page", $data) === false)
{
echoAnswer ("PHP скрипт: получен некорректный JSON-объект без ключа page", false);
exit;
}
$pageFromRequest = $data["page"];
$pagesArr = file("pages.txt");
$pageToShow = null;
for ($i = 0; $i < count($pagesArr); $i ++)
{
$pageArr = explode("\t", $pagesArr[$i]);
if ( $pageFromRequest === $pageArr[0])
{
$pageToShow = $pageArr[0];
$title = trim($pageArr[2]);
$pageFile = $pageArr[1];
break;
}
}
if ($pageToShow === null)
{
echoAnswer("Запрошенной страницы нет в pages.txt", false);
exit;
}
if (file_exists ( "pages/".$pageFile.".html" ))
{
$content = file_get_contents("pages/".$pageFile.".html");
}
else
{
echoAnswer("Запрошенная страница есть в pages.txt, но отстутствует файл ".$pageFile, false);
exit;
}
echoAnswer($content, true);
function echoAnswer($text, $allOk) {
global $title, $pageFromRequest;
$answer = [
"text" => $text,
"allOk" => $allOk,
"title" => $title,
"page" => $pageFromRequest,
];
$text = json_encode( $answer );
echo $text;
}
?>
Ну, а это сам JS-код:
// Полифил метода closest
// На самом деле он не нужен, ведь в этом коде клики в старых браузерах работают как клики по обычному сайту, а не SPA.
window.Element&&!Element.prototype.closest&&(Element.prototype.closest=function(e){var t,o=(this.document||this.ownerDocument).querySelectorAll(e),n=this;do{for(t=o.length;--t>=0&&o.item(t)!==n;);}while(t<0&&(n=n.parentElement));return n});
document.addEventListener('click', handleSPAClick);
window.addEventListener('popstate', handlePopstate);
function handleSPAClick(e) {
if ( !fetch ) { return; } // старые браузеры - обработка клика как по обычному сайту, а не SPA
var linkElem = e.target.closest('a'); // ищу ссылку (тег a) по дереву вверх
if ( linkElem )
{
// определяю, какую страницу хочет открыть пользователь
var pageForUser = null;
var text = 'spa/webmaster';
var pos = linkElem.href.indexOf(text);
if (pos !== - 1)
{
pageForUser = linkElem.href.substr(pos + text.length);
}
if (pageForUser !== null)
{
e.stopPropagation();
e.preventDefault();
loadPage(pageForUser);
}
}
}
function loadPage(page, doNotPushState) {
// Функция подготавливает объект с контентом открываемой пользователем страницы
// page - string - страница, которую надо открыть
// doNotPushState - необязательный boolean - определяет нужно ли добавить в объект свойство, которое потом запрещает вызывать history.pushState в функции updateSPA
// console.log('Пользователь хочет открыть страницу:');
// console.log(page);
var bodyData = JSON.stringify( {page: page} ); // тело для POST запроса к PHP-скрипту
// console.log('Запрашиваю через fetch контент.');
fetch('/spa/webmaster/getpagespa.php',
{
method: 'POST',
headers: {'Content-Type': 'application/json;charset=utf-8'},
body: bodyData,
})
.then(ifFulfilled)
.catch(ifRejected);
function ifRejected(e)
{
console.log('Ошибка при выполнении fetch.');
console.log(e);
}
function ifFulfilled(response)
{
// console.log('Ответ получен.');
if (response.ok) { response.text().then( useResponse ); }
else
{
console.log('Проблема: response.ok равен ' + response.ok + ', а response.status равен ' + response.status);
}
}
function useResponse(answer)
{
answer = JSON.parse(answer); // строка превращается в объект
if (doNotPushState === true)
{
answer.doNotPushState = true;
}
updateSPA(answer);
}
}
function updateSPA (data) {
// Функция обновляет страницу: меняет содержимое тегов title, h1 и div с css-классом pageContent
// После этого меняет историю браузера (если требуется).
if (data.allOk !== true)
{
// Значение allOk заполняет PHP-скрипт
console.log( 'Возникла какая-то проблема. В ответе у allOk не стоит значение true.' );
return;
}
document.title = data.title;
document.querySelector('h1.spa').innerHTML = data.title;
document.querySelector('.pageContent.spa').innerHTML = data.text;
if (data.doNotPushState !== true) // Текущая функция запускается, если пользователь жмёт назад/вперёд в истории браузера, поэтому нельзя в таком случае добавлять в историю элемент.
{
data.doNotPushState = true;
history.pushState( {spaPageData: data}, 'Title', '/spa/webmaster' + data.page);
}
}
function handlePopstate(e) {
// Функция сработает при использованнии Вперёд/Назад истории браузера.
if (e.state && e.state.spaPageData)
{
// Используется старый объект state, "закешированный" браузером.
// Из-за этого может показываться старый контент, если контент за время просмотра успел обновиться.
// При желании можно исользовать код из блока else, который заново запрашивает содержимое страницы.
updateSPA(e.state.spaPageData);
}
else
{
// Сюда попадёт пользователь, когда кнопкой назад вернётся на самую первую страницу сайта, загруженную без SPA-технологии
// console.log('Нет event.state.');
var pageForUser = null;
var text = 'spa/webmaster';
var pos = location.href.indexOf(text);
if (pos !== - 1)
{
pageForUser = location.href.substr(pos + text.length);
}
if (pageForUser !== null)
{
loadPage(pageForUser, true); // второй параметр равный true означает, что не надо добавлять в историю браузера новый элемент
}
}
}
Ну и как обычно, в моём варианте SPA показывает всё нормально старым браузерам.SPA-сайт - это, конечно, интересный вариант построения сайта со своим плюсами и минусами.
(из-за чертовых спамеров урлы в коментах теперь писать нельзя)
Комментариев нет