Введение

Это статья написана, чтобы иллюстрировать применение возможностей Common Lisp языка к типичным задачам веб-разработки.

Я постараюсь показать, как на лиспе реализовываются основные применяемые в веб-программированиях вещи — шаблонизация, роутинг и кеширование. Также я оставил немножко места для макросов в веб-программировании.

Статья в большой степени учебная, тем не менее это вполне <a href="http://rigidus.ru/">работающий веб-сайт</a>.

Шаблоны и их препроцессинг

@code {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} @/code

Этот простой шаблон с помощью библиотеки CL-CLOSURE-TEMPLATE на лету компилируется в машинный код функции root, находящуюся в пакете tpl. Таким образом, применение данных к шаблону — это вызов скомпилированной по этому шаблону функции:

@code (tpl:root (list :headtitle "Мой заголовок"</code>

:content "Hello world")) @/code

Вместо "Hello world" можно подставить вызов функции, в которую компилируется другой шаблон — например шаблон "base", обеспечивающий минимальную сетку для сайта:

@code {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} @/code

Теперь, воспользовавшись примером из <a href="http://habrahabr.ru/blogs/webdev/111365/">вводной статьи</a> и нашим свежесозданным шаблоном, мы могли бы написать request-dispatcher для сайта из одной страницы так:

@code (defun request-dispatcher (request) (tpl:root (list :headtitle "My home page"

:content (tpl:base (list :navpoints ..тут-меню..

:content ..тут-контент..))))) @/code

Маршруты RESTAS

Библиотека RESTAS освобождает нас от увлекательного написания диспетчеров. Теперь диспетчер будет создан на базе задаваемых нами маршрутов (routes), которые мы определяем вот так:

@code (restas:define-module #:rigidus (:use :cl))

(in-package #:rigidus)

(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))) @/code

— Что это за бред? — спросит искушенный веб-разработчик. — Это я же должен задавать для каждого css-файла свой маршрут?

— Вовсе нет! — отвечу я. Для достижения максимального уровня гибкости можно задавать лямбду :requirement, которая решит, подходит ли маршрут или нет. Вот обновленный код, который отдает файл, если находит его на диске в каталоге сайта:

@code (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))) @/code

Здесь мы просто определили маршруты для главной страницы и для отдачи css-файлов - как видите можно использовать, :wildcards

=

Использование макросов

Я подготавливаваю статьи для сайта, используя org-mode - удобный режим Емакса, сочетающий простоту разметки (как вики) и различные удобные средства, вроде сворачивания разделов. Я написал функцию org-to-html, которой передаю текст статьи в формате org-mode, а она автоматически строит мне html с заголовками, извлеченными из метаданнных, указанных прямо в статье, а также возвращает информацию о секциях и подсекциях.

После того, как эта функция обработает мой файл мне может понадобиться изменить некоторые заголовки и чтобы сохранить простоту вызова я использую макрос default-page:

@code (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))))))) @/code

Теперь я могу не только избавиться от сложного вызова в клиентском коде, но и сделать "иньекцию" любого кода внутрь default-page, например так:

@code (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))))) @/code

В следующем разделе этот подход используется более осмысленно.

=

Кеширование

Статьи у меня лежат в файлах, содержащих метаинформацию: заголовки и категории. Чтобы построить страницу "/articles" я прохожу по файлам, что может требовать времени и загружать систему. Однако эти данные можно запомнить в замыкании, что и делает вот такой код:

@code (let ((memo)) (restas:define-route articles ("/articles") (when (null memo) (setf memo (default-page (menu) (base-path "articles.org") (setf content ;; Здесь код, который собирает страницу ;; по файлам (я не стал его приводить) )))) memo)) @/code

Понятно, что если необходимо, чтобы кеш устаревал с течением времени — это тоже довольно несложно реализовать. Пока мне проще зайти в slime, сделать Ctrl+X, Ctrl+E на последней строчке этого кода и он будет выполнен заново, что приведет к обнулению кеша. Загружая новую статью (что бывает не слишком часто) я так и делаю — это хороший повод тут же добавить еще какой-нибудь функционал.

Для интересующихся деталями: Я разместил исходный код на <a href="http://github.com/rigidus/rigidus.ru">http://github.com/rigidus/rigidus.ru</a> А сам сайт находится на <a href="http://rigidus.ru/">http://rigidus.ru/</a> Посмотрим, как он справится с хабраэффектом :)