Для многофункциональных веб-приложений довольно распространено использование какой-либо клиентской маршрутизации: загрузите приложение один раз, затем перейдите по специальным URL-адресам, таким как http://example.com/#/subpage. Браузер не выполняет для них обратную передачу на сервер, но вместо этого правильный фрагмент JavaScript может перестраивать часть страницы локально, скрывать и показывать элементы и т. Д.
Инструменты JavaScript
Для этого есть ряд инструментов JavaScript. Я нахожу два примера особенно вдохновляющими.
Flatiron Director позволяет вам делать что-то вроде:
var author = function () { console.log("author"); };
var books = function () { console.log("books"); };
var routes = {
'/author': author,
'/books': books
};</p>
<p>var router = Router(routes);
router.init();
Затем, когда вы посещаете страницу, оканчивающуюся на #/books, она вызывает booksфункцию. Очень гибкий, но тогда вам нужно написать много кода самостоятельно.
Другой хороший пример с совершенно другой философией — Angular JS Router . На своей странице вы можете благословить специальный раздел как ngView, и настроить маршрутизатор с:
$routeProvider
.when('/author', {templateUrl: '/t_author.html', controller: "authorController"})
.when('/books', {templateUrl: '/t_books.html', controller: "booksController"})
.otherwise({redirectTo: '/books'});
При переходе на #/authorAngular заменит содержимое ngViewна шаблон и установит на него выбранный контроллер. Это хорошо вписывается в угловую философию и имеет некоторые преимущества.
пьедестал
В любом случае, как мы можем использовать маршрутизацию с Pedestal? Оказывается, нет встроенного или даже рекомендуемого способа сделать это, но вы можете подключить все, что захотите. После некоторых исследований я решил использовать goog.History от Closure, который поставляется бесплатно с ClojureScript.
Весь код для этих примеров находится в моем репозитории GitHub . Обратите внимание, что есть 3 тега, соответствующие каждому из решений.
Попытка 1: простая, жестко закодированная
Первое, что я сделал, было довольно уродливое, жестко закодированное решение, использующее весь поток данных Pedestal.
Сначала я установил прослушиватель, goog.Historyкоторый извлекает токен местоположения и помещает его в :routeтему входной очереди Pedestal:
(defn ^:private set-route [input-queue route]
(p/put-message input-queue {msg/topic :route msg/type :set-route :value route}))
(defn configure-router
([input-queue] (configure-router input-queue ""))
([input-queue default-route]
(doto (goog.History.)
(goog.events/listen (goog.object/getValues goog.history/EventType)
(fn [e]
(let [token (.-token e)]
(if (= "" token)
(set-route input-queue default-route)
(set-route input-queue token)))))
(.setEnabled true))))
Я подключил преобразователь и эмиттер по умолчанию, которые в основном выдвигают это сообщение к модели приложения:
(def count-app {:transform {:route {:init nil :fn #(:value %2)}}
:emit {:router {:fn app/default-emitter-fn :input #{:route}}}})
(defn ^:private set-route [input-queue route]
(p/put-message input-queue {msg/topic :route msg/type :set-route :value route}))
Последний шаг на конвейере — рендер. Этот здесь довольно неинтересен, он в основном заменяет содержимое некоторого конкретного div другим текстом:
(defn route-changed [_ [_ _ old-value new-value] input-queue]
(.log js/console "Routing from" old-value "to" new-value)
(let [container (dom/by-id "view-container")]
(dom/destroy-children! container)
(dom/append! container (str "<p>" new-value "</p>"))))
mainФункция:
(defn ^:export main []
(let [app (app/build count-app)
render-fn (push/renderer "content" [[:value [:route] route-changed]])]
(render/consume-app-model app render-fn)
(configure-router (:input app) "first")
(app/begin app)))
Это решение вряд ли можно использовать повторно, но оно также очень гибкое. В любой момент мы можем решить сделать что-то конкретное при переключении местоположения — изменить что-либо в модели данных или приложения, сделать запрос к серверу и т. Д.
Я бы не советовал, это всего лишь первая попытка решить проблему.
Попытка 2: общая
Совершенно очевидно, что приведенное решение может быть обобщено. Один из способов сделать это — обобщить рендер. Мы можем инициализировать его с помощью некоторой конфигурации, сообщающей, какое действие выполнить для каждого пути. Например, что-то вроде этого:
(defn render-route [msg]
(let [container (dom/by-id "view-container")]
(dom/destroy-children! container)
(dom/append! container (str "<p>" msg "</p>"))))
(defn route-first []
(render-route "This is the first route"))
(defn route-second []
(render-route "This is the second route"))
(def router-config
{:routes {"first" route-first
"second" route-second}
:default-route "first"
:listener (fn [old-value new-value]
(.log js/console "Routing from" old-value "to" new-value))})
Здесь происходит не так много интересного, но, безусловно, его можно использовать повторно.
Рендерер теперь инициализируется с помощью этой конфигурации, например:
(defn route-renderer [cfg]
(fn [_ [_ _ old-value new-value] input-queue]
(when-let [listener (:listener cfg)]
(listener old-value new-value))
(if-let [dispatcher (get-in cfg [:routes new-value])]
(dispatcher)
(.log js/console "Unknown route:" new-value))))
Я опущу слушателя истории и главное для ясности, но я надеюсь, что суть ясна. Опять же, код на Github под genericтегом.
Все, что делает это решение — привязывает вызов функции к каждому пути. Я мог бы легко извлечь его в крошечную общую библиотеку. Я также мог бы сделать его более мощным — например, использовать «функции конструктора» более высокого порядка, которые позволяют каждому действию получить доступ к состоянию или отправить в очередь ввода. Я мог бы использовать шаблоны. И так далее.
Попытка 3: общая — легкая
Завершая это, я нашел еще один способ сделать это. Помните, что оба вышеупомянутых решения используют весь стек Pedestal — прослушиватель истории отправляет сообщение во входную очередь, и нам нужен преобразователь и эмиттер, чтобы передать его рендереру. Может быть, мне не нужно привлекать «нижние слои» к навигации и рендерингу?
Я понял, что могу просто включить рендеринг в самом слушателе истории:
(defn route-first [input-queue]
(render-route "This is the first route"))
(defn route-second [input-queue]
(render-route "This is the second route"))
(def router-config
{:routes {"first" route-first
"second" route-second}
:default-route "first"
:listener (fn [new-value]
(.log js/console "Routing to" new-value))})
(defn route-changed [{:keys [input]} route-config]
(fn [e]
(let [token (.-token e)
token (if (= "" token) (:default-route route-config) token)]
(when-let [listener (:listener cfg)]
(listener token))
(if-let [dispatcher (get-in route-config [:routes token])]
(dispatcher input)
(.log js/console "Unknown route:" token)))))
(defn configure-router
([app route-config]
(doto (goog.History.)
(goog.events/listen
(goog.object/getValues goog.history/EventType)
(route-changed app route-config))
(.setEnabled true))))
(defn ^:export main []
(let [app (app/build)]
(configure-router app router-config)
(app/begin app)))
Вот и все. Ему даже не нужен пьедестал. Маршрутизатор — это общая мелочь, основанная только на Google Closure. Однако в этом случае каждое действие также имеет доступ к входной очереди, и совершенно очевидно, как вы могли бы выставлять любые другие вещи из Pedestal. Вы по-прежнему можете отправлять сообщения в Pedestal, устанавливать средства визуализации и т. Д., Но это больше не требуется для самой маршрутизации.
Завершение
В конце концов, я вполне доволен последним решением. Ну, в некотором смысле — это все еще требует от меня тонны ручной работы! Не для самой маршрутизации, а для рендеринга DOM. Это похоже на jQuery, с кучей утомительных ручных манипуляций с DOM.
Я думаю, что я стал немного избалованным Angular, и я уже экспериментирую с женитьбой на них.
Тем не менее, все время я сталкиваюсь с трениями — если я позволю Angular делать слишком много, у меня не будет никакой пользы от пьедестала. С другой стороны, существует некоторое несоответствие импеданса между угловой «ориентированной моделью» и дифференциальными толчками Пьедестала. Если я когда-нибудь сделаю какие-то вменяемые выводы на этот счет, я напишу это. Но это другая история.