вторник, 12 января 2010 г.

"Hello World" с RESTAS

Я немного переработал RESTAS, отказавшись от пары заблуждений (впрочем, во внутренностях ещё остались их следы), что позволило избавиться от одного "магического" элемента в коде примеров и теперь я готов их немного прокомментировать.

Да, теперь RESTAS имеет текущую версию 0.0.3 и доступен для лёгкой установки либо через мой форк gentoo-lisp-overlay, либо через мой форк clbuild.

Hello World

Классический "Hello world", куда же без него:
(asdf:operate 'asdf:load-op '#:restas)

(restas:defsite #:restas.hello-world
(:use :cl))

(in-package #:restas.hello-world)

(define-route main ("")
"<h1>Hello world!</h1>")

(restas:start-site '#:restas.hello-world :port 8080)
Если после выполнения данного кода открыть в браузере ссылку http://localhost:8080/, то можно будет увидеть заголовок "Hello world", очень мило :)

Теперь по порядку, строчка за строчкой:
  1. Для начала загружает сам RESTAS:
    (asdf:operate 'asdf:load-op '#:restas)
  2. Объявляем новый сайт:
    (restas:defsite #:restas.hello-world
    (:use :cl))
    Фактически, данный код просто создаёт пакет и проводит его инициализацию: добавляет несколько переменных. Это довольно любопытный момент, что сайт не является объектом, либо символом, а связан с пакетом. Почему так сделано? Я хочу, что бы процесс создания веб-приложения был максимально простым и не требовал бы много буков. А использовании "пакета в качестве сайта" позволяет во многих случаях не указывать сайт явно, а просто размещать код, как-то влияющий на сайт, внутрь связанного с ним пакета.
  3. Меняем текущий пакет, как следует из предыдущего пункта, это важно:
    (in-package #:restas.hello-world)
  4. Создаём маршрут, ответственный за обработку GET-запроса к корню сайта:
    (define-route main ("")
    "<h1>Hello world!</h1>")
    Внутри макроса define-route может быть произвольный код, который должен вернуть либо строку, либо octets array, либо pathname (в этом случае клиент получит соответствующий файл), либо целое число (которое будет интерпретироваться как статус ответа, например hunchentoot:+http-not-found++). Также, это код может свободно использовать все переменные, указанные в документации на веб-сервер hunchentoot (например, hunchentoot:*request* или hunchentoot:*reply*). О маршрутах в RESTAS можно почитать ещё здесь.
  5. Запускаем веб-сервер и активизуем сайт:
    (restas:start-site '#:restas.hello-world :port 8080)

Обработка POST-запросов

А это пример демонстрирует различную обработку GET и POST-запросов:
(asdf:operate 'asdf:load-op :cl-who)
(asdf:operate 'asdf:load-op :restas)

(restas:defsite :restas.example-1
(:use :cl))

(in-package :restas.example-1)

(define-route main ("" :method :get)
(who:with-html-output-to-string (out)
(:html
(:body
((:form :method :post)
((:input :name "message"))
((:input :type "submit" :value "Send")))))))

(define-route main/post ("" :method :post)
(who:with-html-output-to-string (out)
(:html
(:body
(:div
(:b (who:fmt "test message: ~A"
(hunchentoot:post-parameter "message"))))
((:a :href (restas:genurl 'main)) "Try again")))))

(restas:start-site :restas.example-1 :port 8080)
После его выполнения и открытия страницы http://localhost:8080/ становится доступна элементарная форма, в которую предлагается ввести произвольное сообщение. После чего можно выполнить Send и получить новую страницу, содержащую введённое сообщение и ссылку с предположение повторить, ещё немного доработать и можно будет продавать :)

Данный пример отличается от предыдущего тем, что содержит уже целых два маршрута, один для обработки GET-запроса, а другой для обработки POST. Как видно, тип запроса, за который отвечает маршрут, можно указать с помощью ключа :method, по-умолчанию это :get. Наличие в имени маршрута 'main/post, отвечающего за обработку POST-запроса, суффикса "post" является чистым совпадением: маршруты могу именоваться произвольным образом, лишь бы каждый маршрут имел уникальное имя. Также, в данном коде для генерации html используется библиотека cl-who, которую я совершенно не рекомендую использовать для реальных приложений, но для небольших демонстраций это самое то.

Маршруты с параметрами

Шаблон url, указываемый в define-route не обязан быть столь простым, как в предыдущих примерах, и может содержать несколько параметров, которые являются доступными для кода-обработчика, указанного внутри define-route.
(asdf:operate 'asdf:load-op :cl-who)
(asdf:operate 'asdf:load-op :restas)

(restas:defsite :restas.example-2
(:use :cl :iter))

(in-package :restas.example-2)

(define-route root ("")
(who:with-html-output-to-string (out)
(:html
(:head
(:title "Example 2: Index"))
(:body
(:h1 "Index")
(:ul
(iter (for x from 1 to 10)
(who:htm (:li
((:a :href (genurl 'chapter-?.html :id x))
(who:fmt "Chapter ~A" x))))))))))

(define-route chapter-?.html ("chapter-:(id).html")
(who:with-html-output-to-string (out)
(:html
(:head
(:title (who:fmt "Example 2. Chapter ~A" id)))
(:body
(:h1 (who:fmt "Chapter ~A" id))
(:ul
(iter (for x from 1 to 10)
(who:htm (:li
((:a :href (genurl 'chapter-?-?.html :id1 id :id2 x))
(who:fmt "Chapter ~A-~A" id x))))))
((:a :href (genurl 'root))
"Back to Index")))))

(define-route chapter-?-?.html ("chapter-:(id1)-:(id2).html")
(who:with-html-output-to-string (out)
(:html
(:head
(:title (who:fmt "Example 2. Chapter ~A-~A" id1 id2)))
(:body
(:h1 (who:fmt "Chapter ~A-~A" id1 id2))
(:p (who:fmt "This is a chapter ~A-~A" id1 id2))
((:a :href (genurl 'chapter-?.html :id id1))
(who:fmt "Back to Chapter ~A" id1))))))

(restas:start-site :restas.example-2 :port 8080)
Я не знаю, какой из примеров является более глупым, но данный демонстрирует навигацию по некой "книге с оглавлением" и нравится мне меньше всего. С другой стороны, он позволят не только показать маршруты с параметрами, но также и генерацию ссылок на основе имени маршрутов.

Код приведённых примеров входит в поставку RESTAS и находиться в директории example.

6 комментариев:

  1. Хм, достаточно доходчиво. Вижу что создать свой сайт таким образом достаточно просто. Как раз скоро придется задумываться об этом. Обязательно сделаю себе зарубку на память и потренируюсь

    ОтветитьУдалить
  2. @LiteTabs
    Если что - обращайся, буду консультировать ;)

    ОтветитьУдалить
  3. я пытался первый пример запустить, но restas:defsite не объявлена

    ОтветитьУдалить
  4. @memnek
    Да, я писал в на днях, что провёл большу переработку, вместо defsite теперь надо использовать restas:define-module, а вместо start-site - restas:star. Кстати, этот код есть в директории examples пакета.

    ОтветитьУдалить
  5. у меня не получается установить restas. раньше он компилировался, но не запускался (не работал собственно hunchentoot).
    я использовал bordeaux-threads 0.7, который, судя по описанию в линуксе не работал. нужна была версия 0.6. я установил её, веб-сервер запускается, но вот restas перестал компилироваться (пишет, что не может скомпилировать module и route).
    как быть?

    ОтветитьУдалить
  6. @memnek

    Нассчёт версии bordeaux-thread не понял, у меня 0.7, а в остальном я не вижу что пишет SBLC (?) и не могу даже предположить в чём дело. И как вы устанавливаете? Лучше бы на lisper.ru тему соответствующую создать (там как раз есть пустой раздел по restas).

    ОтветитьУдалить