Какая "большая идея" стоит за маршрутами compojure?
Я новичок в Clojure и использую Compojure для написания базового веб-приложения. Я бьюсь о стену с Compojure defroutes
синтаксис, хотя, и я думаю, что мне нужно понять как "как" и "почему" за всем этим.
похоже, что приложение в стиле кольца начинается с карты HTTP-запроса, а затем просто передает запрос через ряд функций промежуточного программного обеспечения, пока он не преобразуется в карту ответов, которая отправляется обратно в браузер. Этот стиль тоже кажется "низкий уровень" для разработчиков, таким образом, необходимость в таком инструменте, как Compojure. Я вижу эту потребность в большем количестве абстракций и в других программных экосистемах, особенно с WSGI Python.
проблема в том, что я не понимаю подход 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
источник уже давно,но просто не понимаю! Что здесь происходит? Понимание "большой идеи", вероятно, поможет мне ответить на эти конкретные вопросы:
- как получить доступ к Кольцевой среде из маршрутизируемой функции (например,
5 ответов:
Compojure объяснил (до некоторой степени)
NB. Я работаю с Compojure 0.4.1 (здесьфиксация выпуска 0.4.1 на GitHub).
почему?
на самом верху
compojure/core.clj
, вот это полезное резюме цели Compojure:краткий синтаксис для генерации обработчиков колец.
на поверхностном уровне, это все, что есть в вопросе "почему". Чтобы пойти немного глубже, давайте посмотрим, как работает приложение в стиле кольца:
запрос поступает и преобразуется в карту Clojure в соответствии со спецификацией кольца.
эта карта направляется в так называемую" функцию обработчика", которая, как ожидается, даст ответ (который также является картой Clojure).
карта ответов преобразуется в фактический HTTP-ответ и отправляется обратно на сервер клиент.
Шаг 2. в выше является наиболее интересным, так как он несет ответственность обработчика для изучения URI, используемый в запросе, проверить какие-либо cookies и т. д. и в конечном итоге прийти к соответствующему ответу. Очевидно, что вся эта работа должна быть учтена в коллекции четко определенных частей; обычно это "базовая" функция обработчика и коллекция функций промежуточного программного обеспечения, обертывающих ее. цель Compojure состоит в том, чтобы упростить генерация базовой функции обработчика.
как?
Compojure построен вокруг понятия "маршруты". Они фактически реализованы на более глубоком уровне с помощью Пптю библиотека (побочный продукт проекта Compojure-многие вещи были перемещены в отдельные библиотеки в 0.3.x - > 0,4.X-переход). Маршрут определяется (1) методом HTTP (GET, PUT, HEAD...), (2) шаблон URI (указанный с синтаксисом, который, по-видимому, будет знаком Webby Rubyists), (3) Форма деструктурирования, используемая для привязки частей карты запроса к именам, доступным в теле, (4) тело выражений, которое должно произвести действительный Кольцевой ответ (в нетривиальных случаях это обычно просто вызов отдельной функции).
это может быть хорошим моментом, чтобы взглянуть на простой пример:
(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
означает здесь через минуту (но обратите внимание, что это не действительное кольцо respose!).как видно из этого примера,
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-и-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
middleware (напомним, что он неявно включен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] ...)
-- the:filename #".*"
часть вообще ничего не делает (с#".*"
всегда совпадает). Он вызывает функцию утилиты Ringring.util.response/file-response
чтобы произвести его ответ; the{:root "./static"}
часть говорит ему, где искать файл.
(ANY "*" [] ...)
-- все пути. Это хорошая практика Compojure всегда включать такой маршрут в концеdefroutes
форма, чтобы гарантировать, что определяемый обработчик всегда возвращает допустимую кольцевую карту ответа (напомним, что ошибка сопоставления маршрута приводит кnil
).почему так?
одной из целей промежуточного программного обеспечения Ring является добавление информации в карту запросов; таким образом, промежуточное программное обеспечение для обработки файлов cookie добавляет
:cookies
ключ к требованию,wrap-params
добавляет:query-params
и/или:form-params
если строка запроса / форма данных присутствует и так далее. (Строго говоря, вся информация, которую добавляют функции промежуточного программного обеспечения, должна уже присутствовать в карте запросов, так как это то, что они передают; их задача-преобразовать ее, чтобы было удобнее работать с обработчиками, которые они обертывают.) В конечном итоге "обогащенный" запрос передается базовому обработчику, который проверяет карту запроса со всеми красиво предварительно обработанная информация добавляется промежуточным программным обеспечением и выдает ответ. (Промежуточное программное обеспечение может делать более сложные вещи , чем это-например, обертывание нескольких "внутренних" обработчиков и выбор между ними, решение о том, следует ли вообще вызывать обработчик(Ы) и т. д. Это, однако, выходит за рамки этого ответа.)базовый обработчик, в свою очередь, обычно (в нетривиальных случаях) является функцией, которая, как правило, нуждается всего в нескольких элементах информации о запросе. (Например,
ring.util.response/file-response
не заботится о большей части запроса; ему нужно только имя файла.) Отсюда необходимость в простом способе извлечения только соответствующих частей кольцевого запроса. Compojure стремится обеспечить специальный механизм сопоставления шаблонов, так сказать, который делает именно это.
есть отличная статья в booleanknot.com от Джеймса Ривза (автора Compojure), и чтение его сделало его "щелчком" для меня, поэтому я перевел некоторые из них здесь (действительно, это все, что я сделал).
есть также slidedeck здесь от того же автора, что отвечает на этот точный вопрос.
Compojure основан на кольцо, который является абстракцией для 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's 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#] ~@body)))
эта последняя сгенерированная функция выглядит как наш обработчик !
пожалуйста, не забудьте проверить Джеймс в должности, так как он переходит в более подробные объяснения.
на самом деле чтение документы на
let
помог прояснить все " откуда берутся магические ценности?" вопрос.я вставляю соответствующие разделы ниже:
в Clojure поддерживает абстрактные структурные связывание, часто называемое деструктурированием, давайте в обязательные списки, параметр ФН списки, и любой макрос, который расширяется в Пусть или ФН. Основная идея заключается в том, что binding-форма может быть структурой данных литерал, содержащий символы, которые получают привязан к соответствующим частям init-expr. Привязка является абстрактной в что векторный литерал может привязываться к все, что является последовательным, в то время как a литерал карты может привязываться ко всему, что является ассоциативным.
вектор привязки-exprs позволяют привязать имена для частей последовательных вещей (не только векторы), как и векторы, списки, seqs, строки, массивы, и все, что поддерживает nth. Основной последовательная форма-это вектор обязательными формами, которые будут привязаны к последовательные элементы от init-expr, посмотрел вверх через nth. В добавление, и необязательно, & следуют обязательных форм приведет к тому, что обязательные формы должны быть привязаны к остаток последовательности, т. е. что часть еще не связана, посмотрел вверх через nthnext . Наконец, также необязательно: как после этого символ вызовет что символ, который будет привязан ко всему инит-выражение:
(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]]
вектор привязки-exprs позволяют привязать имена для частей последовательных вещей (не только векторы), как векторы, списки, seqs, строки, массивы, и все, что поддерживает nth. Основной последовательная форма-это вектор обязательными формами, которые будут привязаны к последовательные элементы от init-expr, посмотрел вверх через nth. В добавление, и необязательно, & следуют по привязке-формы будут потому что обязательные формы должны быть привязаны к остаток последовательности, т. е. что часть еще не связана, посмотрел вверх через nthnext . Наконец, также необязательно: как после этого символ вызовет это символ, который будет привязан ко всему инит-выражение:
(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]]
в чем дело с деструктурированием ({form-params :form-params})? Какие ключевые слова доступны для меня при деструктурировании?
доступные клавиши-это те, которые находятся на входной карте. Деструктурирование доступно внутри форм let и doseq, либо внутри параметров 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)]
при грамотном использовании, деконструкция declutters код, избегая шаблонного доступа к данным. используя: как и печать результата (или ключей результата) вы можете получить лучшее представление о том, какие другие данные вы могли бы получить доступ.