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

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

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

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

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


person Sean Woods    schedule 15.08.2010    source источник


Ответы (5)


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

person Michał Marczyk    schedule 16.08.2010
comment
Для дополнительного удобства обработчики, определенные для defroutes, неявно заключаются в wrap-params и wrap-cookies. - Начиная с версии 0.6.0, вы должны добавить их явно. Ссылка github.com/weavejester/compojure/commit/ - person Dan Midwood; 26.04.2011
comment
Очень хорошо поставлено. Этот ответ должен быть на домашней странице Compojure. - person Siddhartha Reddy; 16.02.2012
comment
Обязательно к прочтению для всех, кто плохо знаком с Compojure. Я желаю, чтобы каждое сообщение в вики и блоге по этой теме начиналось со ссылки на это. - person jemmons; 15.12.2012
comment
Когда вы обсуждаете маршрут для приема всей почты домена, вы подразумеваете, что там, где есть несколько маршрутов, соответствующих запросу, существует какая-то последовательность приоритетов, чтобы решить, какой маршрут будет выбран. Делается ли это исключительно на основе порядка маршрутов или существуют какие-то правила для определения наилучшего соответствия? - person Andy; 06.12.2020

На 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)))

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

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

person nha    schedule 29.11.2014

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

Фактически читая документы для 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]]
person Pieter Breed    schedule 29.11.2010

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

person nickik    schedule 16.08.2010
comment
Спасибо, эти ссылки определенно полезны. Я работал над этой проблемой большую часть дня, и мне стало легче с ней ... Я постараюсь опубликовать продолжение в какой-то момент. - person Sean Woods; 16.08.2010

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

person noisesmith    schedule 23.04.2013