🚚 Управление типизированным маршрутом в приложении Angular

Проблема

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

Это конфигурация маршрутизации нашего приложения, и она используется, когда мы импортируем RouterModule в наш модуль:

Затем мы можем перейти к этим маршрутам, используя службу Router в нашем компоненте:

Или мы можем использовать директиву routerLink в нашем шаблоне:

Хотя это структура, которую Angular рекомендует в своей документации для настройки ваших маршрутов, ее основным недостатком является интенсивное использование жестко запрограммированных строк. Что произойдет, если (или, что более вероятно, когда) я напишу путь маршрута с ошибкой или решу изменить путь маршрута? Поскольку мы используем жестко заданные строки, Typescript не может сообщить нам, что значение, которое мы предоставили директиве routerLink или функции navigateByUrl, является недопустимым маршрутом. Конечно, мы можем использовать найти все и заменить в нашем редакторе кода, когда меняем маршруты, но полагаться на то, что другие или на себя в будущем не забудут это сделать, — не лучшая идея.

Первоначальное решение

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

Затем мы удалим жестко закодированные строки внутри нашего объекта маршрута и заменим их ссылками на пути маршрута в нашем классе AppRouteNames.

После этого мы можем импортировать класс AppRouteNames в наш компонент и использовать путь маршрута в качестве значения для функции navigateByUrl.

Отлично! В нашей кодовой базе больше нет дублирующихся жестко запрограммированных строк. Однако у этого решения есть несколько проблем. Если мы хотим использовать свойства пути статического маршрута внутри HTML наших компонентов, нам нужно добавить класс AppRouteNames в качестве защищенного свойства внутри нашего компонента:

Затем мы можем использовать класс AppRouteNames внутри нашего шаблона:

Это работает, но добавление класса AppRouteNames в качестве свойства к каждому компоненту, где мы хотим использовать пути маршрута в шаблоне, станет очень утомительным.

Предоставление класса AppRouteNames

Мы можем избавиться от утомительной работы по созданию свойства класса AppRouteNames, добавив класс AppRouteNames в качестве поставщика. Во-первых, мы удалим ключевое слово static из всех свойств класса AppRouteNames:

Далее мы добавим нового провайдера в массив провайдеров внутри нашего AppModule для класса AppRouteNames:

Теперь, когда мы удалили ключевое слово static из всех свойств пути маршрута в классе AppRouteNames, нам также нужно изменить способ предоставления маршрутов для класса RouterModule. Вместо того, чтобы передавать постоянную переменную Routes, мы передадим пустой массив:

Затем мы изменим постоянную переменную Routes на постоянную функцию с именем buildRoutes, которая принимает наш класс AppRouteNames и использует этот параметр для доступа к путям маршрута в нашей конфигурации маршрута:

Чтобы предоставить эту конфигурацию маршрута для RouterModule, мы добавим еще одного провайдера ниже поставщика для AppRouteNames в нашем AppModule. Мы будем использовать токен внедрения ROUTES, импортированный из @angular/router, как вещь, которую мы предоставляем, использовать фабрику и нашу функцию buildRoutes, чтобы вернуть конфигурацию маршрута, установить multi в true и обязательно указать класс AppRouteNames в качестве зависимости:

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

А также то, как это теперь используется в шаблоне:

Больше не нужно добавлять класс AppRouteNames в качестве свойства компонента. Довольно гладко!

Но подождите... Есть еще (проблемы)!

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

Вместо того, чтобы перейти к нашему FirstChildComponent, мы вместо этого перенаправляемся на наш резервный маршрут, что указывает на то, что мы передали нашему маршрутизатору недопустимый путь маршрута. Если мы вернемся к нашему классу AppRouteNames и посмотрим на свойство FIRST_CHILD_COMPONENT, мы увидим, что значение равно 'first-child-component'. Хотя эта настройка могла работать для любого из наших маршрутов на первом уровне нашей конфигурации маршрута, всякий раз, когда нам нужно перейти к маршруту, вложенному в другой маршрут, нам нужно включить как путь родительского(ых) маршрута, так и путь маршрута для компонент, к которому мы пытаемся перейти. Мы можем временно решить эту проблему, соединив пути THIRD_COMPONENT_WITH_CHILDREN и FIRST_CHILD_COMPONENT route косой чертой:

Теперь эта ссылка успешно ведет на страницу FirstChildComponent:

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

Улучшение нашего класса AppRouteNames

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

