Что такое "большая идея" за маршрутами?

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

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

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

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

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

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

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

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

Другие вопросы по тегам