Статьи

Маршрутизация на стороне клиента с пьедесталом

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