Перевод статьи Create a Modern Vanilla Javascript Router. В ней рассмотрен вопрос как написать свой собственный роутер для простых вебприложений.

Одностраничные приложения на основе React, Vue или vanillaJS уже повсюду вокруг нас. И чтобы ваше одностраничное приложение было действительно хорошим вам нужен просто фантастический механизм маршрутизации.

Такие библиотеки, как navigo или react-router, действительно великолепны. Но как они работают? Нам действительно нужно импортировать всю библиотеку? Это для вас приемлемо, использовать 10% возможностей кода библиотеки?

И наконец, создание быстрого и удобного маршрутизатора очень просто и требует чуть менее 100 строк кода. Приступим?

Требования

Каким наш роутер должен быть:

  • написан с использованием стандарта es6+;
  • использовать историю браузера и хеш-маршрутизацию;
  • универсальным для дальнейшего использования.

Давайте начнем кодировать

Обычно в любом веб-приложении используется только один экземпляр маршрутизатора, однако в некоторых случаях нам может понадобиться более одного. Поэтому для его реализации мы не будем использовать шаблон Singleton. Для работы нашему роутеру нужно 4 основных параметра:

  • routes: список зарегистрированных маршрутов;
  • mode: что используем хэш или историю;
  • root: корень (начальная точка) приложения, если мы находимся в режиме истории;
  • constructor: базовая функция для создания нового экземпляра нашего маршрутизатора.
class Router {
  routes = [];
  mode = null;
  root = "/";

  constructor(options) {
    this.mode = window.history.pushState ? "history" : "hash";
    if (options.mode) this.mode = options.mode;
    if (options.root) this.root = options.root;
  }
}

export default Router;

Добавляем и удаляем маршруты

Добавление или удаление маршрута для нашего веб-приложения эквивалентно добавлению или удалению соответствующего элемента из массива routes.

class Router {
  routes = [];
  mode = null;
  root = "/";

  constructor(options) {
    this.mode = window.history.pushState ? "history" : "hash";
    if (options.mode) this.mode = options.mode;
    if (options.root) this.root = options.root;
  }

  add = (path, cb) => {
    this.routes.push({ path, cb });
    return this;
  };

  remove = path => {
    for (let i = 0; i < this.routes.length; i += 1) {
      if (this.routes[i].path === path) {
        this.routes.slice(i, 1);
        return this;
      }
    }
    return this;
  };

  flush = () => {
    this.routes = [];
    return this;
  };
}

export default Router;

Получаем текущий маршрут

Очевидно, что нам всегда необходимо знать, где в нашем приложении мы находимся.

Для этого мы должны предусмотреть обработку обоих случаев: режим истории history mode и режим хеш-навигации hash mode . В первом случае мы должны удалить у строки, взятой из значения window.location корневой путь, во втором — #. Также нам нужна функция clearSlash(), которая будет удалять символ / из начала или конца строки, чтобы избавить других разработчиков, которые будут далее использовать наш роутер, о виде представления маршрута на этапе его написания.

[...]

  clearSlashes = path =>
    path
      .toString()
      .replace(/\/$/, "")
      .replace(/^\//, "");

  getFragment = () => {
    let fragment = "";

    if (this.mode === "history") {
      fragment = this.clearSlashes(decodeURI(window.location.pathname + window.location.search));
      fragment = fragment.replace(/\?(.*)$/, "");
      fragment = this.root !== "/" ? fragment.replace(this.root, "") : fragment;
    } else {
      const match = window.location.href.match(/#(.*)$/);
      fragment = match ? match[1] : "";
    }
    return this.clearSlashes(fragment);
  };

}

export default Router;

Переход на страницу

Отлично, теперь у нас есть API для добавления и удаления URL (маршрутов). Также можем получить свое текущее положение (маршрут). Следующим шагом будет переход по маршруту (на другую страницу). Опять же, мы будем действовать по разному в зависимости от вида свойства mode.

[...]

  getFragment = () => {
    let fragment = "";

    if (this.mode === "history") {
      fragment = this.clearSlashes(decodeURI(window.location.pathname + window.location.search));
      fragment = fragment.replace(/\?(.*)$/, "");
      fragment = this.root !== "/" ? fragment.replace(this.root, "") : fragment;
    } else {
      const match = window.location.href.match(/#(.*)$/);
      fragment = match ? match[1] : "";
    }
    return this.clearSlashes(fragment);
  };

  navigate = (path = "") => {
    if (this.mode === "history") {
      window.history.pushState(null, null, this.root + this.clearSlashes(path));
    } else {
      window.location.href = `${window.location.href.replace(/#(.*)$/, "")}#${path}`;
    }
    return this;
  };

}

export default Router;

Прослушиваем изменения

Теперь нам нужен функционал, который будет уведомлять нас об изменениях в адресной строке. Каждый тип изменений, например ввод ссылки ссылка или переход с использованием нашего метода .navigate(). Мы также должны быть уверены, что правильно отобразим нужную страницу при самом первом посещении (первичной загрузке страницы).

Событие popstate вызывается, когда изменяется активная запись истории браузера. Если изменение записи истории было вызвано методом history.pushState() или history.replaceState(), то состояние события popstate будет содержать stateкопии входящего в историю объекта. Обратите внимание, history.pushState() или history.replaceState() не вызывают событие popstate. Событие popstate будет вызвано при совершении действий в браузере, таких как нажатие кнопок «Вперед» или «Назад» (или при вызове history.back() или history.forward() из JavaScript).

Мы могли бы прослушивать событие popstate, чтобы регистрировать каждое изменение в браузере, но так как интересует только логика, мы просто используем метод setInterval().

class Router {
  routes = [];
  mode = null;
  root = "/";

  constructor(options) {
    this.mode = window.history.pushState ? "history" : "hash";
    if (options.mode) this.mode = options.mode;
    if (options.root) this.root = options.root;

    this.listen();
  }
  
  [...]

  listen = () => {
    clearInterval(this.interval);
    this.interval = setInterval(this.interval, 50);
  };

  interval = () => {
    if (this.current === this.getFragment()) return;
    this.current = this.getFragment();

    this.routes.some(route => {
      const match = this.current.match(route.path);

      if (match) {
        match.shift();
        route.cb.apply({}, match);
        return match;
      }
      return false;
    });
  };
}

export default Router;

Вывод

Наша роутер, он же маршрутизатора, готов к использованию! Только 84 строки кода полностью обеспечивают работу нашего роутера. Очевидно, что библиотеки, которые мы находим на github содержат больше функционала. Тем не менее сколько раз вам случалось разрабатывать сайты, которым нужно отрендерить всего лишь пару маршрутов.

Я создал репозиторий Github с кодом и примером его использования. Вы можете найти его перейдя по ссылке: thecreazy/create-a-modern-javascript-router.

Оставить комментарий