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 має на меті створити механізм узгодження спеціального малюнка, як би це було, що робить саме це.