ASP.NET MVC client-side routing

    Постановка проблемы


    Система маршрутизации ASP.NET MVC избавляет разработчика от необходимости вручную поддерживать URL, используемые в приложении при помощи таблиц маршрутизации и url шаблонов. Таким образом, с программиста снимается задача по формированию URL вручную. Напротив, в его распоряжении оказывается богатый набор URL-хелперов. Это замечательно! Но все меняется, когда приходят они — AJAX-запросы.

    Проблема заключается в том, что таблицы маршрутизации и механизм генерации URL являются частью серверной архитектуры asp.net mvc и недоступны из Javascript-файлов.
    Как следствие, в скриптах часто можно встретить такой код:

        var url="/items/get-items?typeid="+typeId+"&groupid="+groupId+"&skip="+skip+"&take="+take;
    

    Помимо того, что этот код выглядит не очень красиво, его появление хотя бы в одном месте в ваших скриптах нарушает изолированность информации о конечном виде URL. Теперь, в случае, если вам потребуется изменить структуру URL и, например, перенести параметры из query string в тело URL, вам недостаточно будет просто отредактировать шаблон. Вы должны будете пройтись по скриптам и внести необходимые изменения. Это может стать причиной ошибок: где-то поменяли, где-то забыли, где-то опечатались.

    Предварительная серверная генерация URL


    Клиентские скрипты должны располагаться в .js файлах, поэтому мы сразу отбрасываем вариант с генерацией скриптов при формировании View. Одним из решений в данной ситуации является предварительная генерация URL на сервере и запись его в в data-* атрибут элемента, например так:
    <div id="some_container"  data-url="@Url.Action("Action","Controller",new {Id=10})">
    ...
    </div>
    

    Здесь мы по-прежнему полагаемся на механизм роутинга ASP.NET MVC и избавляемся от ручного формирования URL на клиенте, получая его из атрибута. В определенных случаях этого решения будет достаточно, но полностью проблему это не решает. Рассмотрим ситуацию. Вы пишете javascript-модуль, который планируется использовать во многих частях приложения, при этом ему необходимо будет осуществлять ajax-запросы. В этом случае не хотелось бы выносить информацию об URL для запросов куда-то за пределы этого модуля. Поэтому вариант с хранением URL в атрибутах и передача его внутрь модуля нам не подходит. Но и вручную формировать его мы не хотим. Что же делать?

    Библиотека RouteJs


    В идеале хотелось бы иметь доступ к URL routes напрямую из JavaScript, но встроенного механизма в ASP.NET MVC для этого нет. Однако его можно добавить, подключив небольшую библиотеку RouteJs. Она работает как с WebForms, так и с ASP.NET MVC (начиная с 2-ой версии) и не требует использования сторонних JavaScript фреймворков. Добавить ее к проекту можно через Nuget. В этом случае вас останется только включить в страницу ссылку на скрипт:

    <script src="@RouteJs.RouteJsHandler.HandlerUrl"></script>
    

    В результате вы получите доступ к вашим роутам прямо из JavaScript при помощи объекта Router.
    Примеры

    Дефолтный маршрут

    routes.MapRoute(
                    name: "Default",
                    url: "{controller}/{action}/{id}",
                    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
                );
    

    Что получаем:
    >Router.action('Home','Index')
    "/Home/Index"
    >Router.action('Home','Index',{id:1})
    "/Home/Index/1"
    >Router.action('Home','Index',{type:1, group:2})
    "/Home/Index?type=1&group=2"
    

    А если добавить Area?

    public override void RegisterArea(AreaRegistrationContext context)
            {
                context.MapRoute(
                    "TestArea_default",
                    "TestArea/{controller}/{action}/{id}",
                    new { action = "Index", id = UrlParameter.Optional }
                );
    }
    

    Проверяем:
    >Router.action('Home','Index',{area:'TestArea'})
    "/TestArea/Home/Index"
    

    Constraints

    RouteJs умеет работать с дефолтными regexp ограничениями:
    routes.MapRoute(
               name: "DefaultWithConstraint",
               url: "constr/{controller}/{action}/{id}",
               defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
               constraints:new {action="^ConstrAction$"}
    

    Проверка:
    >Router.action('Home','Index')
    "/Home/Index"
    >Router.action('Home','ConstrAction')
    "/constr/Home/ConstrAction"
    

    Однако по понятным причинам он не сможет обработать ваши кастомные IRouteConstaints. Более того, в текущей версии Router перестает работать в случае, если в каком-то из маршрутов используются кастомные Constraints. (Происходит ошибка в JavaScript при обращении к объекту. Чтобы обойти это, следует навесить атрибут [HideRoutesInJavaScript]на проблемный контролер. Я думаю в ближайшем будущем этот баг будет исправлен).

    Кэширование

    Сгенерированный скрипт отдается вместе с заголовком Expires, который сообщает браузеру о том, что скрипт нужно закешировать на 1 год. При этом обратите внимание на формат ссылки, которая добавляется на страницу:
      <script src="/routejs.axd/2512dbb48b9018a4c8f9eac5fb92f903800d94da/router.js"></script>
    

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

    Библиотека JsAction


    Библиотека JsAction предлагает другой подход к решению проблемы client-side маршрутизации. Установить ее также можно через Nuget. После этого требуется добавить в Layout ссылку (подключать следует после jQuery):
    @Html.JsActionScript()
    

    Далее Action, который планируется использовать для Ajax-запросов помечается специальным атрибутом [JsAction]:
    [JsAction]
    [HttpGet]
    public JsonResult Do(int a, int b)
    {
        return Json(a + b, JsonRequestBehavior.AllowGet);
    }
    

    После этого вы можете вызвать этот Action из JavaScript по имени:
    $(function() {
        JsActions.Home.Do(1, 3);
    })
    

    Таким образом, эта библиотека делает нечто больше, чем просто перенос URL маршрутов на клиент. Она избавляет разработчика от ручного написания Ajax-запросов. При этом есть ряд приятных побочных эффектов. Представьте, что метод, который был доступен по GET запросу нужно заменить на POST. Ваши действия? Вы поменяете атрибут [HttpGet] на [HttpPost] и потом пойдете искать вызов этого метода в скрипте, чтобы поменять настройки вызова $.Ajax (если вы используете jQuery, конечно). Эта библиотека избавляет вас от этой необходимости. Отныне эти настройки будут находится только в атрибутах метода. Обработка результатов ajax-запроса происходит так же как и при обычном вызове $.ajax():
    $(document).ready(function () {
         JsActions.Home.Do(1,2).then(function(data){ alert(data); });
    });
    

    В случае, если вы используете jQuery до версии 1.5, вам доступен аналогичный механизм:
    $(document).ready(function () {
        var ret = JsActions.Home.Do(1, 2, {
            success: function (data) {
                alert(data);
            }
        });
    });
    


    Intellisense

    JsAction поддерживает механизм Intellisense в Visual Studio. Работает это следующим образом. Вы добавляете xml-комментарии к вашему методу:
            /// <summary>
            /// Sums two numbers
            /// </summary>
            /// <param name="a">First number</param>
            /// <param name="b">Second number</param>
            /// <returns></returns>
            [JsAction()]
            [HttpGet]
            public JsonResult Do(int a, int b)
            {
                return Json(a + b, JsonRequestBehavior.AllowGet);
            }
    

    При этом в настройках проекта должна стоять галочка «XML documentation file»:



    После этого открываем Tools->Library Package Manager->Package Manager Console и вводим команду JsAction-GenDoc. В результате в папке Scripts будет сгенерирован файл JsActions.vsdoc.js. Теперь при написании скриптов Visual Studio сможет помочь вам:



    Кеширование

    В отличие от предыдущей библиотеки, кеширование динамически сформированного скрипта в браузере в текущей версии не предусмотрено.

    Заключение


    Каждая из представленных библиотек успешно решает проблему client-side маршрутизации. Выбор в пользу того или иного инструмента зависит от ваших требований и предпочтений. RouteJs фокусируется на решении задачи url-маршрутизации, не делая ничего лишнего. JsAction, напротив, предлагает более высокоуровневый и гибкий подход к выполнению ajax-запросов, предоставляя собственный интерфейс.

    Дополнительные ссылки:

    1. Для решения проблемы, поднятой в статье так же можно использовать библиотеку RazorJs, которая позволяет делает вставки Razor внутрь JavaScript файлов;
    2. https://github.com/zowens/ASP.NET-MVC-JavaScript-Routing еще одна библиотека для переноса маршрутов на client-side;
    3. Статья Phil Haack со ссылкой на его библиотеку.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 6

      0
      Если я правильно понимаю, то на клиенте мы имеем все роуты, которые сможет просмотреть пользователь через get (при желании)?
        0
        При использовании RouteJs на клиенте по умолчанию будут доступны все роуты, которые вы описали для приложения. Т.е. в данном случае вы пользуетесь методом Router.action() на клиенте, так же как @Url.Action() на сервере. Так же есть возможность выносить на клиент не все роуты, а только необходимые, помечая нужны экшены специальным атрибутом.
          0
          Полагаю вопрос качался не удобства а безопасности. Мы отдаём клиентам внутреннюю структуру нашего приложения?
            0
            Структуру не отдаем. Максимум, что появляется на клиенте — это названия некоторых контроллеров и действий. Что так же происходит в любом ASP.NET MVC приложении с дефолтным шаблоном маршрутизации. Не думаю, что это является какой-то угрозой безопасности.
        0
        *Это я вслух думаю. Конечного идеального решения у меня в голове пока нет*

        При том, что сам сталкивался с этой проблемой, мне кажется эти библиотеки решают не ту проблему.
        Кмк, проблема не в том, как передать в яваскрипт аякс-урл а в том, что яваскрипт воспринимается, как небольшое дополнение к серверной бизнес-логике. Конечно, если на весь проект, единственный аякс запрос — это что-то вроде вернуть общее количество записей в БД, тогда можно и адрес метода в тег сгенирировать.

        Но если в каждом контроллере есть хотя бы один аякс метод и к тому же к нему появляются параметры — стоит задуматься об комбинации аякс-апи / MVC-фреймворк. Сильно много времени это не заберёт, но во-первых, вынудит явно сформулировать интерфейс (REST например) на сервере и освободит клиент от жёсткой зависимости. Тот же Angular прекрасно умеет подставлять значения в запросы и все пути можно описать в одном месте.

        Опять же это открывает дорогу для других клиентов (вот затребует клиент не только веб сайт, но и десктоп или вообще, мобильную апп).
          0
          К слову, Ajax — это не обязательно JSON, могут загружаться куски уже сгенерированной на сервере страницы. Именно при таком подходе удобно использовать описанный подход.

          А что касается Single-Page App (на Backbone, Angular, Ember, итд) — тут концепция немного другая. И тогда не нужно генерировать URL в data-* атрибут какого-нибудь тега, нужно передавать URL централизованно (что лучше) или же использовать какие-то соглашения, чтобы URL на клиенте «магическим образом» совпадали с роутами сервера (что хуже — ибо, на практике часто расходится и во многих случаях необходимо пойти немного в разрез каноническому REST, а также нельзя забывать про base-URL).

        Only users with full accounts can post comments. Log in, please.