Композитор объяснил (до некоторой степени)
NB. Я работаю с Compojure 0.4.1 ( здесь это коммит релиза 0.4.1 на GitHub).
Почему?
В самом верху compojure/core.clj
приведено краткое описание цели Compojure:
Краткий синтаксис для генерации обработчиков Ring.
На поверхностном уровне это все, что есть к вопросу «почему». Для более глубокого понимания давайте посмотрим, как функционирует приложение в стиле Ring:
Приходит запрос и преобразуется в карту Clojure в соответствии со спецификацией Кольца.
Эта карта направлена в так называемую «функцию-обработчик», которая, как ожидается, выдаст ответ (который также является картой Clojure).
Карта ответов преобразуется в фактический ответ HTTP и отправляется обратно клиенту.
Шаг 2. описанный выше является наиболее интересным, так как на обработчике лежит ответственность за проверку URI, используемого в запросе, проверку любых файлов cookie и т. Д. И, в конечном итоге, получение соответствующего ответа. Совершенно очевидно, что необходимо объединить всю эту работу в набор четко определенных частей; обычно это «базовая» функция-обработчик и набор промежуточных функций, обертывающих ее. Цель 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
означает здесь через минуту (но обратите внимание, что это недопустимое повторное размещение!).
Как видно из этого примера, example-route
- это просто функция, причем очень простая; он просматривает запрос, определяет, заинтересован ли он в его обработке (проверяя :request-method
и :uri
) и, если да, возвращает базовую карту ответов.
Что также очевидно, так это то, что тело маршрута на самом деле не нуждается в оценке для правильной карты ответов; Compojure обеспечивает нормальную обработку по умолчанию для строк (как показано выше) и ряда других типов объектов; подробнее см. мультиметод compojure.response/render
(код полностью самодокументируется здесь).
Давайте попробуем использовать defroutes
сейчас:
(defroutes example-routes
(GET "/" [] "get")
(HEAD "/" [] "head"))
Ответы на приведенный выше пример запроса и его вариант с :request-method :head
такие же, как ожидалось.
Внутренняя работа example-routes
такова, что каждый маршрут пробуется по очереди; как только один из них возвращает не-1072 * ответ, этот ответ становится возвращаемым значением всего обработчика 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
из "foo"
на запрос из предыдущего примера.
В этом последнем примере появилось две вещи: "/:fst/*"
и непустой связывающий вектор [fst]
. Первый - это вышеупомянутый подобный Rails и 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\"}"}
Самое время взглянуть на пример из текста вопроса:
<code>(defroutes main-routes
(GET "/" [] (workbench))
(POST "/save" {form-params :form-params} (str form-params))
(GET "/test" [& more] (str "<pre>" more "
"))
(GET ["/: filename": filename # ". *"] [Filename]
(имя файла ответа / файла-ответа {: root "./static"}))
(ЛЮБОЙ "*" [] "
Страница не найдена.
"))
Давайте разберем каждый маршрут по очереди:
(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
, чтобы гарантировать, что определяемый обработчик всегда возвращает действительную карту ответа Ring (напомним, что сбой при сопоставлении маршрута приводит к nil
).
Почему так?
Одной из целей промежуточного программного обеспечения Ring является добавление информации в карту запроса; таким образом, промежуточное ПО для обработки файлов cookie добавляет в запрос ключ :cookies
, wrap-params
добавляет :query-params
и / или :form-params
, если имеются данные строки / формы запроса и т. д. (Строго говоря, вся информация, которую добавляют функции промежуточного программного обеспечения, должна уже присутствовать в карте запросов, поскольку именно это они и передают; их задача - преобразовать ее, чтобы было удобнее работать с обработчиками, которые они переносят.) В конечном итоге «расширенный» запрос передается базовому обработчику, который анализирует карту запросов со всей предварительно обработанной информацией, добавленной промежуточным программным обеспечением, и выдает ответ. (Промежуточное программное обеспечение может делать более сложные вещи - например, оборачивать несколько «внутренних» обработчиков и выбирать между ними, решать, следует ли вообще вызывать обработанные обработчики и т. Д. Это, однако, выходит за рамки этого ответа.)
Базовый обработчик, в свою очередь, обычно (в нетривиальных случаях) является функцией, которая, как правило, требует лишь нескольких элементов информации о запросе. (Например, ring.util.response/file-response
не заботится о большей части запроса; ему нужно только имя файла.) Следовательно, необходим простой способ извлечения только соответствующих частей запроса Ring. Compojure стремится предоставить специальный механизм сопоставления с образцом, который как раз и делает это.