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 стремится предоставить специальный механизм сопоставления с образцом, так сказать, который именно это и делает.
person
Michał Marczyk
schedule
16.08.2010