Что такое "большая идея" за маршрутами? - PullRequest
106 голосов
/ 15 августа 2010

Я новичок в Clojure и использую Compojure для написания базового веб-приложения.Я бью стену с синтаксисом Compojure defroutes, хотя, и я думаю, что мне нужно понять как «как» и «почему» за всем этим.

Это похоже на приложение в стиле Ringначинается с карты HTTP-запроса, затем просто пропускает запрос через ряд функций промежуточного программного обеспечения, пока не преобразуется в карту ответа, которая отправляется обратно в браузер.Этот стиль кажется слишком «низким уровнем» для разработчиков, поэтому требуется такой инструмент, как Compojure.Я вижу эту потребность в большем количестве абстракций и в других программных экосистемах, особенно с помощью Python WSGI.

Проблема в том, что я не понимаю подход Compojure.Давайте возьмем следующее defroutes S-выражение:

<code>(defroutes main-routes
  (GET "/"  [] (workbench))
  (POST "/save" {form-params :form-params} (str form-params))
  (GET "/test" [& more] (str "<pre>" more "
")) (GET [" /: filename ": filename #". * "] [Filename] (response / file-response filename {: root"./static "})) (ЛЮБОЙ" * "[]"

Страница не найдена.

"))

Я знаю, что ключ к пониманию всего этого лежит в некоторыхмакрос voodoo, но я не совсем понимаю макросы (пока). Я долго смотрел на источник defroutes, но просто не понимаю! Что здесь происходит? Понимание «большой идеи» поможетвозможно, помогут мне ответить на следующие конкретные вопросы:

  1. Как получить доступ к среде Ring из маршрутизируемой функции (например, функции workbench)? Например, скажем, я хотел получить доступ к заголовкам HTTP_ACCEPT иликакая-то другая часть запроса / промежуточного программного обеспечения?
  2. Какое дело с деструктуризацией ({form-params :form-params})? Какие ключевые слова мне доступны при деструктуризации?

Мне действительно нравится Clojure, ноЯ такой тупой!

Ответы [ 5 ]

208 голосов
/ 16 августа 2010

Композитор объяснил (до некоторой степени)

NB. Я работаю с Compojure 0.4.1 ( здесь это коммит релиза 0.4.1 на GitHub).

Почему?

В самом верху compojure/core.clj приведено краткое описание цели Compojure:

Краткий синтаксис для генерации обработчиков Ring.

На поверхностном уровне это все, что есть к вопросу «почему». Для более глубокого понимания давайте посмотрим, как функционирует приложение в стиле Ring:

  1. Приходит запрос и преобразуется в карту Clojure в соответствии со спецификацией Кольца.

  2. Эта карта направлена ​​в так называемую «функцию-обработчик», которая, как ожидается, выдаст ответ (который также является картой Clojure).

  3. Карта ответов преобразуется в фактический ответ HTTP и отправляется обратно клиенту.

Шаг 2. описанный выше является наиболее интересным, так как на обработчике лежит ответственность за проверку URI, используемого в запросе, проверку любых файлов cookie и т. Д. И, в конечном итоге, получение соответствующего ответа. Совершенно очевидно, что необходимо объединить всю эту работу в набор четко определенных частей; обычно это «базовая» функция-обработчик и набор промежуточных функций, обертывающих ее. Цель 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 (карта запроса ниже является минимальной действительной картой запроса вызова):

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 такова, что каждый маршрут пробуется по очереди; как только один из них возвращает не-1072 * ответ, этот ответ становится возвращаемым значением всего обработчика 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\"}"}

Самое время взглянуть на пример из текста вопроса:

<code>(defroutes main-routes
  (GET "/"  [] (workbench))
  (POST "/save" {form-params :form-params} (str form-params))
  (GET "/test" [& more] (str "<pre>" more "
")) (GET ["/: filename": filename # ". *"] [Filename] (имя файла ответа / файла-ответа {: root "./static"})) (ЛЮБОЙ "*" [] "

Страница не найдена.

"))

Давайте разберем каждый маршрут по очереди:

  1. (GET "/" [] (workbench)) - при работе с GET запросом с :uri "/" вызовите функцию workbench и отобразите все, что возвращается в карту ответов. (Напомним, что возвращаемое значение может быть картой, но также строкой и т. Д.)

  2. (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, этот ...)

  3. (GET "/test" [& more] (str "<pre> more "</pre>")) - это будет, например, возвратите строковое представление карты {"foo" "1"}, если пользовательский агент запросил "/test?foo=1".

  4. (GET ["/:filename" :filename #".*"] [filename] ...) - часть :filename #".*" вообще ничего не делает (так как #".*" всегда совпадает). Вызывает служебную функцию Ring ring.util.response/file-response для получения своего ответа; часть {:root "./static"} указывает, где искать файл.

  5. (ANY "*" [] ...) - универсальный маршрут. Хорошей практикой Compojure всегда является включение такого маршрута в конец формы defroutes, чтобы гарантировать, что определяемый обработчик всегда возвращает действительную карту ответа Ring (напомним, что сбой при сопоставлении маршрута приводит к nil).

Почему так?

Одной из целей промежуточного программного обеспечения Ring является добавление информации в карту запроса; таким образом, промежуточное ПО для обработки файлов cookie добавляет в запрос ключ :cookies, wrap-params добавляет :query-params и / или :form-params, если имеются данные строки / формы запроса и т. д. (Строго говоря, вся информация, которую добавляют функции промежуточного программного обеспечения, должна уже присутствовать в карте запросов, поскольку именно это они и передают; их задача - преобразовать ее, чтобы было удобнее работать с обработчиками, которые они переносят.) В конечном итоге «расширенный» запрос передается базовому обработчику, который анализирует карту запросов со всей предварительно обработанной информацией, добавленной промежуточным программным обеспечением, и выдает ответ. (Промежуточное программное обеспечение может делать более сложные вещи - например, оборачивать несколько «внутренних» обработчиков и выбирать между ними, решать, следует ли вообще вызывать обработанные обработчики и т. Д. Это, однако, выходит за рамки этого ответа.)

Базовый обработчик, в свою очередь, обычно (в нетривиальных случаях) является функцией, которая, как правило, требует лишь нескольких элементов информации о запросе. (Например, ring.util.response/file-response не заботится о большей части запроса; ему нужно только имя файла.) Следовательно, необходим простой способ извлечения только соответствующих частей запроса Ring. Compojure стремится предоставить специальный механизм сопоставления с образцом, который как раз и делает это.

7 голосов
/ 29 ноября 2014

Есть отличная статья на booleanknot.com от Джеймса Ривза (автора 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)))

Эта последняя сгенерированная функция выглядит как наш обработчик!

Пожалуйста, не забудьте проверить Джеймс пост , так как в нем более подробные объяснения.

3 голосов
/ 29 ноября 2010

Для любого, кто все еще изо всех сил пытался выяснить, что происходит с маршрутами, может случиться так, что, подобно мне, вы не понимаете идею разрушения.

На самом деле чтение документов для let помогло прояснить весь вопрос "откуда берутся магические ценности?" вопрос.

Я вставляю соответствующие разделы ниже:

Clojure поддерживает абстрактные структурные связывание, часто называемое деструктуризацией, в списках привязки let, параметр fn списки и любой макрос, который расширяется в пусть или фн. Основная идея заключается в том, что обязательная форма может быть структурой данных литерал, содержащий символы, которые получают привязаны к соответствующим частям INIT-выраж. Привязка абстрактная в что векторный литерал может связываться с все, что является последовательным, в то время как Карта буквально может связывать с чем угодно является ассоциативным.

Векторные привязки-exprs позволяют связывать имена частей последовательных вещей (не только векторы), как векторы, списки, последовательности, строки, массивы и все, что поддерживает nth. Основа последовательной формой является вектор Обязательные формы, которые будут связаны с последовательные элементы из init-expr, посмотрел вверх через nth. В дополнение, и, необязательно, с помощью обязательных форм приведет к тому, что обязательная форма для привязки к остаток последовательности, т.е. часть еще не связана, посмотрел вверх через nthnext. Наконец, также необязательно: как сопровождаемый символом приведет к тому, что символ, который будет привязан ко всему INIT-выражение:

(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 позволяют связывать имена частей последовательных вещей (не только векторы), как векторы, списки, последовательности, строки, массивы и все, что поддерживает nth. Основа последовательной формой является вектор Обязательные формы, которые будут связаны с последовательные элементы из init-expr, посмотрел вверх через nth. В Кроме того, и, необязательно, & следуют с помощью обязательных форм приведет к тому, что обязательная форма для привязки к остаток последовательности, т.е. часть еще не связана, посмотрел вверх через nthnext. Наконец, также необязательно: как сопровождаемый символом приведет к тому, что символ, который будет привязан ко всему INIT-выражение:

(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]]
3 голосов
/ 16 августа 2010
1 голос
/ 23 апреля 2013

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

...