Недавно я столкнулся со следующим вопросом о переполнении стека, касающимся обработки состояния при обновлении в одностраничном приложении (SPA). Мне показался интересным подход OP к разработке SPA: они хотели избежать сторонних библиотек и фреймворков, потому что они «сначала предпочитают более простые способы».
Вот вопрос, из-за которого возникла эта статья:
Я сделал простое доказательство концепции, чтобы проиллюстрировать, как можно достичь того, что они искали, что можно увидеть здесь (или клонировать из github).
В загруженной версии я использую последний параметр URL, чтобы определить, что нужно загрузить. Это стандартная практика, но она требует конфигурации на стороне сервера, чтобы направлять все входящие запросы в один файл.
Другая альтернатива (вариант кода по умолчанию на github) использует фрагмент привязки. Мой скрипт поддерживает обе альтернативы, и вы можете увидеть здесь загруженную версию, в которой вместо перезаписи URL используются привязки.
Код довольно прост (хотя его можно еще упростить, используя выборку или менее подробную реализацию XHR).
HTML
Здесь нас беспокоят только две вещи:
- раздел
#content-box
(строка 21), куда мы будем помещать любой контент, который мы загружаем из наших API. - элементы
a
, используемые для внутренней маршрутизации, с которыми должен быть связан классlink
(строки 14-17).
JavaScript
Сначала мы инициализируем несколько глобальных переменных:
const useHash = true;
const apiUrl = 'https://lucasreta.com/stack-overflow/spa-vanilla-js/api';
const routes = ['section-1', 'section-2'];
const content_box = document.getElementById("content_box");
useHash
определит, следует ли использовать привязку (хэш) URL-адреса или последний параметр для нашей внутренней маршрутизации.
apiUrl
устанавливает базовый URL-адрес нашего простого API.
routes
определяет допустимые пути нашего приложения.
content_box
- это элемент DOM, который мы обновляем без данных.
Затем мы определяем наш асинхронный метод получения, который оставался довольно стандартным вызовом XHR, аналогичным тому, что запросчик имел в примере кода (обработка ошибок отсутствует):
function get(page) {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
data = JSON.parse(xhr.responseText);
content_box.innerHTML = data.content;
const title = `${data.title} | App Manual`;
document.title = title;
window.history.pushState(
{ 'content': data.content, 'title': title},
title,
useHash ?
`#${page}` :
page
);
}
};
xhr.open('GET', `${apiUrl}/${page}`, true);
xhr.send();
}
Здесь мы отправляем нашей get
функции параметр с именем page
, который соответствует конечной точке API, который мы будем использовать, и имени, которое мы будем использовать в нашем состоянии и URL-адресе, чтобы определить, что мы должны показать.
Наконец, мы должны обработать три события, в которых изменяется состояние нашего одностраничного приложения:
// add event listener to links const links = document.getElementsByClassName('link'); for(let i = 0; i < links.length; i++) { links[i].addEventListener('click', function(event) { event.preventDefault(); get(links[i].href.split('/').pop()); }, false); }
// add event listener to history changes window.addEventListener("popstate", function(e) { const state = e.state; document.title = state.title; content_box.innerHTML = state.content; });
// add ready event for initial load of our site (function(fn = function() { const page = useHash ? window.location.hash.split('#').pop() : window.location.href.split('/').pop(); get(routes.indexOf(page) >= 0 ? page : routes[0]); }) { if (document.readyState != 'loading'){ fn(); } else { document.addEventListener('DOMContentLoaded', fn); } })();
Сначала мы получаем все элементы с классом .link
и присоединяем к ним прослушиватель событий, чтобы при щелчке по ним событие по умолчанию останавливалось, а вместо этого вызывалась наша функция get
с последним параметром href.
Поэтому, когда мы щелкаем первую ссылку, указанную выше, мы выполним запрос GET на api.com/section-1
и обновим URL-адрес нашего приложения до app.com/section-1
или app.com/#section-1
.
Здесь лежат два ограничения моей реализации:
- Маршруты API и приложения должны совпадать.
- маршруты не могут иметь несколько параметров.
И то, и другое исправимо, я не буду вдаваться в подробности, поскольку это выходит за рамки простого POC, но я должен был указать на это. Первый можно исправить, используя какой-то словарь, который сопоставляет наши маршруты к конечным точкам, которые они должны получить. Вторую проблему можно исправить, немного усложнив логику в нашем приемнике событий для ссылок, расширив простой links[i].href.split('/').pop()
, чтобы включить все ожидаемые параметры.
Затем у нас есть прослушиватель событий для изменений в истории. Поскольку мы сохраняем содержимое, возвращаемое API, в самих состояниях истории, все, что нам нужно сделать при изменении истории, - это повторно заполнить content_box
нашим state.content
.
Наконец, у нас есть функция готовности, вызываемая, когда DOM изначально завершает загрузку, где мы проверяем наш URL, чтобы получить либо последний параметр, либо значение хэша / привязки, а затем проверяем, существует ли то, что мы получили от URL, в нашем массив определенных внутренних маршрутов. Если это так, мы вызываем нашу get
функцию с этим параметром. Если это не так, мы получаем первый маршрут из нашего массива и вместо этого вызываем его.
Это более простой способ?
Хотя я думаю, что любой может извлечь выгоду, зная, что это можно сделать и как, я не знаю, согласен ли я с OP, что отказ от сторонних библиотек - это «более простой способ» делать что-то. Даже у этого простого POC есть недостатки, и многие из них проявятся, если попытаться масштабировать его для более сложных сценариев.
Я считаю, что кажущаяся простота этого подхода - только результат еще более необоснованного представления о сложности текущих интерфейсных фреймворков. Я бы посоветовал каждому дать Vue или React шанс, прежде чем отказаться от них и выбрать ванильный путь JS.
Обо мне. Я веб-разработчик полного цикла с более чем 7-летним опытом, и в настоящее время я открыт для новых возможностей. Вы можете проверить мой веб-сайт для получения дополнительной информации или написать мне по адресу [email protected]