Какова “большая идея” позади маршрутов 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. Как я получаю доступ к Кольцевой среде из направленной функции (например, workbench функция)? Например, скажите, что я хотел получить доступ к заголовкам HTTP_ACCEPT или некоторой другой части запроса/промежуточного программного обеспечения?
  2. Каково соглашение с destructuring ({form-params :form-params})? Какие ключевые слова доступны для меня когда destructuring?

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

107
задан Sean Woods 15 August 2010 в 17:05
поделиться

2 ответа

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>"}

If : request -method были : head , вместо этого ответ будет nil . Мы вернемся к вопросу о том, что здесь означает nil , через минуту (но обратите внимание, что это не действительный ответ кольца!).

Как видно из этого примера, example-route - это просто функция, причем очень простая; он просматривает запрос, определяет, заинтересован ли он в его обработке (проверяя : метод-запроса и : uri ), и, если да, возвращает базовую карту ответов.

Также очевидно, что тело маршрута на самом деле не нуждается в оценке для правильной карты ответов; Compojure обеспечивает разумную обработку по умолчанию для строк (как показано выше) и ряда других типов объектов; подробности см. в мультиметоде compojure.response / render (здесь код полностью самодокументируется).

Давайте попробуем использовать defroutes сейчас:

(defroutes example-routes
  (GET "/" [] "get")
  (HEAD "/" [] "head"))

Ответы на приведенный выше пример запроса и на его вариант с : request-method: head ожидаются.

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

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

(def echo-first-path-component-route
  (GET "/:fst/*" [fst] fst))

Это отвечает a : тело из "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 , this ...)

  3. (GET "/ test" [& more] (str "

     more" 
    ")) - - это, например, будет отображать строковое представление карты {"foo" "1"} , если пользовательский агент запросит "/ test? foo = 1" .

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

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

Почему именно так?

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

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

209
ответ дан 24 November 2019 в 03:39
поделиться

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

3
ответ дан 24 November 2019 в 03:39
поделиться
Другие вопросы по тегам:

Похожие вопросы: