Pull to refresh

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
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.