Эти пути маршрута с префиксом RELATIVE_ — это пути маршрута, которые мы будем использовать в нашей конфигурации маршрута, которую мы передаем в RouterModule внутри нашего AppModule. Нам нужно будет изменить нашу конфигурацию маршрута, чтобы использовать эти новые относительные пути маршрута.

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

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

Затем мы можем вернуться к тому месту, где мы изначально просто использовали путь маршрута FIRST_CHILD_COMPONENT, и изменить его назад только на FIRST_CHILD_COMPONENT.

Теперь это приводит нас к нашему вложенному компоненту правильно, без конкатенации строк внутри нашего шаблона!

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

Обработка параметров маршрута

До сих пор мы не использовали никаких маршрутов, использующих преимущества параметров маршрута Angular. Мы можем начать с добавления относительного маршрута к нашему классу AppRouteNames.

Затем мы можем добавить маршрут в нашу конфигурацию маршрута в функции buildRoutes.

В этом случае мы не сможем использовать другое свойство класса для полного пути маршрута, потому что нам нужно иметь возможность динамически генерировать часть :id. Вместо этого мы будем использовать функцию, которая принимает id в виде строки. Мы по-прежнему можем использовать вспомогательную функцию buildURL для построения URL-адреса, но из-за раздела :id пути относительного маршрута мы не сможем использовать его внутри функции, и вместо этого нам придется дублировать часть 'fourth-component' URL-адреса. путь маршрута.

Теперь мы можем использовать этот маршрут в нашем шаблоне:

Направляем нас к этому компоненту с идентификатором 1:

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

ngx-продвинутый маршрутизатор

Именно этот вопрос привел меня к созданию ngx-advanced-router. Он работает как замена стандартного Angular RouterModule и позволяет указать конфигурацию маршрутизации в качестве внедряемой службы и быстро и легко получить доступ к путям маршрутов. Мы можем начать с установки этого пакета в наш проект Angular:

npm install --save ngx-advanced-router

Далее нам нужно создать новый сервис, расширяющий AdvancedRouteService. Расширение этого класса требует, чтобы мы реализовали одно абстрактное свойство: routesConfig. В это свойство мы поместим аналогичную конфигурацию маршрута, которую мы вернули ранее в нашей функции buildRoutes. Однако вместо массива объектов маршрута у нас будет объект с ключами и объектами маршрута в качестве значений. Вот как это будет выглядеть, если мы преобразуем конфигурацию маршрута из нашей функции buildRoutes:

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

Чтобы использовать эту службу, мы вернемся к нашему AppModule и удалим провайдеров AppRouteNames и ROUTES из нашего массива провайдеров, а также RouterModule из нашего массива импорта. Вместо этого мы добавим AdvancedRouterModule с его функцией forRoot в массив импорта, передав нашу недавно созданную службу маршрутизации:

В нашем AppComponent, куда мы вводили класс AppRouteNames, мы вместо этого импортируем класс AppRouteService. Чтобы использовать эту службу для доступа к нашим путям маршрутов, мы будем использовать свойство службы routes. Доступ к этому свойству дает нам хороший набор параметров автозаполнения на основе конфигурации маршрута, которую мы установили в службе ранее.

Если мы хотим перейти к «первому» маршруту, мы можем использовать свойство path, которое возвращает полный путь маршрута.

Для маршрутов с дочерними элементами у них будет свойство children в дополнение к свойству path, что также дает нам хороший список автозаполнения.

Вот как выглядит полный код для доступа к этому дочернему маршруту:

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

Вот и все! Это решение решает все проблемы, которые у нас были:

  1. Нет повторяющихся строк
    Теперь строки используются один раз в конфигурации маршрута, а свойство routes используется для доступа к путям маршрута для всего остального.
  2. Нет дополнительных свойств компонента
    Наша служба маршрутизации использует внедрение зависимостей Angular, поэтому нет необходимости добавлять свойство к нашему компоненту только для доступа к нашим путям маршрута.
  3. Никакого импровизированного построения путей маршрута
    Служба маршрутизации позаботится о создании наших путей маршрута для нас, независимо от того, находятся ли они на корневом уровне или на 5 уровнях в глубину.
  4. Отсутствуют относительные пути маршрутов
    ngx-advanced-router обеспечивает маршруты к RouterModule Angular в том формате, который он ищет, и предоставляет вам абсолютные пути маршрутов.
  5. Нет дополнительного класса пути маршрута
    Однажды объявите настройку маршрута в службе маршрутизации, используйте ее в функции AdvancedRouterModule.forRoot(), и все готово!

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