Хорошо настроенный роутинг фронтенда

0
547
views

Перевод статьи Хайме Ямасаки Вукелика «Client-side routing done right».

Роутинг фронтенда
Photo by Jack Anstey on Unsplash

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

Шаблон, представленный в этой статье, я сформулировал несколько недель назад. Хочу поделиться им с вами, потому что он существенно улучшил наше приложение.

Что такое «роутинг»?

Если вы не знаете, что такое роутинг фронтенда, вам повезло. Не стоит углубляться в эту тему. Но поскольку вы все равно рано или поздно с этим столкнетесь, я все же поясню.

Концепция роутинга существовала в бэкенд-приложениях задолго до того, как проникла во фронтенд. Там она прекрасно справлялась со своими задачами. Если вы когда-либо писали бэкенд-приложения, вы знаете, насколько необходим роутинг.

Словарное определение роутинга это «направление по определенному пути». В этом вся суть. Вы сопоставляете URL-шаблоны с какими-то функциями вашего кода (или с компонентами, классами и т. д.). А когда пользователь обращается к вашему приложению по этому URL-шаблону, вы направляете его к нужному участку вашего кода, который будет обрабатывать запрос. Конечно, было бы классно, если бы все было так просто, но это не так. Во фронтенде вещи не столь прямолинейны, как в бэкенде.

В этой статье я буду использовать некоторые предопределенные шаблоны (а также регулярные выражения), которые выглядят как URL-адреса.

Если вы видите что-то вроде:

router.route('/book/:id', BookComponent)

– бегите!

Теперь, когда мы побегали, давайте рассмотрим, как решаются проблемы роутинга.

Как насчет сохраненных URL-ссылок?

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

Возможность делать закладки на определенном состоянии приложения (или его частях) не зависит от того, используете ли вы для роутинга фронтенд-библиотеки. Фактически, это не обязательно. Когда ваше приложение загружается, вам просто нужно посмотреть window.location и понять, каким должно быть начальное состояние (дальше об этом будет подробнее).

Что, если пользователь нажмет клавишу «назад»?

Да, пользователи – они такие. Они обязательно нажмут клавишу «назад». Хотел бы я, чтоб было иначе, но они все равно это сделают. И когда пользователь нажимает эту кнопку, он ожидает, что приложение поведет себя правильным образом.

Это хорошо, но разве библиотеки роутинга не решают эту задачу? Конечно, решают. (А если нет, то их вряд ли можно называть фронтенд-библиотеками роутинга). Но давайте вернемся назад (каламбур) и подумаем над тем, что же они делают на самом деле.

Все, чем они занимаются, это управляют событием popstate. Это довольно простое событие: оно просто дает вам знать, что URL в адресной строке изменился. Затем вы можете обратиться к window.location и получить всю необходимую информацию.

В нашем случае все немного отличается, потому что состояние не инициализируется, а изменяется. Но это касается любого события в вашем приложении. Так почему же с popstate что-то должно быть по-другому, верно?

А теперь о хорошем

Я все время говорю «обратиться к window.location». Если вы считаете, что URL-адреса – это просто сериализованное состояние приложения (ну, не все), вы можете существенно изменить свое понятие об URL-адресах, и осознать, что да, на самом деле роутинг вам не нужен.

Некоторые части состояния вашего приложения важны по двум указанным выше причинам (вероятно, и по другим причинам тоже, просто сейчас не могу о них вспомнить). Какие «некоторые части» при этом имеются в виду, зависит от вашего приложения, поэтому я просто приведу пример с приложением, над которым сейчас работаю.

В нашем случае приложение разделено на модули (нет, я не собираюсь сейчас убеждать вас в необходимости модульности кода, у нас модуль – просто часть бизнес-концепции). Эти модули делятся на секции. Чтобы инициализировать любую комбинацию модуль-секция в нашем приложении, нам нужно знать, о каком модуле и секции идет речь, а также нужную для этого модуля информацию. Например, модуль компании должен знать ID компании.

Для полного описания любого представления в нашем приложении у нас есть объект, который выглядит вот так:

{
  module: 'company',
  moduleArgs: ['some-id-123'],
  section: 'profile',
  sectionArgs: {},
}

Это просто JavaScript-объект. В отличие от URLs, его легко создать и легко им манипулировать с помощью инструментов, которые у вас уже есть и с которыми вы уже знакомы. Так же, как и с любыми данными в вашем приложении.

Теперь мы преобразуем это в URL, придерживаясь следующих правил:

  1. первый сегмент пути это модуль;
  2. остальные сегменты – аргументы модуля (или параметры);
  3. параметр строки запроса view это имя секции;
  4. любые другие параметры строки запроса это аргументы секции.

Объект, указанный в примере, преобразуется в такой URL:

