Введение

Это статья написана, чтобы иллюстрировать применение возможностей Common Lisp к типичным задачам веб-разработки.
Я постараюсь показать, как на лиспе реализовываются основные применяемые в веб-программировании вещи — шаблонизация, роутинг и кеширование. Также я оставил немножко места для макросов.
Статья в большой степени учебная, тем не менее это вполне работающий веб-сайт — rigidus.ru
Шаблоны и их компиляция
{namespace tpl} {template root} <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">{\n} <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">{\n} <head>{\n} <title>{$headtitle}</title>{\n} <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />{\n} <link rel="stylesheet" type="text/css" media="screen" href="/style.css" />{\n} <link rel="Shortcut Icon" type="image/x-icon" href="/favicon.ico" />{\n} </head>{\n} <body id="top">{\n} {$content | noAutoescape}{\n} </body>{\n} </html>{\n} {/template}
Этот простой шаблон с помощью библиотеки CL-CLOSURE-TEMPLATE на лету компилируется в машинный код функции root, находящейся в пакете tpl. Таким образом, применение данных к шаблону — это вызов скомпилированной функции:
(tpl:root (list :headtitle "Мой заголовок" :content "Hello world"))
Вместо «Hello world» можно подставить вызов функции, в которую компилируется другой шаблон — например шаблон «base», обеспечивающий минимальную сетку для сайта:
{template base} <div id="center"> <div class="col1 left"> <a id="logo" href="index.html"> <img src="http://www.gravatar.com/avatar/d8a986606b9d5e4769ba062779e95d9f?s=45" style="border: 1px solid #7F7F7F"/> </a> <ul id="nav"> {foreach $elt in $navpoints} {call navelt data="$elt" /} {/foreach} </ul> </div> {$content |noAutoescape} <div class="clear">.</div> </div> <div id="footer"> <p> <a href="/about">About</a> | <a href="/contacts">Contacts</a> </p> </div> {/template}
Теперь, воспользовавшись примером вводной статьи habrahabr.ru/blogs/webdev/111365 и нашим свежесозданным шаблоном, мы могли бы написать request-dispatcher для сайта из одной страницы так:
(defun request-dispatcher (request) (tpl:root (list :headtitle "My home page" :content (tpl:base (list :navpoints ..тут-меню.. :content ..тут-контент..)))))
Маршруты RESTAS
Библиотека RESTAS освобождает нас от увлекательного написания диспетчеров.
Теперь диспетчер будет создан на базе задаваемых нами маршрутов (routes), которые мы определяем вот так:
(restas:define-route main ("") (tpl:main (list :headtitle "My main page" :content "Hello! <a href=\"/articles\">Articles</a>"))) (restas:define-route css ("/css/:cssfile") (hunchentoot:handle-static-file (format nil "~a/css/~a" *base-dir* cssfile)))
— Что это за бред? — спросит искушенный веб-разработчик. — Это я же должен задавать для каждого css-файла свой маршрут?
— Вовсе нет! — отвечу я. Можно задавать лямбду :requirement, которая решит, подходит ли маршрут или нет. Вот обновленный код, который отдает файл, если находит его на диске в каталоге сайта:
(restas:define-route static ("/:staticfile" :requirement (lambda () (let ((request-file (pathname (format nil "~a/~a" *base-dir* (hunchentoot:request-uri hunchentoot:*request*)))) (files (directory (format nil "~a/*.*" *base-dir*)))) (not (null (find request-file files :test #'equal)))))) (hunchentoot:handle-static-file (format nil "~a/~a" *base-dir* staticfile)))
Здесь мы просто определили маршруты для главной страницы и для отдачи css-файлов — как видите можно использовать, :wildcards
Использование макросов
Я подготавливаваю статьи для сайта, используя org-mode — удобный режим Емакса, сочетающий простоту разметки (как вики) и различные удобные средства, вроде сворачивания разделов. Я написал функцию org-to-html,
которой передаю текст статьи в формате org-mode, а она автоматически строит мне html с заголовками, извлеченными из метаданнных, указанных прямо в статье, а также возвращает информацию о секциях и подсекциях.
После того, как эта функция обработает мой файл мне может понадобиться изменить некоторые заголовки и чтобы сохранить простоту вызова я использую макрос default-page:
(defmacro default-page (menu file-path &optional (body nil)) `(let ((menu-memo ,menu)) (multiple-value-bind (content sections directives) (org-to-html (alexandria:read-file-into-string ,file-path)) (let ((title (getf directives :title))) ,body (page title menu-memo (tpl:default (list :title title :navpoints menu-memo :sections (loop :for i :from 1 :for section :in sections :collect (list :anchor (format nil "anchor-~a" i) :level (format nil "level-~a" (car section)) :title (cadr section))) :content content)))))))
Теперь я могу не только избавиться от сложного вызова в клиентском коде, но и сделать «иньекцию» любого кода внутрь default-page, например так:
(restas:define-route about ("/about") (default-page (menu) (base-path "about.org") ;; Здесь я могу подсчитать кол-во секций (let ((cnt (length sections))) ;; И вывести их например в заголовкe (setf title (format nil "~a — ~a секций" title cnt)))))
В следующем разделе этот подход используется более осмысленно.
Кеширование
Статьи у меня лежат в файлах, содержащих метаинформацию: заголовки и категории. Чтобы построить страницу "/articles" я прохожу по файлам, что может требовать времени и загружать систему. Однако эти данные можно
запомнить в замыкании, что и делает вот такой код:
(let ((memo)) (restas:define-route articles ("/articles") (when (null memo) (setf memo (default-page (menu) (base-path "articles.org") (setf content ;; Здесь код, который собирает страницу ;; по файлам (я не стал его приводить) )))) memo))
Понятно, что если необходимо, чтобы кеш устаревал с течением времени — это тоже довольно несложно реализовать. Пока мне проще зайти в slime, сделать Ctrl+X, Ctrl+E на последней строчке этого кода и он будет выполнен заново, что приведет к обнулению кеша. Загружая новую статью (что бывает не
слишком часто) я так и делаю — это хороший повод тут же добавить еще какой-нибудь функционал.
Для интересующихся деталями:
Я разместил исходный код на github.com/rigidus/rigidus.ru
А сам сайт находится на rigidus.ru
Посмотрим, как он справится с хабраэффектом.
