JS, простая библиотека и роутер

Всем привет.

Пишу эту статью по той причине, что сам не нашел достаточно материала на эти темы.

Примерно месяц назад я решил написать свою первую библиотеку. Вот требования к ней:

  1. Выбор элементов как JQuery — по селектору, через функцию вида "$(selector)".
  2. Вызов функций для выбранного элемента через методы библиотеки.
  3. Очень маленький вес (хоть и по причине малого функционала).


Когда работа была завершена, я решил добавить к библиотеке простой роутер, который будет реализован через «window.location.hash». И вот требования к нему:

  1. Рендер контента без перезагрузки страницы.
  2. Автоматизированная работа роутера, а именно рендер контента, соответствующего какому-либо роуту, без добавления дополнительных условий такого типа — "if (window.location.hash == 'home') document.body.innerHTML = homePageContent".
  3. Наличие страницы «404».


Да, да, я знаю, что это «велосипед», что такая библиотека сейчас совсем не нужна, а реализация роутера через «window.location.hash» уже устарела, но это все было написано не для продакшена!
Главной целью всего этого было получение опыта, так как я только новичок и очень многое еще просто не понимаю. Пока я писал эти два маленьких проекта, я научился работать с циклами, узнал про свойства функций, я узнал и понял довольно много нового, стал смотреть на язык иначе.

Ну, хватит тянуть, переходим к реализации


Начнем c библиотеки


Основана она будет на свойствах функций в JavaScript. Но также прошу заметить, что ее можно было реализовать и через классы в спецификации «ES6». Напишем вот такую заготовку:

"use strict";

function Library(element) {
  // Вот первое свойство нашей функции, через параметр мы получаем выбранный элемент
  this.element = element;
}

Теперь можем добавить первый полноценный метод, который по сути является укороченным «addEventListener». В примере я опишу лишь два метода, чтобы было ясно, как они в общем устроены, и как самостоятельно их создать.

// Создаем метод "on"
this.on = function(name, f) {
  // Перебираем все элементы, которые позже получим через дополнительную функцию
  for (var i = 0; i < this.element.length; i++) {
    // В теле цикла находится сама функция, которую вызывает метод
    // Используем параметры
    this.element[i].addEventListener(name, f);
  }
  // Возвращаем главную функцию
  return this;
}

Можем по такому же принципу добавить в нашу библиотеку еще один метод — «render».

// Опять создаем метод
this.render = function(content) {
  // Снова перебираем элементы
  for (var i = 0; i < element.length; i++) {
    // В теле цикла снова находится наша функция
    this.element[i].innerHTML = content;
  }
  // Не забываем про эту строчку!
  return this;
}

Теперь напишем функцию, через которую будем получать сами элементы.

function $(selector) {
  // Переменная "elements" будет содержать один или больше полученных элементов
  var elements = document.querySelectorAll(selector);
  // Возвращаем главную функцию с полученными элементами
  return new Library(elements);
}

Все вместе должно выглядеть примерно так:

"use strict";

function Library(element) {

  this.element = element;

  this.on = function(name, f) {
    for (var i = 0; i < element.length; i++) {
      this.element[i].addEventListener(name, f);
    }
    return this;
  }

  this.render = function(content) {
    for (var i = 0; i < element.length; i++) {
      this.element[i].innerHTML = content;
    }
    return this;
  }

}

function $(selector) {
  var elements = document.querySelectorAll(selector);
  return new Library(elements);
}

Пожалуй, мы закончили с прототипом нашей маленькой библиотеки. Опробуем ее!

// Пробуем рендер
$("body").render(`<div id="title">
                 Hello, this words was printed by a render function!</div>
                 <button id="btn">Alert</button>`);
// И листенер тоже
$("#btn").on("click", function() { alert("Button was clicked") });
// Да, мой английский - так себе...
// Ну а на странице должна появиться эта надпись
// А при нажатии всплыть уведомление

Вроде все работает.

Займемся роутером


Создадим объект-константу с именем «router», содержащую один метод-функцию — «getURL()».
Еще добавим пустой массив-переменную с именем «routes».

// Создаем объект
const router = {
  // Теперь его метод
  getURL() {
    // Возвращаем наше текущее местоположение по "hash"
    return window.location.hash.slice(1);
    // Пропускаем первый символ, так как он всегда будет "#"
  }
}

var routes = [];

Теперь опишем функцию «renderRoute()», а в ней вспомогательную функцию — «isUndefined()», которая будет проверять наш роут на равенство с «undefined».

// Вспомогательная функция
function renderRoute() {

  function isUndefined(z) {
    return typeof z === "undefined"; // Только строгое сравнение
    // Если undefined, то функция вернет "true"
  }

  let url = router.getURL(); // По сути let url = window.location.hash.slice(1)

  let route = routes.find(r => r.path === url);
  // Перебираем массив "routes" и ищем роут с "path", равным переменной "url"

  if (isUndefined(route)) {
    // И снова перебор
    route = routes.find(r => r.path === "***");
    // Если роута с таким "path" нет, то адрес сменится на "***", это страница 404
  }

  // Создаем переменную "routerView" с нужным нам для рендера элементом
  var routerView = document.querySelector("#router-view");

  // Проверяем наличие нужного нам элемента в файле html
  if (!view) {
    console.log("Не удалось найти view-элемент!") // Если элемента нет
  } else {
    routerView.innerHTML = route.content; // Если элемент есть
  }

}

Далее реализуем функцию, которая будет отслеживать изменения «window.location.hash» и рендерить соответствующий контент.

function initRoutes() {
  // "Вешаем прослушку" на "window"
  window.addEventListener("hashchange", renderRoute);
  // Да, все правильно, без скобок, я сам долго мучился с этой ошибкой
  renderRoute();
  // Вызвали рендер для того, чтобы контент грузился сразу после загрузки страницы
}

Ну все, с роутером мы тоже закончили, осталось его протестировать. Вот базовый способ его запуска:

// Создаем наши страницы
var firstPage = `This is a first page`;
var secondPage = `This is a first page`;
var page404 = `This page not found, <a href='#'>First page</a>`;

// Заполняем массив "routes" объектами, содержащими "path" и "content"
var routes = [
  { path: "", content: firstPage }, // Пустой путь означает, что страница главная
  { path: "second_page", content: secondPage }, // Путь указываем без "#"
  { path: "page_404", component: page404 }
];

// Запускаем наш роутер
initRoutes();

Действительной для этого роутера будет ссылка с атрибутом «href» такого вида — «href='#page'».

Ну вот и все, мы закончили, у нас получилось, мы молодцы!

Итог


Теперь с этими инструментами можно реализовать простенькое (а может и не простенькое) прогрессивное одностраничное приложение — PSPWA (ага, сам придумал, совместил SPA и PWA).

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

Вот и закончилась моя первая статья. Дорогой Читатель, хочу сказать тебе спасибо за внимание! Я очень старался и надеюсь, что тебе поможет это небольшое!

Если нашли ошибку, пишите в комментарии, буду очень благодарен. Если будут какие-то вопросы — постараюсь ответить. Всем спасибо и пока!

Источники


SPA
PWA
Про «use strict»
Учебник — переменные
Учебник — функции
Учебник — циклы
Учебник — массивы
Учебник — объекты
Статься об ООП в JS от другого пользователя habr'а
Учебное видео по роутингу во фреймворке от WebForMySelf
Теги:
javascript library, javascript, js, router, single page application, spa, pwa

Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.