Введение
Это статья написана, чтобы иллюстрировать применение возможностей 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
Посмотрим, как он справится с хабраэффектом.