Недавно я столкнулся со следующим вопросом о переполнении стека, касающимся обработки состояния при обновлении в одностраничном приложении (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]