1276 lines
51 KiB
HTML
1276 lines
51 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta name="generator" content=
|
||
"HTML Tidy for HTML5 for Linux version 5.2.0">
|
||
<title>Web development</title>
|
||
<meta charset="utf-8">
|
||
<meta name="description" content="A collection of examples of using Common Lisp">
|
||
<meta name="viewport" content=
|
||
"width=device-width, initial-scale=1">
|
||
<link rel="icon" href=
|
||
"assets/cl-logo-blue.png"/>
|
||
<link rel="stylesheet" href=
|
||
"assets/style.css">
|
||
<script type="text/javascript" src=
|
||
"assets/highlight-lisp.js">
|
||
</script>
|
||
<script type="text/javascript" src=
|
||
"assets/jquery-3.2.1.min.js">
|
||
</script>
|
||
<script type="text/javascript" src=
|
||
"assets/jquery.toc/jquery.toc.min.js">
|
||
</script>
|
||
<script type="text/javascript" src=
|
||
"assets/toggle-toc.js">
|
||
</script>
|
||
|
||
<link rel="stylesheet" href=
|
||
"assets/github.css">
|
||
|
||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
|
||
</head>
|
||
<body>
|
||
<h1 id="title-xs"><a href="index.html">The Common Lisp Cookbook</a> – Web development</h1>
|
||
<div id="logo-container">
|
||
<a href="index.html">
|
||
<img id="logo" src="assets/cl-logo-blue.png"/>
|
||
</a>
|
||
|
||
<div id="searchform-container">
|
||
<form onsubmit="duckSearch()" action="javascript:void(0)">
|
||
<input id="searchField" type="text" value="" placeholder="Search...">
|
||
</form>
|
||
</div>
|
||
|
||
<div id="toc-container" class="toc-close">
|
||
<div id="toc-title">Table of Contents</div>
|
||
<ul id="toc" class="list-unstyled"></ul>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="content-container">
|
||
<h1 id="title-non-xs"><a href="index.html">The Common Lisp Cookbook</a> – Web development</h1>
|
||
|
||
<!-- Announcement we can keep for 1 month or more. I remove it and re-add it from time to time. -->
|
||
<p class="announce">
|
||
📢 🤶 ⭐
|
||
<a style="font-size: 120%" href="https://www.udemy.com/course/common-lisp-programming/?couponCode=LISPY-XMAS2023" title="This course is under a paywall on the Udemy platform. Several videos are freely available so you can judge before diving in. vindarel is (I am) the main contributor to this Cookbook."> Discover our contributor's Lisp course with this Christmas coupon.</a>
|
||
<strong>
|
||
Recently added: 18 videos on MACROS.
|
||
</strong>
|
||
<a style="font-size: 90%" href="https://github.com/vindarel/common-lisp-course-in-videos/">Learn more</a>.
|
||
</p>
|
||
<p class="announce-neutral">
|
||
📕 <a href="index.html#download-in-epub">Get the EPUB and PDF</a>
|
||
</p>
|
||
|
||
|
||
<div id="content"
|
||
<p>For web development as for any other task, one can leverage Common Lisp’s
|
||
advantages: the unmatched REPL that even helps to interact with a running web
|
||
app, the exception handling system, performance, the ability to build a
|
||
self-contained executable, stability, good threads story, strong typing, etc. We
|
||
can, say, define a new route and try it right away, there is no need to restart
|
||
any running server. We can change and compile <em>one function at a time</em> (the
|
||
usual <code>C-c C-c</code> in Slime) and try it. The feedback is immediate. We can choose
|
||
the degree of interactivity: the web server can catch exceptions and fire the
|
||
interactive debugger, or print lisp backtraces on the browser, or display a 404
|
||
error page and print logs on standard output. The ability to build
|
||
self-contained executables eases deployment tremendously (compared to, for
|
||
example, npm-based apps), in that we just copy the executable to a server and
|
||
run it.</p>
|
||
|
||
<p>And when we have deployed our app, we can still interact with it,
|
||
allowing for hot reload, that even works when new dependencies have to
|
||
be installed. If you are careful and don’t want to use full live
|
||
reload, you might still enjoy this capability to reload, for example, a user’s
|
||
configuration file.</p>
|
||
|
||
<p>We’ll present here some established web frameworks and other common
|
||
libraries to help you getting started in developing a web
|
||
application. We do <em>not</em> aim to be exhaustive nor to replace the
|
||
upstream documentation. Your feedback and contributions are
|
||
appreciated.</p>
|
||
|
||
<!-- form creation, form validation -->
|
||
|
||
<!-- Javascript -->
|
||
|
||
<h2 id="overview">Overview</h2>
|
||
|
||
<p><a href="https://edicl.github.io/hunchentoot">Hunchentoot</a> and <a href="https://github.com/fukamachi/clack">Clack</a> are two projects that
|
||
you’ll often hear about.</p>
|
||
|
||
<p>Hunchentoot is</p>
|
||
|
||
<blockquote>
|
||
<p>a web server and at the same time a toolkit for building dynamic websites. As a stand-alone web server, Hunchentoot is capable of HTTP/1.1 chunking (both directions), persistent connections (keep-alive), and SSL. It provides facilities like automatic session handling (with and without cookies), logging, customizable error handling, and easy access to GET and POST parameters sent by the client.</p>
|
||
</blockquote>
|
||
|
||
<p>It is a software written by Edi Weitz (“Common Lisp Recipes”,
|
||
<code>cl-ppcre</code> and <a href="https://edicl.github.io/">much more</a>), it’s used and
|
||
proven solid. One can achieve a lot with it, but sometimes with more
|
||
friction than with a traditional web framework. For example,
|
||
dispatching a route by the HTTP method is a bit convoluted, one must
|
||
write a function for the <code>:uri</code> parameter that does the check, when it
|
||
is a built-in keyword in other frameworks like Caveman.</p>
|
||
|
||
<p>Clack is</p>
|
||
|
||
<blockquote>
|
||
<p>a web application environment for Common Lisp inspired by Python’s WSGI and Ruby’s Rack.</p>
|
||
</blockquote>
|
||
|
||
<p>Also written by a prolific lisper
|
||
(<a href="https://github.com/fukamachi/">E. Fukamachi</a>), it actually uses
|
||
Hunchentoot by default as the server, but thanks to its pluggable
|
||
architecture one can use another web server, like the asynchronous
|
||
<a href="https://github.com/fukamachi/woo">Woo</a>, built on the
|
||
<a href="http://software.schmorp.de/pkg/libev.html">libev</a> event loop, maybe
|
||
“the fastest web server written in any programming language”.</p>
|
||
|
||
<p>We’ll cite also <a href="https://github.com/orthecreedence/wookie">Wookie</a>, an asynchronous HTTP server, and its
|
||
companion library
|
||
<a href="https://github.com/orthecreedence/cl-async">cl-async</a>, for general
|
||
purpose, non-blocking programming in Common Lisp, built on libuv, the
|
||
backend library in Node.js.</p>
|
||
|
||
<p>Clack being more recent and less documented, and Hunchentoot a
|
||
de-facto standard, we’ll concentrate on the latter for this
|
||
recipe. Your contributions are of course welcome.</p>
|
||
|
||
<p>Web frameworks build upon web servers and can provide facilities for
|
||
common activities in web development, like a templating system, access
|
||
to a database, session management, or facilities to build a REST api.</p>
|
||
|
||
<p>Some web frameworks include:</p>
|
||
|
||
<ul>
|
||
<li><a href="https://github.com/fukamachi/caveman">Caveman</a>, by E. Fukamachi. It provides, out of the box,
|
||
database management, a templating engine (Djula), a project skeleton
|
||
generator, a routing system à la Flask or Sinatra, deployment options
|
||
(mod_lisp or FastCGI), support for Roswell on the command line, etc.</li>
|
||
<li><a href="https://github.com/Shirakumo/radiance">Radiance</a>, by <a href="https://github.com/Shinmera">Shinmera</a>
|
||
(Qtools, Portacle, lquery, …), is a web application environment,
|
||
more general than usual web frameworks. It lets us write and tie
|
||
websites and applications together, easing their deployment as a
|
||
whole. It has thorough <a href="https://shirakumo.github.io/radiance/">documentation</a>, a <a href="https://github.com/Shirakumo/radiance-tutorial">tutorial</a>, <a href="https://github.com/Shirakumo/radiance-contribs">modules</a>, <a href="https://github.com/Shirakumo?utf8=%E2%9C%93&q=radiance&type=&language=">pre-written applications</a> such as <a href="https://github.com/Shirakumo/purplish">an image board</a> or a <a href="https://github.com/Shirakumo/reader">blogging platform</a>, and more.
|
||
For example websites, see
|
||
<a href="https://shinmera.com/">https://shinmera.com/</a>,
|
||
<a href="https://reader.tymoon.eu/">reader.tymoon.eu</a> and <a href="https://events.tymoon.eu/">events.tymoon.eu</a>.</li>
|
||
<li><a href="https://github.com/joaotavora/snooze">Snooze</a>, by João Távora (Sly, Emacs’ Yasnippet, Eglot, …),
|
||
is “an URL router designed around REST web services”. It is
|
||
different because in Snooze, routes are just functions and HTTP
|
||
conditions are just Lisp conditions.</li>
|
||
<li><a href="https://github.com/mmontone/cl-rest-server">cl-rest-server</a> is a library for writing REST web
|
||
APIs. It features validation with schemas, annotations for logging,
|
||
caching, permissions or authentication, documentation via OpenAPI (Swagger),
|
||
etc.</li>
|
||
<li>last but not least, <a href="https://github.com/40ants/weblocks">Weblocks</a> is a venerable Common Lisp
|
||
web framework that permits to write ajax-based dynamic web
|
||
applications without writing any JavaScript, nor writing some lisp
|
||
that would transpile to JavaScript. It is seeing an extensive
|
||
rewrite and update since 2017. We present it in more details below.</li>
|
||
</ul>
|
||
|
||
<p>For a full list of libraries for the web, please see the <a href="https://github.com/CodyReichert/awesome-cl#network-and-internet">awesome-cl
|
||
list
|
||
#network-and-internet</a>
|
||
and <a href="https://www.cliki.net/Web">Cliki</a>. If you are looking for a
|
||
featureful static site generator, see
|
||
<a href="https://github.com/coleslaw-org/coleslaw">Coleslaw</a>.</p>
|
||
|
||
<h2 id="installation">Installation</h2>
|
||
|
||
<p>Let’s install the libraries we’ll use:</p>
|
||
|
||
<pre><code class="language-lisp">(ql:quickload '("hunchentoot" "caveman2" "spinneret"
|
||
"djula" "easy-routes"))
|
||
</code></pre>
|
||
|
||
<p>To try Weblocks, please see its documentation. The Weblocks in
|
||
Quicklisp is not yet, as of writing, the one we are interested in.</p>
|
||
|
||
<p>We’ll start by serving local files and we’ll run more than one local
|
||
server in the running image.</p>
|
||
|
||
<h2 id="simple-webserver">Simple webserver</h2>
|
||
|
||
<h3 id="serve-local-files">Serve local files</h3>
|
||
|
||
<h4 id="hunchentoot">Hunchentoot</h4>
|
||
|
||
<p>Create and start a webserver like this:</p>
|
||
|
||
<pre><code class="language-lisp">(defvar *acceptor* (make-instance 'hunchentoot:easy-acceptor
|
||
:port 4242))
|
||
(hunchentoot:start *acceptor*)
|
||
</code></pre>
|
||
|
||
<p>We create an instance of <code>easy-acceptor</code> on port 4242 and we start
|
||
it. We can now access <a href="http://127.0.0.1:4242/">http://127.0.0.1:4242/</a>. You should get a welcome
|
||
screen with a link to the documentation and logs to the console.</p>
|
||
|
||
<p>By default, Hunchentoot serves the files from the <code>www/</code> directory in
|
||
its source tree. Thus, if you go to the source of
|
||
<code>easy-acceptor</code> (<code>M-.</code> in Slime), which is probably
|
||
<code>~/quicklisp/dists/quicklisp/software/hunchentoot-v1.2.38/</code>, you’ll
|
||
find the <code>www/</code> directory. It contains:</p>
|
||
|
||
<ul>
|
||
<li>an <code>errors/</code> directory, with the error templates <code>404.html</code> and <code>500.html</code>,</li>
|
||
<li>an <code>img/</code> directory,</li>
|
||
<li>an <code>index.html</code> file.</li>
|
||
</ul>
|
||
|
||
<p>To serve another directory, we give the option <code>:document-root</code> to
|
||
<code>easy-acceptor</code>. We can also set the slot with its accessor:</p>
|
||
|
||
<pre><code class="language-lisp">(setf (hunchentoot:acceptor-document-root *acceptor*)
|
||
#p"path/to/www")
|
||
</code></pre>
|
||
|
||
<p>Let’s create our <code>index.html</code> first. Put this in a new
|
||
<code>www/index.html</code> at the current directory (of the lisp repl):</p>
|
||
|
||
<pre><code class="language-html"><html>
|
||
<head>
|
||
<title>Hello!</title>
|
||
</head>
|
||
<body>
|
||
<h1>Hello local server!</h1>
|
||
<p>
|
||
We just served our own files.
|
||
</p>
|
||
</body>
|
||
</html>
|
||
</code></pre>
|
||
|
||
<p>Let’s start a new acceptor on a new port:</p>
|
||
|
||
<pre><code class="language-lisp">(defvar *my-acceptor* (make-instance 'hunchentoot:easy-acceptor
|
||
:port 4444
|
||
:document-root #p"www/"))
|
||
(hunchentoot:start *my-acceptor*)
|
||
</code></pre>
|
||
|
||
<p>go to <a href="http://127.0.0.1:4444/">http://127.0.0.1:4444/</a> and see the difference.</p>
|
||
|
||
<p>Note that we just created another <em>acceptor</em> on a different port on
|
||
the same lisp image. This is already pretty cool.</p>
|
||
|
||
<h2 id="access-your-server-from-the-internet">Access your server from the internet</h2>
|
||
|
||
<h3 id="hunchentoot-1">Hunchentoot</h3>
|
||
|
||
<p>With Hunchentoot we have nothing to do, we can see the server from the
|
||
internet right away.</p>
|
||
|
||
<p>If you evaluate this on your VPS:</p>
|
||
|
||
<pre><code>(hunchentoot:start (make-instance 'hunchentoot:easy-acceptor :port 4242))
|
||
</code></pre>
|
||
|
||
<p>You can see it right away on your server’s IP.</p>
|
||
|
||
<p>Stop it with <code>(hunchentoot:stop *)</code>.</p>
|
||
|
||
<h2 id="routing">Routing</h2>
|
||
|
||
<h3 id="simple-routes">Simple routes</h3>
|
||
|
||
<h4 id="hunchentoot-2">Hunchentoot</h4>
|
||
|
||
<p>To bind an existing function to a route, we create a “prefix dispatch”
|
||
that we push onto the <code>*dispatch-table*</code> list:</p>
|
||
|
||
<pre><code class="language-lisp">(defun hello ()
|
||
(format nil "Hello, it works!"))
|
||
|
||
(push
|
||
(hunchentoot:create-prefix-dispatcher "/hello.html" #'hello)
|
||
hunchentoot:*dispatch-table*)
|
||
</code></pre>
|
||
|
||
<p>To create a route with a regexp, we use <code>create-regex-dispatcher</code>, where
|
||
the url-as-regexp can be a string, an s-expression or a cl-ppcre scanner.</p>
|
||
|
||
<p>If you didn’t yet, create an acceptor and start the server:</p>
|
||
|
||
<pre><code class="language-lisp">(defvar *server* (make-instance 'hunchentoot:easy-acceptor :port 4242))
|
||
(hunchentoot:start *server*)
|
||
</code></pre>
|
||
|
||
<p>and access it on <a href="http://localhost:4242/hello.html">http://localhost:4242/hello.html</a>.</p>
|
||
|
||
<p>We can see logs on the REPL:</p>
|
||
|
||
<pre><code>127.0.0.1 - [2018-10-27 23:50:09] "get / http/1.1" 200 393 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0"
|
||
127.0.0.1 - [2018-10-27 23:50:10] "get /img/made-with-lisp-logo.jpg http/1.1" 200 12583 "http://localhost:4242/" "Mozilla/5.0 (X11; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0"
|
||
127.0.0.1 - [2018-10-27 23:50:10] "get /favicon.ico http/1.1" 200 1406 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0"
|
||
127.0.0.1 - [2018-10-27 23:50:19] "get /hello.html http/1.1" 200 20 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0"
|
||
</code></pre>
|
||
|
||
<hr />
|
||
|
||
<p><a href="https://edicl.github.io/hunchentoot/#define-easy-handler">define-easy-handler</a> allows to create a function and to bind it to an uri at once.</p>
|
||
|
||
<p>Its form follows</p>
|
||
|
||
<pre><code>define-easy-handler (function-name :uri <uri> …) (lambda list parameters)
|
||
</code></pre>
|
||
|
||
<p>where <code><uri></code> can be a string or a function.</p>
|
||
|
||
<p>Example:</p>
|
||
|
||
<pre><code class="language-lisp">(hunchentoot:define-easy-handler (say-yo :uri "/yo") (name)
|
||
(setf (hunchentoot:content-type*) "text/plain")
|
||
(format nil "Hey~@[ ~A~]!" name))
|
||
</code></pre>
|
||
|
||
<p>Visit it at <a href="http://localhost:4242/yo">http://localhost:4242/yo</a> and add parameters on the url:
|
||
<a href="http://localhost:4242/yo?name=Alice">http://localhost:4242/yo?name=Alice</a>.</p>
|
||
|
||
<p>Just a thought… we didn’t explicitly ask Hunchentoot to add this
|
||
route to our first acceptor of the port 4242. Let’s try another acceptor (see
|
||
previous section), on port 4444: <a href="http://localhost:4444/yo?name=Bob">http://localhost:4444/yo?name=Bob</a> It
|
||
works too ! In fact, <code>define-easy-handler</code> accepts an <code>acceptor-names</code>
|
||
parameter:</p>
|
||
|
||
<blockquote>
|
||
<p>acceptor-names (which is evaluated) can be a list of symbols which means that the handler will only be returned by DISPATCH-EASY-HANDLERS in acceptors which have one of these names (see ACCEPTOR-NAME). acceptor-names can also be the symbol T which means that the handler will be returned by DISPATCH-EASY-HANDLERS in every acceptor.</p>
|
||
</blockquote>
|
||
|
||
<p>So, <code>define-easy-handler</code> has the following signature:</p>
|
||
|
||
<pre><code>define-easy-handler (function-name &key uri acceptor-names default-request-type) (lambda list parameters)
|
||
</code></pre>
|
||
|
||
<p>It also has a <code>default-parameter-type</code> which we’ll use in a minute to get url parameters.</p>
|
||
|
||
<p>There are also keys to know for the lambda list. Please see the documentation.</p>
|
||
|
||
<h4 id="easy-routes-hunchentoot">Easy-routes (Hunchentoot)</h4>
|
||
|
||
<p><a href="https://github.com/mmontone/easy-routes">easy-routes</a> is a route
|
||
handling extension on top of Hunchentoot. It provides:</p>
|
||
|
||
<ul>
|
||
<li><strong>dispatch</strong> based on the HTTP method, such as GET or POST (which is otherwise cumbersome to do in Hunchentoot)</li>
|
||
<li><strong>arguments extraction</strong> from the url path</li>
|
||
<li><strong>decorators</strong> (functions to run before the route body, typically used to add a layer of authentication or changing the returned content type)</li>
|
||
<li><strong>URL generation</strong> from route names and given URL parameters</li>
|
||
<li>visualization of routes</li>
|
||
<li>and more</li>
|
||
</ul>
|
||
|
||
<p>To use it, don’t create a server with <code>hunchentoot:easy-acceptor</code> but
|
||
with <code>easy-routes:easy-routes-acceptor</code>:</p>
|
||
|
||
<pre><code class="language-lisp">(setf *server* (make-instance 'easy-routes:easy-routes-acceptor))
|
||
</code></pre>
|
||
|
||
<p>Note: there is also <code>routes-acceptor</code>. The difference is that
|
||
<code>easy-routes-acceptor</code> iterates over Hunchentoot’s <code>*dispatch-table*</code>
|
||
if no route is found by <code>easy-routes</code>. That allows us, for example, to
|
||
serve static content the usual way with Hunchentoot.</p>
|
||
|
||
<p>Then define a route like this:</p>
|
||
|
||
<pre><code class="language-lisp">(easy-routes:defroute my-route-name ("/foo/:x" :method :get) (y &get z)
|
||
(format nil "x: ~a y: ~a z: ~a" x y z))
|
||
</code></pre>
|
||
|
||
<p>the route signature is made up of two parts:</p>
|
||
|
||
<pre><code>("/foo/:x" :method :get) (y &get z)
|
||
</code></pre>
|
||
|
||
<p>Here, <code>:x</code> captures the path parameter and binds it to the <code>x</code>
|
||
variable into the route body. <code>y</code> and <code>&get z</code> define URL parameters,
|
||
and we can have <code>&post</code> parameters to extract from the HTTP request
|
||
body.</p>
|
||
|
||
<p>These parameters can take an <code>:init-form</code> and <code>:parameter-type</code>
|
||
options as in <code>define-easy-handler</code>.</p>
|
||
|
||
<p>Now, imagine that we are deeper in our web application logic, and we
|
||
want to redirect our user to the route “/foo/3”. Instead of hardcoding
|
||
the URL, we can <strong>generate the URL from its name</strong>. Use
|
||
<code>easy-routes:genurl</code> like this:</p>
|
||
|
||
<pre><code class="language-lisp">(easy-routes:genurl my-route-name :id 3)
|
||
;; => /foo/3
|
||
|
||
(easy-routes:genurl my-route-name :id 3 :y "yay")
|
||
;; => /foo/3?y=yay
|
||
</code></pre>
|
||
|
||
<p><strong>Decorators</strong> are functions that are executed before the route body. They
|
||
should call the <code>next</code> parameter function to continue executing the
|
||
decoration chain and the route body finally. Examples:</p>
|
||
|
||
<pre><code class="language-lisp">(defun @auth (next)
|
||
(let ((*user* (hunchentoot:session-value 'user)))
|
||
(if (not *user*)
|
||
(hunchentoot:redirect "/login")
|
||
(funcall next))))
|
||
|
||
(defun @html (next)
|
||
(setf (hunchentoot:content-type*) "text/html")
|
||
(funcall next))
|
||
|
||
(defun @json (next)
|
||
(setf (hunchentoot:content-type*) "application/json")
|
||
(funcall next))
|
||
(defun @db (next)
|
||
(postmodern:with-connection *db-spec*
|
||
(funcall next)))
|
||
</code></pre>
|
||
|
||
<p>See <code>easy-routes</code>’ readme for more.</p>
|
||
|
||
<h4 id="caveman">Caveman</h4>
|
||
|
||
<p><a href="https://lispcookbook.github.io/cl-cookbook/caveman">Caveman</a> provides two ways to
|
||
define a route: the <code>defroute</code> macro and the <code>@route</code> pythonic
|
||
<em>annotation</em>:</p>
|
||
|
||
<pre><code class="language-lisp">(defroute "/welcome" (&key (|name| "Guest"))
|
||
(format nil "Welcome, ~A" |name|))
|
||
|
||
@route GET "/welcome"
|
||
(lambda (&key (|name| "Guest"))
|
||
(format nil "Welcome, ~A" |name|))
|
||
</code></pre>
|
||
|
||
<p>A route with an url parameter (note <code>:name</code> in the url):</p>
|
||
|
||
<pre><code class="language-lisp">(defroute "/hello/:name" (&key name)
|
||
(format nil "Hello, ~A" name))
|
||
</code></pre>
|
||
|
||
<p>It is also possible to define “wildcards” parameters. It works with
|
||
the <code>splat</code> key:</p>
|
||
|
||
<pre><code class="language-lisp">(defroute "/say/*/to/*" (&key splat)
|
||
; matches /say/hello/to/world
|
||
(format nil "~A" splat))
|
||
;=> (hello world)
|
||
</code></pre>
|
||
|
||
<p>We must enable regexps with <code>:regexp t</code>:</p>
|
||
|
||
<pre><code class="language-lisp">(defroute ("/hello/([\\w]+)" :regexp t) (&key captures)
|
||
(format nil "Hello, ~A!" (first captures)))
|
||
</code></pre>
|
||
|
||
<h3 id="accessing-get-and-post-parameters">Accessing GET and POST parameters</h3>
|
||
|
||
<h4 id="hunchentoot-3">Hunchentoot</h4>
|
||
|
||
<p>First of all, note that we can access query parameters anytime with</p>
|
||
|
||
<pre><code class="language-lisp">(hunchentoot:parameter "my-param")
|
||
</code></pre>
|
||
|
||
<p>It acts on the default <code>*request*</code> object which is passed to all handlers.</p>
|
||
|
||
<p>There is also <code>get-parameter</code> and <code>post-parameter</code>.</p>
|
||
|
||
<p>Earlier we saw some key parameters to <code>define-easy-handler</code>. We now
|
||
introduce <code>default-parameter-type</code>.</p>
|
||
|
||
<p>We defined the following handler:</p>
|
||
|
||
<pre><code class="language-lisp">(hunchentoot:define-easy-handler (say-yo :uri "/yo") (name)
|
||
(setf (hunchentoot:content-type*) "text/plain")
|
||
(format nil "Hey~@[ ~A~]!" name))
|
||
</code></pre>
|
||
|
||
<p>The variable <code>name</code> is a string by default. Let’s check it out:</p>
|
||
|
||
<pre><code class="language-lisp">(hunchentoot:define-easy-handler (say-yo :uri "/yo") (name)
|
||
(setf (hunchentoot:content-type*) "text/plain")
|
||
(format nil "Hey~@[ ~A~] you are of type ~a" name (type-of name)))
|
||
</code></pre>
|
||
|
||
<p>Going to <a href="http://localhost:4242/yo?name=Alice">http://localhost:4242/yo?name=Alice</a> returns</p>
|
||
|
||
<pre><code>Hey Alice you are of type (SIMPLE-ARRAY CHARACTER (5))
|
||
</code></pre>
|
||
|
||
<p>To automatically bind it to another type, we use <code>default-parameter-type</code>. It can be
|
||
one of those simple types:</p>
|
||
|
||
<ul>
|
||
<li><code>'string</code> (default),</li>
|
||
<li><code>'integer</code>,</li>
|
||
<li><code>'character</code> (accepting strings of length 1 only, otherwise it is nil)</li>
|
||
<li>or <code>'boolean</code></li>
|
||
</ul>
|
||
|
||
<p>or a compound list:</p>
|
||
|
||
<ul>
|
||
<li><code>'(:list <type>)</code></li>
|
||
<li><code>'(:array <type>)</code></li>
|
||
<li><code>'(:hash-table <type>)</code></li>
|
||
</ul>
|
||
|
||
<p>where <code><type></code> is a simple type.</p>
|
||
|
||
<h3 id="accessing-a-json-request-body">Accessing a JSON request body</h3>
|
||
|
||
<h4 id="hunchentoot-4">Hunchentoot</h4>
|
||
|
||
<p>To read a request body, use <code>hunchentoot:raw-post-data</code>, to which you
|
||
can add <code>:force-text t</code> to always get a string (and not a vector of
|
||
octets).</p>
|
||
|
||
<p>Then you can parse this string to JSON with the library of your choice (<a href="https://github.com/Zulu-Inuoe/jzon/">jzon</a>, <a href="https://github.com/yitzchak/shasht">shasht</a>…).</p>
|
||
|
||
<pre><code class="language-lisp">(easy-routes route-api-demo ("/api/:id/update" :method :post) ()
|
||
(let ((json (ignore-errors
|
||
(jzon:parse (hunchentoot:raw-post-data :force-text t)))))
|
||
(when json
|
||
…)))
|
||
</code></pre>
|
||
|
||
<!-- ## Sessions -->
|
||
|
||
<!-- todo ? -->
|
||
|
||
<!-- ## Cookies -->
|
||
|
||
<h2 id="error-handling">Error handling</h2>
|
||
|
||
<p>In all frameworks, we can choose the level of interactivity. The web
|
||
framework can return a 404 page and print output on the repl, it can
|
||
catch errors and invoke the interactive lisp debugger, or it can show
|
||
the lisp backtrace on the html page.</p>
|
||
|
||
<h3 id="hunchentoot-5">Hunchentoot</h3>
|
||
|
||
<p>The global variables to set to choose the error handling behaviour are:</p>
|
||
|
||
<ul>
|
||
<li><code>*catch-errors-p*</code>: set to <code>nil</code> if you want errors to be caught in
|
||
the interactive debugger (for development only, of course):</li>
|
||
</ul>
|
||
|
||
<pre><code class="language-lisp">(setf hunchentoot:*catch-errors-p* nil)
|
||
</code></pre>
|
||
|
||
<p>See also the generic function <code>maybe-invoke-debugger</code> if you want to
|
||
fine-tune this behaviour. You might want to specialize it on specific
|
||
condition classes (see below) for debugging purposes. The default method <a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_invoke.htm">invokes
|
||
the debugger</a>
|
||
if <code>*catch-errors-p*</code> is <code>nil</code>.</p>
|
||
|
||
<ul>
|
||
<li><code>*show-lisp-errors-p*</code>: set to <code>t</code> if you want to see errors in HTML output in the browser.</li>
|
||
<li><code>*show-lisp-backtraces-p*</code>: set to <code>nil</code> if the errors shown in HTML
|
||
output (when <code>*show-lisp-errors-p*</code> is <code>t</code>) should <em>not</em> contain
|
||
backtrace information (defaults to <code>t</code>, shows the backtrace).</li>
|
||
</ul>
|
||
|
||
<p>Hunchentoot defines condition classes. The superclass of all
|
||
conditions is <code>hunchentoot-condition</code>. The superclass of errors is <code>hunchentoot-error</code> (itself a subclass of <code>hunchentoot-condition</code>).</p>
|
||
|
||
<p>See the documentation: <a href="https://edicl.github.io/hunchentoot/#conditions">https://edicl.github.io/hunchentoot/#conditions</a>.</p>
|
||
|
||
<h3 id="clack">Clack</h3>
|
||
|
||
<p>Clack users might make a good use of plugins, like the clack-errors middleware: <a href="https://github.com/CodyReichert/awesome-cl#clack-plugins">https://github.com/CodyReichert/awesome-cl#clack-plugins</a>.</p>
|
||
|
||
<p><img src="assets/clack-errors.png" width="800" /></p>
|
||
|
||
<h2 id="weblocks---solving-the-javascript-problem">Weblocks - solving the “JavaScript problem”©</h2>
|
||
|
||
<p><a href="https://github.com/40ants/weblocks">Weblocks</a> is a widgets-based and
|
||
server-based framework with a built-in ajax update mechanism. It
|
||
allows to write dynamic web applications <em>without the need to write
|
||
JavaScript or to write lisp code that would transpile to JavaScript</em>.</p>
|
||
|
||
<p><img src="assets/weblocks-quickstart-check-task.gif" alt="" /></p>
|
||
|
||
<p>Weblocks is an old framework developed by Slava Akhmechet, Stephen
|
||
Compall and Leslie Polzer. After nine calm years, it is seeing a very
|
||
active update, refactoring and rewrite effort by Alexander Artemenko.</p>
|
||
|
||
<p>It was initially based on continuations (they were removed to date)
|
||
and thus a lispy cousin of Smalltalk’s
|
||
<a href="https://en.wikipedia.org/wiki/Seaside_(software)">Seaside</a>. We can
|
||
also relate it to Haskell’s Haste, OCaml’s Eliom,
|
||
Elixir’s Phoenix LiveView and others.</p>
|
||
|
||
<p>The <a href="http://ultralisp.org/">Ultralisp</a> website is an example Weblocks
|
||
website in production known in the CL community.</p>
|
||
|
||
<hr />
|
||
|
||
<p>Weblock’s unit of work is the <em>widget</em>. They look like a class definition:</p>
|
||
|
||
<pre><code class="language-lisp">(defwidget task ()
|
||
((title
|
||
:initarg :title
|
||
:accessor title)
|
||
(done
|
||
:initarg :done
|
||
:initform nil
|
||
:accessor done)))
|
||
</code></pre>
|
||
|
||
<p>Then all we have to do is to define the <code>render</code> method for this widget:</p>
|
||
|
||
<pre><code class="language-lisp">(defmethod render ((task task))
|
||
"Render a task."
|
||
(with-html
|
||
(:span (if (done task)
|
||
(with-html
|
||
(:s (title task)))
|
||
(title task)))))
|
||
</code></pre>
|
||
|
||
<p>It uses the Spinneret template engine by default, but we can bind any
|
||
other one of our choice.</p>
|
||
|
||
<p>To trigger an ajax event, we write lambdas in full Common Lisp:</p>
|
||
|
||
<pre><code class="language-lisp">...
|
||
(with-html
|
||
(:p (:input :type "checkbox"
|
||
:checked (done task)
|
||
:onclick (make-js-action
|
||
(lambda (&key &allow-other-keys)
|
||
(toggle task))))
|
||
...
|
||
</code></pre>
|
||
|
||
<p>The function <code>make-js-action</code> creates a simple javascript function
|
||
that calls the lisp one on the server, and automatically refreshes the
|
||
HTML of the widgets that need it. In our example, it re-renders one
|
||
task only.</p>
|
||
|
||
<p>Is it appealing ? Carry on this quickstart guide here: <a href="http://40ants.com/weblocks/quickstart.html">http://40ants.com/weblocks/quickstart.html</a>.</p>
|
||
|
||
<h2 id="templates">Templates</h2>
|
||
|
||
<h3 id="djula---html-markup">Djula - HTML markup</h3>
|
||
|
||
<p><a href="https://github.com/mmontone/djula">Djula</a> is a port of Python’s
|
||
Django template engine to Common Lisp. It has <a href="https://mmontone.github.io/djula/djula/">excellent documentation</a>.</p>
|
||
|
||
<p>Caveman uses it by default, but otherwise it is not difficult to
|
||
setup. We must declare where our templates are with something like</p>
|
||
|
||
<pre><code class="language-lisp">(djula:add-template-directory (asdf:system-relative-pathname "webapp" "templates/"))
|
||
</code></pre>
|
||
|
||
<p>and then we can declare and compile the ones we use, for example::</p>
|
||
|
||
<pre><code class="language-lisp">(defparameter +base.html+ (djula:compile-template* "base.html"))
|
||
(defparameter +welcome.html+ (djula:compile-template* "welcome.html"))
|
||
</code></pre>
|
||
|
||
<p>A Djula template looks like this (forgive the antislash in <code>{\%</code>, this
|
||
is a Jekyll limitation):</p>
|
||
|
||
<pre><code>{\% extends "base.html" \%}
|
||
{\% block title %}Memberlist{\% endblock \%}
|
||
{\% block content \%}
|
||
<ul>
|
||
{\% for user in users \%}
|
||
<li><a href=""></a></li>
|
||
{\% endfor \%}
|
||
</ul>
|
||
{\% endblock \%}
|
||
</code></pre>
|
||
|
||
<p>At last, to render the template, call <code>djula:render-template*</code> inside a route.</p>
|
||
|
||
<pre><code class="language-lisp">(easy-routes:defroute root ("/" :method :get) ()
|
||
(djula:render-template* +welcome.html+ nil
|
||
:users (get-users)
|
||
</code></pre>
|
||
|
||
<p>Note that for efficiency Djula compiles the templates before rendering them.</p>
|
||
|
||
<p>It is, along with its companion
|
||
<a href="https://github.com/AccelerationNet/access/">access</a> library, one of
|
||
the most downloaded libraries of Quicklisp.</p>
|
||
|
||
<h4 id="djula-filters">Djula filters</h4>
|
||
|
||
<p>Filters allow to modify how a variable is displayed. Djula comes with
|
||
a good set of built-in filters and they are <a href="https://mmontone.github.io/djula/doc/build/html/filters.html">well documented</a>. They are not to be confused with <a href="https://mmontone.github.io/djula/doc/build/html/tags.html">tags</a>.</p>
|
||
|
||
<p>They look like this: ``, where <code>lower</code> is an
|
||
existing filter, which renders the text into lowercase.</p>
|
||
|
||
<p>Filters sometimes take arguments. For example: `` calls
|
||
the <code>add</code> filter with arguments <code>value</code> and 2.</p>
|
||
|
||
<p>Moreover, it is very easy to define custom filters. All we have to do
|
||
is to use the <code>def-filter</code> macro, which takes the variable as first
|
||
argument, and which can take more optional arguments.</p>
|
||
|
||
<p>Its general form is:</p>
|
||
|
||
<pre><code class="language-lisp">(def-filter :myfilter-name (value arg) ;; arg is optional
|
||
(body))
|
||
</code></pre>
|
||
|
||
<p>and it is used like this: ``.</p>
|
||
|
||
<p>Here’s how the <code>add</code> filter is defined:</p>
|
||
|
||
<pre><code class="language-lisp">(def-filter :add (it n)
|
||
(+ it (parse-integer n)))
|
||
</code></pre>
|
||
|
||
<p>Once you have written a custom filter, you can use it right away
|
||
throughout the application.</p>
|
||
|
||
<p>Filters are very handy to move non-trivial formatting or logic from the
|
||
templates to the backend.</p>
|
||
|
||
<h3 id="spinneret---lispy-templates">Spinneret - lispy templates</h3>
|
||
|
||
<p><a href="https://github.com/ruricolist/spinneret">Spinneret</a> is a “lispy”
|
||
HTML5 generator. It looks like this:</p>
|
||
|
||
<pre><code class="language-lisp">(with-page (:title "Home page")
|
||
(:header
|
||
(:h1 "Home page"))
|
||
(:section
|
||
("~A, here is *your* shopping list: " *user-name*)
|
||
(:ol (dolist (item *shopping-list*)
|
||
(:li (1+ (random 10)) item))))
|
||
(:footer ("Last login: ~A" *last-login*)))
|
||
</code></pre>
|
||
|
||
<p>The author finds it is easier to compose the HTML in separate
|
||
functions and macros than with the more famous cl-who. But it
|
||
has more features under it sleeves:</p>
|
||
|
||
<ul>
|
||
<li>it warns on invalid tags and attributes</li>
|
||
<li>it can automatically number headers, given their depth</li>
|
||
<li>it pretty prints html per default, with control over line breaks</li>
|
||
<li>it understands embedded markdown</li>
|
||
<li>it can tell where in the document a generator function is (see <code>get-html-tag</code>)</li>
|
||
</ul>
|
||
|
||
<h2 id="serve-static-assets">Serve static assets</h2>
|
||
|
||
<h3 id="hunchentoot-6">Hunchentoot</h3>
|
||
|
||
<p>With Hunchentoot, use <code>create-folder-dispatcher-and-handler prefix directory</code>.</p>
|
||
|
||
<p>For example:</p>
|
||
|
||
<pre><code class="language-lisp">(push (hunchentoot:create-folder-dispatcher-and-handler
|
||
"/static/" (merge-pathnames
|
||
"src/static" ; <-- starts without a /
|
||
(asdf:system-source-directory :myproject)))
|
||
hunchentoot:*dispatch-table*)
|
||
</code></pre>
|
||
|
||
<p>Now our project’s static files located under
|
||
<code>/path/to/myproject/src/static/</code> are served with the <code>/static/</code> prefix:</p>
|
||
|
||
<pre><code class="language-html"><img src="/static/img/banner.jpg" />
|
||
</code></pre>
|
||
|
||
<h2 id="connecting-to-a-database">Connecting to a database</h2>
|
||
|
||
<p>Please see the <a href="databases.html">databases section</a>. The Mito ORM
|
||
supports SQLite3, PostgreSQL, MySQL, it has migrations and db schema
|
||
versioning, etc.</p>
|
||
|
||
<p>In Caveman, a database connection is alive during the Lisp session and is
|
||
reused in each HTTP requests.</p>
|
||
|
||
<h3 id="checking-a-user-is-logged-in">Checking a user is logged-in</h3>
|
||
|
||
<p>A framework will provide a way to work with sessions. We’ll create a
|
||
little macro to wrap our routes to check if the user is logged in.</p>
|
||
|
||
<p>In Caveman, <code>*session*</code> is a hash table that represents the session’s
|
||
data. Here are our login and logout functions:</p>
|
||
|
||
<pre><code class="language-lisp">(defun login (user)
|
||
"Log the user into the session"
|
||
(setf (gethash :user *session*) user))
|
||
|
||
(defun logout ()
|
||
"Log the user out of the session."
|
||
(setf (gethash :user *session*) nil))
|
||
</code></pre>
|
||
|
||
<p>We define a simple predicate:</p>
|
||
|
||
<pre><code class="language-lisp">(defun logged-in-p ()
|
||
(gethash :user cm:*session*))
|
||
</code></pre>
|
||
|
||
<p>and we define our <code>with-logged-in</code> macro:</p>
|
||
|
||
<pre><code class="language-lisp">(defmacro with-logged-in (&body body)
|
||
`(if (logged-in-p)
|
||
(progn ,@body)
|
||
(render #p"login.html"
|
||
'(:message "Please log-in to access this page."))))
|
||
</code></pre>
|
||
|
||
<p>If the user isn’t logged in, there will nothing in the session store,
|
||
and we render the login page. When all is well, we execute the macro’s
|
||
body. We use it like this:</p>
|
||
|
||
<pre><code class="language-lisp">(defroute "/account/logout" ()
|
||
"Show the log-out page, only if the user is logged in."
|
||
(with-logged-in
|
||
(logout)
|
||
(render #p"logout.html")))
|
||
|
||
(defroute ("/account/review" :method :get) ()
|
||
(with-logged-in
|
||
(render #p"review.html"
|
||
(list :review (get-review (gethash :user *session*))))))
|
||
</code></pre>
|
||
|
||
<p>and so on.</p>
|
||
|
||
<h3 id="encrypting-passwords">Encrypting passwords</h3>
|
||
|
||
<h4 id="with-cl-pass">With cl-pass</h4>
|
||
|
||
<p><a href="https://github.com/eudoxia0/cl-pass">cl-pass</a> is a password hashing and verification library. It is as simple to use as this:</p>
|
||
|
||
<pre><code class="language-lisp">(cl-pass:hash "test")
|
||
;; "PBKDF2$sha256:20000$5cf6ee792cdf05e1ba2b6325c41a5f10$19c7f2ccb3880716bf7cdf999b3ed99e07c7a8140bab37af2afdc28d8806e854"
|
||
(cl-pass:check-password "test" *)
|
||
;; t
|
||
(cl-pass:check-password "nope" **)
|
||
;; nil
|
||
</code></pre>
|
||
|
||
<p>You might also want to look at
|
||
<a href="https://github.com/eudoxia0/hermetic">hermetic</a>, a simple
|
||
authentication system for Clack-based applications.</p>
|
||
|
||
<h4 id="manually-with-ironclad">Manually (with Ironclad)</h4>
|
||
|
||
<p>In this recipe we do the encryption and verification ourselves. We use the de-facto standard
|
||
<a href="https://github.com/froydnj/ironclad">Ironclad</a> cryptographic toolkit
|
||
and the <a href="https://github.com/cl-babel/babel">Babel</a> charset
|
||
encoding/decoding library.</p>
|
||
|
||
<p>The following snippet creates the password hash that should be stored in your
|
||
database. Note that Ironclad expects a byte-vector, not a string.</p>
|
||
|
||
<pre><code class="language-lisp">(defun password-hash (password)
|
||
(ironclad:pbkdf2-hash-password-to-combined-string
|
||
(babel:string-to-octets password)))
|
||
</code></pre>
|
||
|
||
<p><code>pbkdf2</code> is defined in <a href="https://tools.ietf.org/html/rfc2898">RFC2898</a>.
|
||
It uses a pseudorandom function to derive a secure encryption key
|
||
based on the password.</p>
|
||
|
||
<p>The following function checks if a user is active and verifies the
|
||
entered password. It returns the user-id if active and verified and
|
||
nil in all other cases even if an error occurs. Adapt it to your
|
||
application.</p>
|
||
|
||
<pre><code class="language-lisp">(defun check-user-password (user password)
|
||
(handler-case
|
||
(let* ((data (my-get-user-data user))
|
||
(hash (my-get-user-hash data))
|
||
(active (my-get-user-active data)))
|
||
(when (and active (ironclad:pbkdf2-check-password (babel:string-to-octets password)
|
||
hash))
|
||
(my-get-user-id data)))
|
||
(condition () nil)))
|
||
</code></pre>
|
||
|
||
<p>And the following is an example on how to set the password on the
|
||
database. Note that we use <code>(password-hash password)</code> to save the
|
||
password. The rest is specific to the web framework and to the DB
|
||
library.</p>
|
||
|
||
<pre><code class="language-lisp">(defun set-password (user password)
|
||
(with-connection (db)
|
||
(execute
|
||
(make-statement :update :web_user
|
||
(set= :hash (password-hash password))
|
||
(make-clause :where
|
||
(make-op := (if (integerp user)
|
||
:id_user
|
||
:email)
|
||
user))))))
|
||
</code></pre>
|
||
|
||
<p><em>Credit: <code>/u/arvid</code> on <a href="https://www.reddit.com/r/learnlisp/comments/begcf9/can_someone_give_me_an_eli5_on_hiw_to_encrypt_and/">/r/learnlisp</a></em>.</p>
|
||
|
||
<h2 id="runnning-and-building">Runnning and building</h2>
|
||
|
||
<h3 id="running-the-application-from-source">Running the application from source</h3>
|
||
|
||
<p>To run our Lisp code from source, as a script, we can use the <code>--load</code>
|
||
switch from our implementation.</p>
|
||
|
||
<p>We must ensure:</p>
|
||
|
||
<ul>
|
||
<li>to load the project’s .asd system declaration (if any)</li>
|
||
<li>to install the required dependencies (this demands we have installed Quicklisp previously)</li>
|
||
<li>and to run our application’s entry point.</li>
|
||
</ul>
|
||
|
||
<p>We could use such commands:</p>
|
||
|
||
<pre><code class="language-lisp">;; run.lisp
|
||
|
||
(load "myproject.asd")
|
||
|
||
(ql:quickload "myproject")
|
||
|
||
(in-package :myproject)
|
||
(handler-case
|
||
;; The START function starts the web server.
|
||
(myproject::start :port (ignore-errors
|
||
(parse-integer
|
||
(uiop:getenv "PROJECT_PORT"))))
|
||
(error (c)
|
||
(format *error-output* "~&An error occured: ~a~&" c)
|
||
(uiop:quit 1)))
|
||
</code></pre>
|
||
|
||
<p>In addition we have allowed the user to set the application’s port
|
||
with an environment variable.</p>
|
||
|
||
<p>We can run the file like so:</p>
|
||
|
||
<pre><code>sbcl --load run.lisp
|
||
</code></pre>
|
||
|
||
<p>After loading the project, the web server is started in the
|
||
background. We are offered the usual Lisp REPL, from which we can
|
||
interact with the running application.</p>
|
||
|
||
<p>We can also connect to the running application from our preferred
|
||
editor, from home, and compile the changes in our editor to the
|
||
running instance. See the following section
|
||
<a href="web.html#connecting-to-a-remote-lisp-image">#connecting-to-a-remote-lisp-image</a>.</p>
|
||
|
||
<h3 id="building-a-self-contained-executable">Building a self-contained executable</h3>
|
||
|
||
<p>As for all Common Lisp applications, we can bundle our web app in one
|
||
single executable, including the assets. It makes deployment very
|
||
easy: copy it to your server and run it.</p>
|
||
|
||
<pre><code>$ ./my-web-app
|
||
Hunchentoot server is started.
|
||
Listening on localhost:9003.
|
||
</code></pre>
|
||
|
||
<p>See this recipe on <a href="scripting.html#for-web-apps">scripting#for-web-apps</a>.</p>
|
||
|
||
<h3 id="continuous-delivery-with-travis-ci-or-gitlab-ci">Continuous delivery with Travis CI or Gitlab CI</h3>
|
||
|
||
<p>Please see the section on <a href="testing.html#continuous-integration">testing#continuous-integration</a>.</p>
|
||
|
||
<h3 id="multi-platform-delivery-with-electron">Multi-platform delivery with Electron</h3>
|
||
|
||
<p><a href="https://ceramic.github.io/">Ceramic</a> makes all the work for us.</p>
|
||
|
||
<p>It is as simple as this:</p>
|
||
|
||
<pre><code class="language-lisp">;; Load Ceramic and our app
|
||
(ql:quickload '(:ceramic :our-app))
|
||
|
||
;; Ensure Ceramic is set up
|
||
(ceramic:setup)
|
||
(ceramic:interactive)
|
||
|
||
;; Start our app (here based on the Lucerne framework)
|
||
(lucerne:start our-app.views:app :port 8000)
|
||
|
||
;; Open a browser window to it
|
||
(defvar window (ceramic:make-window :url "http://localhost:8000/"))
|
||
|
||
;; start Ceramic
|
||
(ceramic:show-window window)
|
||
</code></pre>
|
||
|
||
<p>and we can ship this on Linux, Mac and Windows.</p>
|
||
|
||
<p>There is more:</p>
|
||
|
||
<blockquote>
|
||
<p>Ceramic applications are compiled down to native code, ensuring both performance and enabling you to deliver closed-source, commercial applications.</p>
|
||
</blockquote>
|
||
|
||
<p>Thus, no need to minify our JS.</p>
|
||
|
||
<h2 id="deployment">Deployment</h2>
|
||
|
||
<h3 id="deploying-manually">Deploying manually</h3>
|
||
|
||
<p>We can start our executable in a shell and send it to the background (<code>C-z bg</code>), or run it inside a <code>tmux</code> session. These are not the best but hey, it works©.</p>
|
||
|
||
<h3 id="systemd-daemonizing-restarting-in-case-of-crashes-handling-logs">Systemd: Daemonizing, restarting in case of crashes, handling logs</h3>
|
||
|
||
<p>This is actually a system-specific task. See how to do that on your system.</p>
|
||
|
||
<p>Most GNU/Linux distros now come with Systemd, so here’s a little example.</p>
|
||
|
||
<p>Deploying an app with Systemd is as simple as writing a configuration file:</p>
|
||
|
||
<pre><code>$ sudo emacs -nw /etc/systemd/system/my-app.service
|
||
[Unit]
|
||
Description=your lisp app on systemd example
|
||
|
||
[Service]
|
||
WorkingDirectory=/path/to/your/project/directory/
|
||
ExecStart=/usr/bin/make run # or anything
|
||
Type=simple
|
||
Restart=on-failure
|
||
|
||
[Install]
|
||
WantedBy=network.target
|
||
</code></pre>
|
||
|
||
<p>Then we have a command to <code>start</code> it, only now:</p>
|
||
|
||
<pre><code>sudo systemctl start my-app.service
|
||
</code></pre>
|
||
|
||
<p>and a command to install the service, to <strong>start the app after a boot
|
||
or reboot</strong> (that’s the “[Install]” part):</p>
|
||
|
||
<pre><code>sudo systemctl enable my-app.service
|
||
</code></pre>
|
||
|
||
<p>Then we can check its <code>status</code>:</p>
|
||
|
||
<pre><code>systemctl status my-app.service
|
||
</code></pre>
|
||
|
||
<p>and see our application’s <strong>logs</strong> (we can write to stdout or stderr,
|
||
and Systemd handles the logging):</p>
|
||
|
||
<pre><code>journalctl -u my-app.service
|
||
</code></pre>
|
||
|
||
<p>(you can also use the <code>-f</code> option to see log updates in real time, and in that case augment the number of lines with <code>-n 50</code> or <code>--lines</code>).</p>
|
||
|
||
<p>Systemd handles crashes and <strong>restarts the application</strong>. That’s the <code>Restart=on-failure</code> line.</p>
|
||
|
||
<p>Now keep in mind a couple things:</p>
|
||
|
||
<ul>
|
||
<li>we want our app to crash so that it can be re-started automatically:
|
||
you’ll want the <code>--disable-debugger</code> flag with SBCL.</li>
|
||
<li>Systemd will, by default, run your app as root. If you rely on your
|
||
Lisp to read your startup file (<code>~/.sbclrc</code>), especially to setup
|
||
Quicklisp, you will need to use the <code>--userinit</code> flag, or to set the
|
||
Systemd user with <code>User=xyz</code> in the <code>[service]</code> section. And if you
|
||
use a startup file, be aware that the line <code>(user-homedir-pathname)</code>
|
||
will not return the same result depending on the user, so the snippet
|
||
might not find Quicklisp’s setup.lisp file.</li>
|
||
</ul>
|
||
|
||
<p>See more: <a href="https://www.freedesktop.org/software/systemd/man/systemd.service.html">https://www.freedesktop.org/software/systemd/man/systemd.service.html</a>.</p>
|
||
|
||
<h3 id="with-docker">With Docker</h3>
|
||
|
||
<p>There are several Docker images for Common
|
||
Lisp. For example:</p>
|
||
|
||
<ul>
|
||
<li><a href="https://hub.docker.com/r/clfoundation/sbcl/">clfoundation/sbcl</a>
|
||
includes the latest version of SBCL, many OS packages useful for CI
|
||
purposes, and a script to install Quicklisp.</li>
|
||
<li><a href="https://github.com/40ants/base-lisp-image">40ants/base-lisp-image</a>
|
||
is based on Ubuntu LTS and includes SBCL, CCL, Quicklisp, Qlot and
|
||
Roswell.</li>
|
||
<li><a href="https://github.com/container-lisp/s2i-lisp">container-lisp/s2i-lisp</a>
|
||
is CentOs based and contains the source for building a Quicklisp based
|
||
Common Lisp application as a reproducible docker image using OpenShift’s
|
||
source-to-image.</li>
|
||
</ul>
|
||
|
||
<h3 id="with-guix">With Guix</h3>
|
||
|
||
<p><a href="https://www.gnu.org/software/guix/">GNU Guix</a> is a transactional
|
||
package manager, that can be installed on top of an existing OS, and a
|
||
whole distro that supports declarative system configuration. It allows
|
||
to ship self-contained tarballs, which also contain system
|
||
dependencies. For an example, see the <a href="https://github.com/atlas-engineer/nyxt/">Nyxt browser</a>.</p>
|
||
|
||
<h3 id="running-behind-nginx">Running behind Nginx</h3>
|
||
|
||
<p>There is nothing CL-specific to run your Lisp web app behind Nginx. Here’s an example to get you started.</p>
|
||
|
||
<p>We suppose you are running your Lisp app on a web server, with the IP
|
||
address 1.2.3.4, on the port 8001. Nothing special here. We want to
|
||
access our app with a real domain name (and eventuall benefit of other
|
||
Nginx’s advantages, such as rate limiting etc). We bought our domain
|
||
name and we created a DNS record of type A that links the domain name
|
||
to the server’s IP address.</p>
|
||
|
||
<p>We must configure our server with Nginx to tell it that all
|
||
connections coming from “your-domain-name.org”, on port 80, are to be
|
||
sent to the Lisp app running locally.</p>
|
||
|
||
<p>Create a new file: <code>/etc/nginx/sites-enabled/my-lisp-app.conf</code> and add this proxy directive:</p>
|
||
|
||
<pre><code class="language-lisp">server {
|
||
listen www.your-domain-name.org:80;
|
||
server_name your-domain-name.org www.your-domain-name.org; # with and without www
|
||
location / {
|
||
proxy_pass http://1.2.3.4:8001/;
|
||
}
|
||
|
||
# Optional: serve static files with nginx, not the Lisp app.
|
||
location /files/ {
|
||
proxy_pass http://1.2.3.4:8001/files/;
|
||
}
|
||
}
|
||
</code></pre>
|
||
|
||
<p>Note that on the proxy_pass directive: <code>proxy_pass
|
||
http://1.2.3.4:8001/;</code> we are using our server’s public IP
|
||
address. Oten, your Lisp webserver such as Hunchentoot directly
|
||
listens on it. You might want, for security reasons, to run the Lisp
|
||
app on localhost.</p>
|
||
|
||
<p>Reload nginx (send the “reload” signal):</p>
|
||
|
||
<pre><code>$ nginx -s reload
|
||
</code></pre>
|
||
|
||
<p>and that’s it: you can access your Lisp app from the outside through <code>http://www.your-domain-name.org</code>.</p>
|
||
|
||
<h3 id="deploying-on-heroku-and-other-services">Deploying on Heroku and other services</h3>
|
||
|
||
<p>See <a href="https://gitlab.com/duncan-bayne/heroku-buildpack-common-lisp">heroku-buildpack-common-lisp</a> and the <a href="https://github.com/CodyReichert/awesome-cl#deployment">Awesome CL#deploy</a> section for interface libraries for Kubernetes, OpenShift, AWS, etc.</p>
|
||
|
||
<h2 id="monitoring">Monitoring</h2>
|
||
|
||
<p>See <a href="https://github.com/deadtrickster/prometheus.cl">Prometheus.cl</a>
|
||
for a Grafana dashboard for SBCL and Hunchentoot metrics (memory,
|
||
threads, requests per second,…).</p>
|
||
|
||
<h2 id="connecting-to-a-remote-lisp-image">Connecting to a remote Lisp image</h2>
|
||
|
||
<p>This this section: <a href="debugging.html#remote-debugging">debugging#remote-debugging</a>.</p>
|
||
|
||
<h2 id="hot-reload">Hot reload</h2>
|
||
|
||
<p>This is an example from <a href="https://github.com/stylewarning/quickutil/blob/master/quickutil-server/">Quickutil</a>. It is actually an automated version of the precedent section.</p>
|
||
|
||
<p>It has a Makefile target:</p>
|
||
|
||
<pre><code class="language-lisp">hot_deploy:
|
||
$(call $(LISP), \
|
||
(ql:quickload :quickutil-server) (ql:quickload :swank-client), \
|
||
(swank-client:with-slime-connection (conn "localhost" $(SWANK_PORT)) \
|
||
(swank-client:slime-eval (quote (handler-bind ((error (function continue))) \
|
||
(ql:quickload :quickutil-utilities) (ql:quickload :quickutil-server) \
|
||
(funcall (symbol-function (intern "STOP" :quickutil-server))) \
|
||
(funcall (symbol-function (intern "START" :quickutil-server)) $(start_args)))) conn)) \
|
||
$($(LISP)-quit))
|
||
</code></pre>
|
||
|
||
<p>It has to be run on the server (a simple fabfile command can call this
|
||
through ssh). Beforehand, a <code>fab update</code> has run <code>git pull</code> on the
|
||
server, so new code is present but not running. It connects to the
|
||
local swank server, loads the new code, stops and starts the app in a
|
||
row.</p>
|
||
|
||
<h2 id="see-also">See also</h2>
|
||
|
||
<ul>
|
||
<li><a href="https://hg.sr.ht/~wnortje/feather">Feather</a>, a template for web
|
||
application development, shows a functioning Hello World app
|
||
with an HTML page, a JSON API, a passing test suite, a Postgres DB
|
||
and DB migrations. Uses Qlot, Buildapp, SystemD for deployment.</li>
|
||
<li><a href="https://github.com/vindarel/lisp-web-template-productlist">lisp-web-template-productlist</a>,
|
||
a simple project template with Hunchentoot, Easy-Routes, Djula and Bulma CSS.</li>
|
||
<li><a href="https://github.com/vindarel/lisp-web-live-reload-example/">lisp-web-live-reload-example</a> -
|
||
a toy project to show how to interact with a running web app.</li>
|
||
</ul>
|
||
|
||
<h2 id="credits">Credits</h2>
|
||
|
||
<ul>
|
||
<li><a href="https://lisp-journey.gitlab.io/web-dev/">https://lisp-journey.gitlab.io/web-dev/</a></li>
|
||
</ul>
|
||
|
||
|
||
|
||
<p class="page-source">
|
||
Page source: <a href="https://github.com/LispCookbook/cl-cookbook/blob/master/web.md">web.md</a>
|
||
</p>
|
||
</div>
|
||
|
||
<script type="text/javascript">
|
||
|
||
// Don't write the TOC on the index.
|
||
if (window.location.pathname != "/cl-cookbook/") {
|
||
$("#toc").toc({
|
||
content: "#content", // will ignore the first h1 with the site+page title.
|
||
headings: "h1,h2,h3,h4"});
|
||
}
|
||
|
||
$("#two-cols + ul").css({
|
||
"column-count": "2",
|
||
});
|
||
$("#contributors + ul").css({
|
||
"column-count": "4",
|
||
});
|
||
</script>
|
||
|
||
|
||
|
||
<div>
|
||
<footer class="footer">
|
||
<hr/>
|
||
© 2002–2023 the Common Lisp Cookbook Project
|
||
<div>
|
||
📹 Discover <a style="color: darkgrey; text-decoration: underline", href="https://www.udemy.com/course/common-lisp-programming/?referralCode=2F3D698BBC4326F94358">vindarel's Lisp course on Udemy</a>
|
||
</div>
|
||
</footer>
|
||
|
||
</div>
|
||
<div id="toc-btn">T<br>O<br>C</div>
|
||
</div>
|
||
|
||
<script text="javascript">
|
||
HighlightLisp.highlight_auto({className: null});
|
||
</script>
|
||
|
||
<script type="text/javascript">
|
||
function duckSearch() {
|
||
var searchField = document.getElementById("searchField");
|
||
if (searchField && searchField.value) {
|
||
var query = escape("site:lispcookbook.github.io/cl-cookbook/ " + searchField.value);
|
||
window.location.href = "https://duckduckgo.com/?kj=b2&kf=-1&ko=1&q=" + query;
|
||
// https://duckduckgo.com/params
|
||
// kj=b2: blue header in results page
|
||
// kf=-1: no favicons
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<script async defer data-domain="lispcookbook.github.io/cl-cookbook" src="https://plausible.io/js/plausible.js"></script>
|
||
|
||
</body>
|
||
</html>
|