Яка "велика ідея" за композиторськими маршрутами?


109

Я новачок у Clojure і використовую Compojure для написання основної веб-програми. Я б'є по стіні з defroutesсинтаксисом Compojure , і я думаю, що мені потрібно зрозуміти як "як", так і "чому" позаду цього.

Здається, що додаток у стилі Ring починається з карти запитів 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.

На поверхневому рівні це все, що стосується питання "чому". Щоб зайти трохи глибше, давайте подивимось, як функціонує додаток у стилі Ring:

  1. Запит надходить і перетворюється на карту Clojure відповідно до специфікації Ring.

  2. Ця карта об'єднана у так звану "оброблювальну функцію", яка, як очікується, призведе до відповіді (що також є карткою Clojure).

  3. Карта відповідей перетворюється на фактичну відповідь HTTP та надсилається назад клієнту.

Крок 2. у вищесказаному є найцікавішим, оскільки відповідальність обробника має вивчити URI, який використовується у запиті, вивчити будь-які файли cookie тощо і, нарешті, отримати відповідну відповідь. Зрозуміло, що необхідно, щоб вся ця робота була включена до колекції чітко визначених творів; це, як правило, функція "базового" обробника і набір функцій проміжного програмного забезпечення. Метою Compojure є спрощення генерації функції базового обробника.

Як?

Компожур будується навколо поняття "маршрути". Вони реально реалізовані на більш глибокому рівні бібліотекою Clout (спінофф проекту Compojure - багато речей було переміщено до окремих бібліотек при переході 0,3.x -> 0,4.x). Маршрут визначається (1) методом HTTP (GET, PUT, HEAD ...), (2) шаблоном URI (вказаний синтаксисом, який, мабуть, знайомий рубістистам Webby), (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означає тут через хвилину (але зауважимо, що це неправдивий відповідь Ring!).

Як видно з цього прикладу, 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
"В якості додаткової зручності оброблювані оброблювачами розмежування загортаються у параметри обгортання та" wrap-cookies "неявно." - Починаючи з версії 0.6.0, ви повинні додати це явно. Ref 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)))

Ця остання створена функція схожа на наш обробник!

Будь ласка, переконайтеся, що перевірити Джеймс пост , оскільки він детальніше пояснює.


4

Для кожного, хто все ще намагався дізнатися, що відбувається з маршрутами, можливо, як і я, ви не розумієте ідеї руйнування.

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

Я вставляю відповідні розділи нижче:

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

Вектор-прив'язки-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]]

Вектор-прив'язки-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, але, ось, ось речі, які я зробив на закладках.


Дякую, ці посилання безумовно корисні. Я працюю над цією проблемою в більшу частину дня і перебуваю в кращому місці з нею ... Я спробую опублікувати наступні дії в якийсь момент.
Шон Вудс

1

Яка угода з руйнуванням ({form-params: form-params})? Які ключові слова доступні для мене під час деструктуризації?

Доступні ключі - це ті, які знаходяться на карті введення. Деструкція доступна у формах дозволу та дози, або всередині параметрів до 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)]

При розумному використанні руйнування знищує ваш код, уникаючи доступу до даних котлів. використовуючи: як і роздруковуючи результат (або клавіші результату), ви можете краще зрозуміти, до яких інших даних ви могли отримати доступ.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.