Compojure объяснил (в некоторой степени)
NB. Я работаю с Compojure 0.4.1 ( вот коммит выпуска 0.4.1 на GitHub).
Зачем?
В самом верху compojure/core.clj
есть полезное изложение цели Compojure:
Краткий синтаксис для создания обработчиков кольца.
На поверхностном уровне это все, что касается вопроса «почему». Чтобы пойти немного глубже, давайте посмотрим, как работает приложение в стиле Ring:
Поступает запрос и преобразуется в карту 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 (карта запроса ниже - это минимальная допустимая карта запроса Ring):
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
такова, что каждый маршрут проверяется по очереди; как только один из них возвращает неответ 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
по "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
формы, чтобы гарантировать, что определяемый обработчик всегда возвращает действительную карту ответа Ring (напомним, что в результате возникает ошибка сопоставления маршрута nil
).
Почему именно так?
Одна из целей промежуточного программного обеспечения Ring - добавить информацию в карту запросов; таким образом, промежуточное ПО для обработки файлов cookie добавляет :cookies
ключ к запросу, wrap-params
добавляет :query-params
и / или:form-params
если присутствует строка запроса / данные формы и т. д. (Строго говоря, вся информация, которую добавляют функции промежуточного программного обеспечения, должна уже присутствовать в карте запроса, поскольку это то, что они передают; их задача - преобразовать ее, чтобы было удобнее работать в обработчиках, которые они обертывают.) В конечном итоге «обогащенный» запрос передается базовому обработчику, который проверяет карту запроса со всей хорошо предварительно обработанной информацией, добавленной промежуточным программным обеспечением, и выдает ответ. (Промежуточное ПО может делать более сложные вещи - например, обертывать несколько «внутренних» обработчиков и выбирать между ними, решать, вызывать ли обернутые обработчики вообще и т. Д. Это, однако, выходит за рамки данного ответа.)
Базовый обработчик, в свою очередь, обычно (в нетривиальных случаях) является функцией, которая, как правило, требует лишь нескольких элементов информации о запросе. (Например ring.util.response/file-response
, не заботится о большей части запроса; ему нужно только имя файла.) Отсюда потребность в простом способе извлечения только соответствующих частей запроса Ring. Compojure стремится предоставить специальный механизм сопоставления с образцом, так сказать, который делает именно это.