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

  1. как получить доступ к Кольцевой среде из маршрутизируемой функции (например,
5 104

5 ответов:

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

NB. Я работаю с Compojure 0.4.1 (здесьфиксация выпуска 0.4.1 на GitHub).

почему?

на самом верху compojure/core.clj, вот это полезное резюме цели Compojure:

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

на поверхностном уровне, это все, что есть в вопросе "почему". Чтобы пойти немного глубже, давайте посмотрим, как работает приложение в стиле кольца:

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

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

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

давайте проанализируем каждый маршрут в очередь:

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

  2. (POST "/save" {form-params :form-params} (str form-params))--:form-params - это запись в карте запроса, предоставленной wrap-params middleware (напомним, что он неявно включен 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] ...) -- the :filename #".*" часть вообще ничего не делает (с #".*" всегда совпадает). Он вызывает функцию утилиты Ring ring.util.response/file-response чтобы произвести его ответ; the {:root "./static"} часть говорит ему, где искать файл.

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