Что такое "большая идея" за маршрутами?
Я новичок в Clojure и использую Compojure для написания базового веб-приложения. Я бью стену с помощью Compojure defroutes
Синтаксис, хотя, и я думаю, что мне нужно понять как "как" и "почему" за всем этим.
Похоже, что приложение в стиле Ring начинается с карты HTTP-запросов, затем просто пропускает запрос через ряд функций промежуточного программного обеспечения, пока не преобразуется в карту ответов, которая отправляется обратно в браузер. Этот стиль кажется слишком "низким уровнем" для разработчиков, поэтому требуется такой инструмент, как Compojure. Я вижу эту потребность в большем количестве абстракций в других программных экосистемах, особенно с помощью Python WSGI.
Проблема в том, что я не понимаю подход 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
источник в течение длительного времени, но просто не понимаю! Что тут происходит? Понимание "большой идеи", вероятно, поможет мне ответить на следующие конкретные вопросы:
- Как получить доступ к среде Ring из маршрутизируемой функции (например,
workbench
функция)? Например, скажем, я хотел получить доступ к заголовкам HTTP_ACCEPT или какой-либо другой части запроса / промежуточного программного обеспечения? - Что за дело с деструктуризацией (
{form-params :form-params}
)? Какие ключевые слова доступны для меня при деструктуризации?
Мне очень нравится Clojure, но я так озадачен!
5 ответов
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 (заданного с помощью синтаксиса, который, очевидно, будет знаком вебби-рубинологам), (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 и 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 #".*"
часть вообще ничего не делает (так как#".*"
всегда совпадает) Вызывает служебную функцию Ringring.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 стремится предоставить специальный механизм сопоставления с образцом, который как раз и делает это.
На booleanknot.com есть отличная статья от Джеймса Ривза (автора Compojure), и чтение ее сделало ее "щелчком" для меня, поэтому я переписал некоторые из них здесь (на самом деле это все, что я сделал).
Здесь же есть слайд от того же автора, который отвечает на этот точный вопрос.
Compojure основан на Ring, который является абстракцией для http-запросов.
A concise syntax for generating Ring handlers.
Итак, что это за обработчики Ring? Выписка из документа:
;; 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 поддерживает абстрактную структурную привязку, часто называемую деструктуризацией, в списках привязки let, списках параметров fn и любом макросе, который расширяется до let или fn. Основная идея заключается в том, что связывающая форма может быть литералом структуры данных, содержащим символы, которые связываются с соответствующими частями init-expr. Связывание является абстрактным в том смысле, что векторный литерал может связываться со всем, что является последовательным, в то время как литерал карты может связываться со всем, что является ассоциативным.
Векторные выражения привязки позволяют вам связывать имена с частями последовательных объектов (не только с векторами), такими как векторы, списки, последовательности, строки, массивы и все, что поддерживает nth. Базовая последовательная форма - это вектор связывающих форм, который будет связан с последовательными элементами из init-expr, найденными через nth. Кроме того, и, необязательно, за & следует за связывающими формами, эта связывающая форма будет связана с остальной частью последовательности, то есть та часть, которая еще не связана, просматривается через nthnext . Наконец, также необязательно, так как после него символ будет привязан ко всему init-expr:
(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]]
Векторные выражения привязки позволяют вам связывать имена с частями последовательных объектов (не только с векторами), такими как векторы, списки, последовательности, строки, массивы и все, что поддерживает nth. Базовая последовательная форма - это вектор связывающих форм, который будет связан с последовательными элементами из init-expr, найденными через nth. Кроме того, и, необязательно, за & следует за связывающими формами, эта связывающая форма будет связана с остальной частью последовательности, то есть та часть, которая еще не связана, просматривается через nthnext . Наконец, также необязательно, так как после него символ будет привязан ко всему init-expr:
(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]]
Я еще не начал работать над Clojure, но сделаю это с закладками.
Что за дело с деструктуризацией ({form-params:form-params})? Какие ключевые слова доступны для меня при деструктуризации?
Доступны ключи, которые находятся на карте ввода. Разрушение доступно в формах let и dosq или в параметрах 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)]
При разумном использовании деструктуризация разрушает ваш код, избегая стандартного доступа к данным. используя: as и печатая результат (или ключи результата), вы можете получить лучшее представление о том, к каким другим данным вы можете получить доступ.