Какая "большая идея" стоит за маршрутами 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-paramsmiddleware (напомним, что он неявно включен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 код, избегая шаблонного доступа к данным. используя: как и печать результата (или ключей результата) вы можете получить лучшее представление о том, какие другие данные вы могли бы получить доступ.