Какая "большая идея" позади композиционных маршрутов?

Я новичок в Clojure и использовал Compojure для написания базового веб-приложения. Тем не менее, я нажимаю на стену с синтаксисом Compojure defroutes, и я думаю, что мне нужно понять как "как", так и "почему" за всем этим.

Кажется, что приложение Ring-style начинается с карты запросов HTTP, а затем просто передает запрос через ряд функций промежуточного программного обеспечения, пока оно не будет преобразовано в карту ответа, которая будет отправлена ​​обратно в браузер. Этот стиль выглядит слишком "низкоуровневым" для разработчиков, поэтому нужен такой инструмент, как Compojure. Я вижу эту потребность в дополнительных абстракциях в других программных экосистемах, в первую очередь с Python WSGI.

Проблема в том, что я не понимаю подход Compojure. Возьмем следующее defroutes S-выражение:

(defroutes main-routes
  (GET "/"  [] (workbench))
  (POST "/save" {form-params :form-params} (str form-params))
  (GET "/test" [& more] (str "<pre>" more "</pre>"))
  (GET ["/:filename" :filename #".*"] [filename]
    (response/file-response filename {:root "./static"}))
  (ANY "*"  [] "<h1>Page not found.</h1>"))

Я знаю, что ключ к пониманию всего этого лежит в каком-то макро-вуду, но я не совсем понимаю макросы (пока). Я долго смотрел на источник defroutes, но просто не понимаю! Что здесь происходит? Понимание "большой идеи", вероятно, поможет мне ответить на эти конкретные вопросы:

  • Как мне получить доступ к среде Ring из маршрутизируемой функции (например, функция workbench)? Например, скажем, я хотел получить доступ к заголовкам HTTP_ACCEPT или какой-либо другой части запроса/промежуточного программного обеспечения?
  • Какая сделка с деструктурированием ({form-params :form-params})? Какие ключевые слова доступны для меня при деструкции?

Мне очень нравится Clojure, но я настолько тупой!

Ответ 1

Compojure объяснил (в некоторой степени)

NB. Я работаю с Compojure 0.4.1 (здесь, фиксация релиза 0.4.1 на GitHub).

Почему?

В самом верху compojure/core.clj, это полезное резюме цели Compojure:

Краткий синтаксис для генерации обработчиков Ring.

На поверхностном уровне все, что касается вопроса "почему". Чтобы немного углубиться, давайте посмотрим, как работает приложение Ring-style:

  • Прибывает запрос и преобразуется в карту Clojure в соответствии со спецификацией Ring.

  • Эта карта направляется в так называемую "функцию обработчика", которая, как ожидается, даст ответ (который также является картой Clojure).

  • Карта ответов преобразуется в фактический HTTP-ответ и отправляется обратно клиенту.

Шаг 2. в приведенном выше примере является самым интересным, так как ответственность за обработчик, используемую в запросе, заключается в том, чтобы обработать URI, изучить любые файлы cookie и т.д. и в конечном итоге получить соответствующий ответ. Очевидно, что вся эта работа должна учитываться в наборе четко определенных частей; обычно это "базовая" функция обработчика и набор функций промежуточного программного обеспечения, которые ее обертывают. Цель Compojure - упростить генерацию функции базового обработчика.

Как?

Compojure построен вокруг понятия "маршруты". Они фактически реализованы на более глубоком уровне библиотекой Clout (в качестве альтернативы проекта Compojure - многие вещи были перемещены в отдельные библиотеки на переход 0.3.x → 0.4.x). Маршрут определяется (1) методом HTTP (GET, PUT, HEAD...), (2) шаблон URI (указанный с синтаксисом, который, по-видимому, будет знаком с Webby Rubyists), (3) форма деструктурирования, используемая в связывающие части карты запроса с именами, доступными в теле, (4) тело выражений, которое должно вызывать действительный ответ Ring (в нетривиальных случаях это обычно просто вызов отдельной функции).

Это может быть хорошим моментом, чтобы взглянуть на простой пример:

(def example-route (GET "/" [] "<html>...</html>"))

Позвольте проверить это на REPL (карта запроса ниже - минимальная действительная карта запроса звонка):

user> (example-route {:server-port 80
                      :server-name "127.0.0.1"
                      :remote-addr "127.0.0.1"
                      :uri "/"
                      :scheme :http
                      :headers {}
                      :request-method :get})
{:status 200,
 :headers {"Content-Type" "text/html"},
 :body "<html>...</html>"}

Если :request-method были :head, ответ был бы nil. Мы вернемся к вопросу о том, что означает nil здесь через минуту (но обратите внимание, что это не действительное Ring respose!).

Как видно из этого примера, example-route является просто функцией и очень простой в этом; он смотрит на запрос, определяет, заинтересован ли он в его обработке (изучая :request-method и :uri), и, если это так, возвращает базовую карту ответа.

Что также очевидно, так это то, что организму маршрута не нужно действительно оценивать правильную карту ответа; Compojure обеспечивает стандартную обработку по умолчанию для строк (как показано выше) и ряд других типов объектов; Подробные сведения см. в compojure.response/render multimethod (код полностью самодокументирован здесь).

Попробуйте использовать defroutes сейчас:

(defroutes example-routes
  (GET "/" [] "get")
  (HEAD "/" [] "head"))

Ответы на примерный запрос, показанный выше, и его вариант с :request-method :head похожи на ожидаемые.

Внутренние работы example-routes таковы, что каждый маршрут проверяется поочередно; как только один из них возвращает ответ не nil, этот ответ становится возвращаемым значением всего обработчика example-routes. В качестве дополнительного удобства defroutes -пределенные обработчики неявно завернуты в wrap-params и wrap-cookies.

Вот пример более сложного маршрута:

(def echo-typed-url-route
  (GET "*" {:keys [scheme server-name server-port uri]}
    (str (name scheme) "://" server-name ":" server-port uri)))

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

Проверка выше:

user> (echo-typed-url-route {:server-port 80
                             :server-name "127.0.0.1"
                             :remote-addr "127.0.0.1"
                             :uri "/foo/bar"
                             :scheme :http
                             :headers {}
                             :request-method :get})
{:status 200,
 :headers {"Content-Type" "text/html"},
 :body "http://127.0.0.1:80/foo/bar"}

Блестящая последующая идея вышеизложенного заключается в том, что более сложные маршруты могут assoc добавить дополнительную информацию на запрос на этапе соответствия:

(def echo-first-path-component-route
  (GET "/:fst/*" [fst] fst))

В ответ на запрос из предыдущего примера отвечает :body of "foo".

В этом последнем примере есть две вещи: "/:fst/*" и непустой вектор привязки [fst]. Первый - это вышеупомянутый синтаксис Rails-and-Sinatra для шаблонов URI. Это немного сложнее, чем то, что видно из приведенного выше примера в том, что поддерживаются ограничения регулярного выражения на сегментах URI (например, ["/:fst/*" :fst #"[0-9]+"] может быть предоставлен для того, чтобы маршрут принимал только всезначные значения :fst в приведенном выше). Второй - упрощенный способ сопоставления записи :params в карте запроса, которая сама по себе является картой; он полезен для извлечения сегментов URI из запроса, параметров строки запроса и параметров формы. Пример, иллюстрирующий последнее:

(defroutes echo-params
  (GET "/" [& more]
    (str more)))

user> (echo-params
       {:server-port 80
        :server-name "127.0.0.1"
        :remote-addr "127.0.0.1"
        :uri "/"
        :query-string "foo=1"
        :scheme :http
        :headers {}
        :request-method :get})
{:status 200,
 :headers {"Content-Type" "text/html"},
 :body "{\"foo\" \"1\"}"}

Это было бы подходящее время, чтобы взглянуть на пример из текста вопроса:

(defroutes main-routes
  (GET "/"  [] (workbench))
  (POST "/save" {form-params :form-params} (str form-params))
  (GET "/test" [& more] (str "<pre>" more "</pre>"))
  (GET ["/:filename" :filename #".*"] [filename]
    (response/file-response filename {:root "./static"}))
  (ANY "*"  [] "<h1>Page not found.</h1>"))

Проанализируйте каждый маршрут по очереди:

  • (GET "/" [] (workbench)) - при работе с запросом GET с :uri "/" вызовите функцию workbench и отрисуйте все, что оно вернет в карту ответов. (Напомним, что возвращаемое значение может быть картой, но также строкой и т.д.)

  • (POST "/save" {form-params :form-params} (str form-params)) - :form-params - это запись в карте запроса, предоставляемой промежуточным программным обеспечением wrap-params (напомним, что она неявно включена в defroutes). Ответ будет стандартным {:status 200 :headers {"Content-Type" "text/html"} :body ...} с (str form-params), замененным на .... (Немного необычный обработчик POST, этот...)

  • (GET "/test" [& more] (str "<pre> more "</pre>")) - это будет, например, повторите строковое представление карты {"foo" "1"}, если пользовательский агент запросил "/test?foo=1".

  • (GET ["/:filename" :filename #".*"] [filename] ...) - часть :filename #".*" ничего не делает (поскольку #".*" всегда совпадает). Он вызывает функцию утилиты Ring ring.util.response/file-response для получения ответа; в разделе {:root "./static"} указывается, где искать файл.

  • (ANY "*" [] ...) - маршрут для всех. Хорошая практика Compojure всегда включает такой маршрут в конце формы defroutes, чтобы гарантировать, что обработчик, определяемый, всегда возвращает действительную карту ответа на звонок (напомните, что сбой соответствия маршрута приводит к nil).

Почему так?

Одной из целей промежуточного программного обеспечения Ring является добавление информации на карту запроса; таким образом, промежуточное программное обеспечение для обработки cookie добавляет к запросу ключ :cookies, wrap-params добавляет :query-params и/или :form-params, если данные строки запроса/формы присутствуют и так далее. (Строго говоря, вся информация, которую добавляют компоненты промежуточного программного обеспечения, должна быть уже представлена ​​в карте запроса, так как это то, через что они передаются, их задача состоит в том, чтобы преобразовать ее, чтобы было удобнее работать с обработчиками, которые они переносят.) В конечном счете, "обогащенный" запрос передается базовому обработчику, который анализирует карту запроса со всей хорошо обработанной информацией, добавленной промежуточным программным обеспечением, и производит ответ. (Middleware может делать более сложные вещи, чем это - например, обматывать несколько "внутренних" обработчиков и выбирать между ними, решая, следует ли вообще называть обработанный обработчик (-и) и т.д. Однако это выходит за рамки этого ответа.)

Базовый обработчик, в свою очередь, обычно (в нетривиальных случаях) - это функция, которая, как правило, нуждается только в нескольких частях информации о запросе. (Например, ring.util.response/file-response не заботится о большей части запроса, ему требуется только имя файла.) Отсюда необходимость простого способа извлечения только соответствующих частей запроса Ring. Compojure нацелен на то, чтобы создать специальный механизм сопоставления шаблонов, который делает именно это.

Ответ 2

Существует отличная статья в booleanknot.com от Джеймса Ривза (автора Compojure), и, читая его, он сделал для меня "click" для меня, поэтому я переписал некоторые из них здесь (действительно, все, что я сделал).

Существует также slidedeck здесь от того же автора, который отвечает на этот точный вопрос.

Compojure основан на Ring, который является абстракцией для HTTP-запросов.

A concise syntax for generating Ring handlers.

Итак, что это за Кольцевые обработчики? Выдержка из документа:

;; Handlers are functions that define your web application.
;; They take one argument, a map representing a HTTP request,
;; and return a map representing the HTTP response.

;; Let take a look at an example:

(defn what-is-my-ip [request]
  {:status 200
   :headers {"Content-Type" "text/plain"}
   :body (:remote-addr request)})

Довольно простой, но также довольно низкий уровень. Вышеуказанный обработчик можно определить более кратко, используя библиотеку ring/util.

(use 'ring.util.response)

(defn handler [request]
  (response "Hello World"))

Теперь мы хотим вызвать разные обработчики в зависимости от запроса. Мы могли бы сделать некоторую статическую маршрутизацию так:

(defn handler [request]
  (or
    (if (= (:uri request) "/a") (response "Alpha"))
    (if (= (:uri request) "/b") (response "Beta"))))

И переформатируйте его так:

(defn a-route [request]
  (if (= (:uri request) "/a") (response "Alpha")))

(defn b-route [request]
  (if (= (:uri request) "/b") (response "Beta"))))

(defn handler [request]
  (or (a-route request)
      (b-route request)))

Интересная вещь, которую отмечает Джеймс, заключается в том, что это позволяет вложенные маршруты, потому что "результат объединения двух или более маршрутов вместе сам по себе является маршрутом".

(defn ab-routes [request]
  (or (a-route request)
      (b-route request)))

(defn cd-routes [request]
  (or (c-route request)
      (d-route request)))

(defn handler [request]
  (or (ab-routes request)
      (cd-routes request)))

В настоящее время мы начинаем видеть некоторый код, который выглядит так, как это можно было бы факторировать, используя макрос. Compojure предоставляет макрос defroutes:

(defroutes ab-routes a-route b-route)

;; is identical to

(def ab-routes (routes a-route b-route))

Compojure предоставляет другие макросы, например макрос GET:

(GET "/a" [] "Alpha")

;; will expand to

(fn [request#]
  (if (and (= (:request-method request#) ~http-method)
           (= (:uri request#) ~uri))
    (let [~bindings request#]
      [email protected])))

Эта последняя сгенерированная функция выглядит как наш обработчик!

Пожалуйста, не забудьте проверить Джеймс пост, поскольку он идет на более подробные объяснения.

Ответ 3

Для всех, кто все еще пытался выяснить, что происходит с маршрутами, может случиться так, что, как и я, вы не понимаете идею деструктурирования.

Фактически чтение документов для let помогло очистить все ", откуда берутся магические значения?" вопрос.

Я вставляю соответствующие разделы ниже:

Clojure поддерживает абстрактные структурные связывание, часто называемое деструктурированием, в списках связывания ссылок, параметр fn списки и любой макрос, который расширяется a let или fn. Основная идея состоит в том, что обязательной формой может быть структура данных литерал, содержащий символы, которые получают связанные с соответствующими частями INIT-выраж. Связывание является абстрактным в что векторный литерал может связываться с все, что является последовательным, в то время как map literal может связывать все, что ассоциативно.

Vector binding-exprs позволяет связывать имена для частей последовательных вещей (не только векторы), подобно векторам, списков, секций, строк, массивов и все, что поддерживает nth. Основа последовательная форма представляет собой вектор связующих форм, которые будут связаны с последовательные элементы из init-expr, посмотрел через n-й. В добавление и, необязательно, посредством связующих форм приведет к тому, что обязательная форма, связанная с остаток последовательности, то есть часть еще не связана, просмотрен через nthnext. Наконец, также необязательно: as за которым следует символ, символ, который должен быть привязан ко всему INIT-выражение:

(let [[a b c & d :as e] [1 2 3 4 5 6 7]]
  [a b c d e])
->[1 2 3 (4 5 6 7) [1 2 3 4 5 6 7]]

Vector binding-exprs позволяет связывать имена для частей последовательных вещей (не только векторы), подобно векторам, списков, секций, строк, массивов и все, что поддерживает nth. Основа последовательная форма представляет собой вектор связующих форм, которые будут связаны с последовательные элементы из init-expr, посмотрел через n-й. В добавление и, необязательно, посредством связующих форм приведет к тому, что обязательная форма, связанная с остаток последовательности, то есть часть еще не связана, просмотрен через nthnext. Наконец, также необязательно: as за которым следует символ, символ, который должен быть привязан ко всему INIT-выражение:

(let [[a b c & d :as e] [1 2 3 4 5 6 7]]
  [a b c d e])
->[1 2 3 (4 5 6 7) [1 2 3 4 5 6 7]]

Ответ 5

Какая сделка с деструктурированием ({form-params: form-params})? Какие ключевые слова доступны для меня при деструкции?

Доступными ключами являются те, которые находятся во входной карте. Разрушение доступно внутри форм let и доз или внутри параметров до fn или defn

Следующий код, мы надеемся, будет информативным:

(let [{a :thing-a
       c :thing-c :as things} {:thing-a 0
                               :thing-b 1
                               :thing-c 2}]
  [a c (keys things)])

=> [0 2 (:thing-b :thing-a :thing-c)]

более продвинутый пример, показывающий вложенное деструктурирование:

user> (let [{thing-id :id
             {thing-color :color :as props} :properties} {:id 1
                                                          :properties {:shape
                                                                       "square"
                                                                       :color
                                                                       0xffffff}}]
            [thing-id thing-color (keys props)])
=> [1 16777215 (:color :shape)]

При разумном использовании деструктурирование отклоняет ваш код, избегая доступа к шаблонам данных. используя: as и распечатку результата (или ключей результата), вы можете получить лучшее представление о том, какие другие данные вы могли бы получить.