Ответ 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 #".*"
ничего не делает (поскольку#".*"
всегда совпадает). Он вызывает функцию утилиты Ringring.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 нацелен на то, чтобы создать специальный механизм сопоставления шаблонов, который делает именно это.