/company/some-id-123?view=profile

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

У нас есть модуль, заботящийся о конверсии между URL и объектом. Это довольно просто, поэтому я не буду утомлять вас подробностями. Давайте рассмотрим некоторые примеры этого:

// We are on '/company/some-id-123?view=profile'
> loc.toLocation(window.location)
{module: 'Company', moduleArgs: ['some-id-123'],
 section: 'profile', sectionArgs: {}}
> loc.toURL({ module: 'lists', moduleArgs: ['id-456'], 
...           section: undefined, sectionArgs: {} })
'/lists/id-456'
> loc.toURL(loc.toLocation({
... pathname: '/lists/id-456', 
... search: ''
... }))
'/lists/id-456'

Эти две функции реверсивны: если вы направите значение через обе, в конце получите начальное значение. (Что-то вроде. ToLocation() будет использовать объект window.location, так что мы получим строку, представляющую URL этого объекта местоположения). Это очень важно для надежности, и у нас есть отдельные тесты для проверки этого поведения.

Но это еще не все. Чтобы поддерживать синхронность URL и состояния приложения, нам нужно инициализировать состояние приложения, реагировать на изменения в URL, а также реагировать на изменения в состоянии.

Когда приложение инициализируется, мы конвертируем window.location в данные, специфичные для приложения, и устанавливаем начальное состояние. В Vue это может выглядеть так:

@Component
class ModuleSelector extends Vue {
  data() {
    return {
      location: loc.toLocation(window.location)
    }
  }
  
  // ...
}

Теперь свойство location является реактивным состоянием, содержащим данные, инициализированные из текущего местонахождения.

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

sevrices.goToLocation({ section: 'anotherSection' })

Примечание: есть одна хитрость в том, как сервис goToLocation() управляет partial location objects. Благодаря ей нам не нужно все переписывать при изменении чего-то одного.

Этот сервис преобразует объект локации в URL и использует window.history.pushState() для обновления адресной строки. Он также передает событие locationChange, специфичное для данного приложения, что позволяет приложению обновить свое состояние. URL в адресной строке и состояние приложения всегда синхронизируются.

Когда пользователь в ходе сессии кликает на кнопку «Назад», мы выдаем событие locationChange, и состояние опять синхронизируется приблизительно таким же образом.

Компонент Vue, который вы видели раньше, имеет такой инструмент:

@Component
class ModuleSelector extends Vue {
  
  // ...
  @Listen('locationChange')
  setLocation(newLocation) {
    this.location = newLocation
  }
}

И последний отрывок касается рендеринга соответствующего модуля:

@Component
class ModuleSelector extends Vue {
   render() {
     // some JSX magic here
     const ModuleComponent = VIEWS[this.location.module]
     return <ModuleComponent section={this.location.section} ... />
   }
   // ...
}

Объект VIEWS это лишь сопоставление имен и компонентов, отображающих представление этого модуля. Например:

const VIEWS = {
  company: Company,
  lists: Lists,
}

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

Погодите, но это может делать роутер!

При более традиционном роутинге технически вы могли бы сделать кое-что из этого. Например, если роутер поддерживает чего-либо после первоначального совпадения (например, /:module/* ). Частично это поможет адресовать URLs к домену приложения. Но как же с обратными ситуациями? Большинство роутеров такого не делают, и чтобы направить браузер по другому адресу, просят вас использовать URL. Адреса заботят нас в той же степени, что и JSON payloads. На самом деле мы заботимся о данных.

Так что нет, роутеры обычно не могут делать то, о чем шла речь выше. Потому что они имеют дело с URL-шаблонами, а не с данными. Они не рассматривают URLs в качестве сосудов для ваших данных. Вместо этого они перетаскивают URLs в ваше приложение.

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

Покажите мне код!

Дело в том, что вам не нужен наш код. Наш код специфичен для нашего приложения. Вам стоит обратиться к MDN, где вы найдете все, что вам нужно знать.

Лишь от вас зависит, что использовать для передачи изменений локации: события, Redux store, Vuex или что-то еще. То, как вы напишете код состояния и его частей, также зависит только от вас. Это часть вашего приложения, так что вы вольны делать все, что хотите, в этом и вся прелесть. А также, как вы могли заметить (надеюсь), это достаточно просто.

Давайте быстро повторим необходимые шаги. Вам нужно:

  • определить, какое состояние приложения содержится в URL;
  • определить формат сериализации и десериализации состояния;
  • инициализировать состояние при запуске приложения;
  • сохранять синхронизацию состояния и URL при помощи вызовов событий pushState() и popstate.

Любой код помимо APIs браузера принадлежит к вашему приложению.

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



ОСТАВЬТЕ ОТВЕТ

Please enter your comment!
Please enter your name here