В чем «большая идея» маршрутов самообладания?

109

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

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

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

(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источник, но не понимаю! Что тут происходит? Понимание «большой идеи», вероятно, поможет мне ответить на эти конкретные вопросы:

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

Мне очень нравится Clojure, но я в тупике!

Шон Вудс
источник

Ответы:

212

Compojure объяснил (в некоторой степени)

NB. Я работаю с Compojure 0.4.1 ( вот коммит выпуска 0.4.1 на GitHub).

Зачем?

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

Краткий синтаксис для создания обработчиков кольца.

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

  1. Поступает запрос и преобразуется в карту Clojure в соответствии со спецификацией Ring.

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

  3. Карта ответов преобразуется в фактический 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>"))

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

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

Михал Марчик
источник
3
«Для дополнительного удобства обработчики, определенные для defroutes, неявно обертываются в wrap-params и wrap-cookies». - Начиная с версии 0.6.0, вы должны добавить их явно. Ссылка github.com/weavejester/compojure/commit/…
Дэн Мидвуд,
3
Очень хорошо поставлено. Этот ответ должен быть на домашней странице Compojure.
Сиддхартха Редди
2
Обязательно к прочтению для всех, кто плохо знаком с Compojure. Я желаю, чтобы каждое сообщение в вики и блоге по этой теме начиналось со ссылки на это.
jemmons
7

На сайте booleanknot.com есть отличная статья Джеймса Ривза (автора Compojure), и чтение ее произвело на меня «щелчок», поэтому я переписал часть ее здесь (на самом деле это все, что я сделал).

Здесь также есть слайд-шоу от того же автора , которое отвечает на этот точный вопрос.

Compojure основан на Ring , который представляет собой абстракцию для 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)))

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

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

Nha
источник
4

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

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

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

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

Vector binding-exprs позволяет вам связывать имена с частями последовательных вещей (не только с векторами), такими как векторы, списки, последовательности, строки, массивы и все, что поддерживает 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]]

Vector binding-exprs позволяет вам связывать имена с частями последовательных вещей (не только с векторами), такими как векторы, списки, последовательности, строки, массивы и все, что поддерживает 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]]
Питер Брид
источник
3

Я еще не начал работать с веб-материалами clojure, но я буду, вот что я добавил в закладки.

nickik
источник
Спасибо, эти ссылки определенно полезны. Я работал над этой проблемой большую часть дня, и мне стало легче с ней ... Я постараюсь опубликовать продолжение в какой-то момент.
Шон Вудс
1

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

шумник
источник