Существуют библиотеки на различных языках, имеющие общие черты. Это compojure, sinatra, grape, express, koa и подобные.
У них схожий подход к роутингу. Они не накладывают никаких ограничений и не предлагают структуру для организации url. Разработчики в таких условиях склонны не заботиться о структуре и впоследствии получают плохо поддерживаемый код.
Другая общая черта — это однонаправленность. Т.е. определенному запросу соответствует определенный обработчик. Разработчики вынуждены прописывать url строками в шаблонах. Нет возможности указать в виде конструкции языка, какой url сгенерировать. Это приводит к тому, что в представлениях остаются мертвые ссылки, и нет способа найти их, кроме как протыкать все страницы.
Я расскажу, как улучшить поддерживаемость кода в экосистеме Clojure, и покажу, как:
- организовать url'ы
- структурировать код обработчиков
- использовать языковые конструкции для генерации url
Важно понимать, что перечисленные выше библиотеки имеют средства для организации обработчиков в модули, можно вручную завернуть url строки в функции и пользоваться только этими функциями. Но об этом, как правило, задумываются слишком поздно.
Я — ruby разработчик, и в других экосистемах (clojure, js, erlang, go) мне не хватает организации роутинга, подобного rails. Мне не хватает REST и понятия "ресурс". Мне не хватает контроллеров. Мне не хватает хелперов для генерации url, вроде admin_page_path(@page).
Если вы не знакомы с rails, то вот ссылка на описание роутинга "Rails Routing from the Outside In". Если вы подзабыли, что такое HTTP или REST, то я советую прочитать короткую и шутливую статью "15 тривиальных фактов о правильной работе с протоколом HTTP" или более обстоятельную "Зачем нужен этот ваш REST, а также о некоторых тонкостях реализации RESTful приложений".
Итак, используйте REST для организации url.
Для того чтобы разобраться с оставшимися двумя пунктами, я покажу примеры использования своей библиотеки darkleaf/router. Т.к. это экосистема clojure, то, разумеется, это ring-совместимый роутинг.
(ns hello-world.core (:require [darkleaf.router :refer :all])) (def pages-controller {:middleware (fn [handler] (fn [req] req)) :member-middleware some-other-middleware :index (fn [req] some-ring-response) :show (fn [req] some-ring-response)}) (def routes (build-routes (resources :pages 'page-id pages-controller))) (def handler (build-handler routes)) (def request-for (build-request-for routes)) (handler {:uri "/pages", :request-method :get}) ;; call index action from pages-controller (request-for :index [:pages]) ;; returns {:uri "/pages", :request-method :get} (handler {:uri "/pages/1", :request-method :get}) ;; call show action from pages-controller (request-for :show [:pages] {:page-id "1"}) ;; returns {:uri "/pages/1", :request-method :get}
Здесь, подобно rails, объявляется контроллер. В данном случае с двумя экшенами: index и show.
Контроллер — это всего лишь map, и вы можете, к примеру, создавать незначительно отличающиеся контроллеры с помощью своей функции.
Routes — это плоский вектор роутов, сгенерированных функциями наподобие resources.
Handler — это ring-совместимый обработчик запросов, а request-for служит для получения запроса, по имени роута и его области. Они генерируются с помощью макросов, т.к. используют внутри core.match.
Контроллер может содержать только следующие ключи:
- :middleware — оборачивает все экшены контроллера, включая обработчики вложенных роутов
- :member-middleware — оборачивает только member actions и обработчики вложенных роутов, помеченные как member
- collection actions: :index, :new, :create
- member actions: :show, :edit, :update, :destroy
Вот полный пример для ресурса:
(resources :pages 'page-id {:index identity :new identity :create identity :show identity :edit identity :update identity :destroy identity} :collection [(action :archived identity)] :member [(resources :comments 'comment-id {:index identity})])
Первым аргументом указывается название ресурсов, им же задается сегмент в url. Вторым параметром задается название идентификатора ресурса.
Как я упоминал выше, ресурсы могут включать в себя вложенные роуты двух типов: collection и member.
;; pages collection routes (request-for :archived [:pages] {}) ;; #=> {:uri "/pages/archived", :request-method :get} ;; pages member routes (request-for :index [:pages :comments] {:page-id "some-id"}) ;; #=> {:uri "/pages/some-id/comments", :request-method :get}
Кроме функции-генератора роутов resources также есть: root, action, wildcard, not-found, scope, guard, resource. Я не буду на них останавливаться, подробные примеры их использования вы найдете в тестах.
Нет способа добавить в контроллер новые экшены. Это осознанное ограничение, подталкивающее к REST и вложенным ресурсам. Если вам все-таки нужны дополнительные экшены, и вы не хотите использовать вложенные ресурсы, то можно использовать вложенный роут, как это показано выше для роута :archived. Но это будет отдельная функция вне контроллера.
Как вы уже заметили, request-for возвращает структуру запроса целиком, в отличие от рельсовых хэлперов, возвращающих только url. Это полезно, когда для определения обработчика используются заголовки или другие параметры запроса, например, host. В следующих релизах планируется поддержка clojurescript, и вы сможете использовать тот же request-for для построения запросов к бэкенду.
Т.к. request-for возвращает запрос целиком, то в нем существует проверка, что полученный запрос попадет в нужный обработчик. Это полезно, когда 2 похожих url обрабатываются различным образом или присутствует ограничение
(guard :locale #{"ru" "en"} (action :localized-page identity)) (not-found itentity)
(request-for :localized-page [:locale] {:locale "it"}) выдаст ошибку, т.к. обработчиком /it/localized-page будет роут not-found, а не :localized-page [:locale].
Библиотека поделена на 2 неймспейса:
darkleaf.router и darkleaf.router.low-level. Если у вас какие-то специфические требования к роутингу или необходимо поддерживать старую схему url, то можно написать свои функции поверх darkleaf.router.low-level, точно так же, как это сделано в darkleaf.router.
Полные примеры использования вы найдете в тестах darkleaf.router-test и darkleaf.router.low-level-test.
Библиотека использует внутри core.match и довольно занятные макросы. Но это уже тема отдельной статьи. Пишите в комментариях, если вам интересно узнать, как это работает внутри.
UPDATE 10-02-2017 выпустил версию 0.2, без макросов и сторонних библиотек, есть поддержка clojurescript https://github.com/darkleaf/router