18 January 2011

Пример сайта на Common Lisp

Website development

Введение





Это статья написана, чтобы иллюстрировать применение возможностей 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
Посмотрим, как он справится с хабраэффектом.
Tags:web common lisp sbcl restas
Hubs: Website development
+62
7.9k 65
Comments 131