Для многофункциональных веб-приложений довольно распространено использование какой-либо клиентской маршрутизации: загрузите приложение один раз, затем перейдите по специальным 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'});
При переходе на #/author
Angular заменит содержимое 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 делать слишком много, у меня не будет никакой пользы от пьедестала. С другой стороны, существует некоторое несоответствие импеданса между угловой «ориентированной моделью» и дифференциальными толчками Пьедестала. Если я когда-нибудь сделаю какие-то вменяемые выводы на этот счет, я напишу это. Но это другая история.