Compojure пояснив (певною мірою)
NB. Я працюю з Compojure 0.4.1 ( ось команда випуску 0.4.1 на GitHub).
Чому?
На самому верху compojure/core.clj
, ось цей корисний підсумок мети Compojure:
Короткий синтаксис для генерації обробників Ring.
На поверхневому рівні це все, що стосується питання "чому". Щоб зайти трохи глибше, давайте подивимось, як функціонує додаток у стилі Ring:
Запит надходить і перетворюється на карту Clojure відповідно до специфікації Ring.
Ця карта об'єднана у так звану "оброблювальну функцію", яка, як очікується, призведе до відповіді (що також є карткою Clojure).
Карта відповідей перетворюється на фактичну відповідь 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>"))
Проаналізуємо по черзі кожен маршрут:
(GET "/" [] (workbench))
- звертаючись із GET
запитом :uri "/"
, викликайте функцію workbench
та візуалізуйте все, що воно повертається у карту відповідей. (Нагадаємо, що поверненим значенням може бути карта, але також рядок тощо)
(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
обробник, це ...)
(GET "/test" [& more] (str "<pre> more "</pre>"))
- це, наприклад, повторює відображення рядка карти, {"foo" "1"}
якщо користувальницький агент запитав "/test?foo=1"
.
(GET ["/:filename" :filename #".*"] [filename] ...)
- :filename #".*"
частина взагалі нічого не робить (оскільки #".*"
завжди збігається). Він викликає функцію утиліти Ring ring.util.response/file-response
для отримання її реакції; {:root "./static"}
частина говорить йому , де шукати файл.
(ANY "*" [] ...)
- загальний маршрут. Добре практика Compojure завжди включати такий маршрут у кінці defroutes
форми, щоб гарантувати, що визначений обробник завжди повертає дійсну карту відповідей Ring (пам'ятайте, що результат збігу маршруту призводить до nil
).
Чому саме так?
Одна мета проміжного програмного забезпечення Ring - додавання інформації до карти запитів; таким чином, проміжне програмне забезпечення для обробки файлів cookie додає :cookies
ключ до запиту, wrap-params
додає :query-params
та / або:form-params
якщо дані рядка / форми запиту є тощо. (Власне кажучи, вся інформація, яку додають функції проміжного програмного забезпечення, повинна бути вже присутня в карті запитів, оскільки саме так вони проходять; їх завдання полягає в тому, щоб перетворити її, щоб було зручніше працювати в обробниках, які вони завершують.) У кінцевому підсумку "збагачений" запит передається базовому оброблювачу, який вивчає карту запиту з усіма добре оброблюваною інформацією, доданою проміжним програмним забезпеченням, і дає відповідь. (Посереднє програмне забезпечення може робити більш складні речі, ніж це - наприклад, загортати декілька "внутрішніх" обробників та обирати між ними, вирішувати, чи взагалі викликати оброблені (-і) оброблювачі (тощо). Це, однак, виходить за межі цієї відповіді.)
Базовий обробник, у свою чергу, зазвичай (у нетривіальних випадках) функція, яка, як правило, потребує лише декількох відомостей про запит. (Наприклад ring.util.response/file-response
, байдужа більшість запитів; йому потрібне лише ім’я файлу.) Звідси необхідність простого способу вилучення лише відповідних частин запиту Ring. Compojure має на меті створити механізм узгодження спеціального малюнка, як би це було, що робить саме це.