В этом блоге мы узнаем о создании одностраничных веб-приложений без использования таких фреймворков, как React, Angular и т. д.

История API

Мы будем использовать History API для реализации одностраничного приложения. History API — это API браузера, который управляет историей навигации. Этот API используется для отслеживания недавно посещенных страниц на вкладке. Например, Если пользователь открывает новую вкладку в браузере, переходит на сайт medium.com и нажимает кнопку «Назад» в браузере, он попадает на страницу «Новая вкладка». Это отслеживание посещенных страниц управляется с помощью History API.

Настройка кода

Создайте файл HTML с указанным ниже содержимым.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Single Page App</title>
</head>
<body>
    <div id="'root"></div>
    <script type="module" src="index.js"></script>
</body>
</html>

Мы будем заполнять элемент div содержимым страницы, которое будет отображаться как дочерний элемент.

JavaScript

Мы будем использовать подход класса ES6, чтобы добавить логику для отображения контента и навигации между страницами.

Создать страницу

export default class About {
  constructor(params) {
    this.params = params;
  }

  render() {
    return `
        <div>
            <p>This is About Page</p>
            <p>I am ${
              this.params?.name ? this.params?.name : ""
            } and I work as a ${this.params?.work ? this.params.work : ""}
        </div>
        `;
  }
}

Метод рендеринга возвращает дочерний элемент html для заполнения в «корневой» div. Мы можем передать любые параметры запроса или параметры пути этому компоненту, выбрав их в конструкторе.

Сопоставление URL-адреса страницы

Чтобы поддерживать связь между компонентом страницы и URL-адресом, мы используем объект, где ключом будет путь URL-адреса, а его значением будет компонент.

import About from "./Pages/About.js";
import Contact from "./Pages/Contact.js";
import Home from "./Pages/Home.js";

const Routes = {
    home: Home,
    about: About,
    contact: Contact,
}

export default Routes;

Навигация

Для выполнения основной логики навигации и рендеринга мы будем использовать отдельный класс.

Мы будем добавлять к URL-адресу страницы символ «#». Это делается для того, чтобы убедиться, что правильная страница отображается при перезагрузке страницы. Например, Если URL-адрес домашней страницы — «http://localhost:3000», а URL-адрес страницы с информацией — «http://localhost:3000/about». Когда мы переходим на страницу с информацией и пытаемся перезагрузить ее, сервер попытается найти путь «/ about» для отправки содержимого страницы. Это приведет к отсутствию страницы результатов, если html не будет возвращен для этого пути. Чтобы избежать этого, мы используем «#», чтобы он оставался в том же каталоге, что и хеш-путь.

Примечание. «#» не требуется, если вы возвращаете файл по всем путям.

Ниже приведена логика получения хэша всякий раз, когда пользователь переходит на другую страницу.

import Home from "./Pages/Home.js";
import Routes from "./routes.js";

export default class App {
  constructor() {
    this.currentPage = null;
  }

  navigate(path) {
    if (!path) {
      const Component = new Home({});
      this.updateContentAndUrl(Component, "", "");
    } else {
      path = path.startsWith("#") ? path.slice(1) : path;
      let paramsString = "";
      if (path.includes("?")) {
        paramsString = path.split("?")[1];
        path = path.split("?")[0];
      }

      let params = this.frameQueryParameters(paramsString);
      if (Routes[path]) {
        const Component = new Routes[path](params);
        this.updateContentAndUrl(Component, path, paramsString);
      }
    }
  }

  render() {
    this.navigate(location.hash);

    window.addEventListener("popstate", () => this.navigate(location.hash));

    document.body.addEventListener("click", (e) => {
      if (e.target.matches("[data-link]")) {
        e.preventDefault();
        this.navigate(e.target.getAttribute("data-link"));
      }
    });
  }
}

Мы инициализируем этот класс и вызываем метод рендеринга из файла index.js при загрузке приложения. Location.hash дает нам хеш-путь. Например, — URL-адрес http://localhost:3000/#home. Location.hash возвращает все, что присутствует после #, то есть #home.

Затем мы вызываем метод навигации, чтобы найти маршрут и отобразить содержимое. Мы можем использовать содержимое по умолчанию, если объект маршрута не имеет пути, связанного с компонентом. Мы также можем включить параметры запроса, как показано в приведенном выше фрагменте.

Визуализация

Как только мы находим компонент для рендеринга, нам нужно заполнить корневой div его дочерними элементами. Чтобы преобразовать строку HTML, возвращаемую компонентом страницы, мы используем createContextualFragment для создания фрагментов, которые можно добавить в качестве дочерних. Метод replaceChildren удаляет текущий дочерний контент и обновляет его фрагментом.

updateContentAndUrl(Component, path, paramsString) {
    const fragment = document
      .createRange()
      .createContextualFragment(Component.render());
    document.getElementById("root").replaceChildren(fragment);
    const url = this.frameUrlPath(path, paramsString);
    history.pushState({}, url, url);
}

frameUrlPath(path, params) {
    if(!path) {
        return '';
    }
    if (!params) {
      return "#" + path;
    } else {
      return "#" + path + "?" + params;
   }
}

FrameUrlPath формирует путь для обновления в браузере. Затем мы используем history.pushState для обновления объекта истории, чтобы отслеживать недавно посещенные страницы и обновлять URL-адрес браузера.

Ниже приведено одно из приложений, созданных с использованием этого подхода.

Демо: https://maheshudvag.github.io/Single-Page-Anime-App/
Репо: https://github.com/MaheshUdvag/Single-Page-Anime-App/tree/ead84fde1edc3970290e7a58d78dafcca260e476

Если вам это понравилось, подпишитесь на Рави Махеш Удваг, чтобы увидеть больше такого контента. Хорошего дня. Спасибо!

Контакт

Linkedin: https://in.linkedin.com/in/mahesh-udvag-a0a834129
Instagram: TechMadeec