The Common Lisp Cookbook – Web 開発

Table of Contents

The Common Lisp Cookbook – Web 開発

📢 🎓 ⭐ Learn Common Lisp efficiently in videos, by the Cookbook's main contributor. Learn more.

🖊️ Discover a new Common Lisp and Coalton editor for beginners: mine and a new VSCode extension for Common Lisp: OLIVE.

Web 開発でも他の作業でも、Common Lisp の利点を活かせます。比類のない REPL は、動いている Web アプリと対話するのにも役立ちます。例外処理、性能、自己完結型実行ファイルを作れること、安定性、スレッドの扱い、強い型付けなどもあります。たとえば新しい route を定義してすぐ試せます。動いている server を再起動する必要はありません。1 関数ずつ 変更してコンパイルし(Slime ならおなじみの C-c C-c)、試せます。フィードバックは即座です。対話性の度合いも選べます。Web サーバーに例外処理を任せず対話デバッガを起動させることも、例外を処理して browser に Lisp の backtrace を出すことも、404 エラーページを表示し標準出力に log を出すこともできます。自己完結型実行ファイルを作れるので、たとえば npm ベースのアプリに比べて deployment は非常に楽です。実行ファイルを server にコピーして実行するだけだからです。

アプリを deployment したあとでも、引き続き対話できます。依存関係のインストールが必要なときでも hot reload が使えます。完全な live reload は使いたくない慎重な場面でも、たとえば利用者の configuration file を reload する、といった用途にはこの能力が役立ちます。

ここでは、Web アプリ開発を始める助けとして、実績のある Web framework と一般的なライブラリを紹介します。網羅を目指してはいませんし、上流のドキュメントの代わりにもなりません。フィードバックや貢献は歓迎します。

INFO: Common Lisp の Web 開発に特化した新しいサイトがあります: Web Apps in Lisp, Know-how (sources).

概要

HunchentootClack は、よく耳にする 2 つのプロジェクトです。

Hunchentoot は次のようなものです。

動的な website を構築するための toolkit であり、同時に web server でもあります。単体の web server として、Hunchentoot は HTTP/1.1 の chunking(両方向)、persistent connection(keep-alive)、SSL に対応しています。自動 session 管理(cookie あり/なし)、logging、カスタマイズ可能な error handling、client が送る GET/POST parameter への簡単なアクセスなども提供します。

これは Edi Weitz によるソフトウェアです(”Common Lisp Recipes”、cl-ppcre、そして そのほか多数)。実績があり、堅牢です。これだけで多くのことができますが、従来型の Web framework より摩擦が大きいこともあります。たとえば HTTP method で route を振り分けるのは少し面倒で、Caveman のような他の framework では組み込みの keyword で済むところを、:uri 引数のための関数を書いて判定しなければなりません。

Clack は次のようなものです。

Python の WSGI と Ruby の Rack に触発された、Common Lisp 向けの web application environment です。

こちらも多作な lisper である E. Fukamachi によるものです。実際には既定の server として Hunchentoot を使いますが、差し替え可能な architecture により、非同期の Woo のような別の web server も使えます。Woo は libev の event loop 上に構築されており、おそらく “あらゆる programming language の中で最速の web server” でしょう。

さらに、非同期 HTTP server の Wookie と、その companion library である cl-async もあります。cl-async は、Node.js の backend library である libuv 上で動く、Common Lisp の汎用 non-blocking programming 向けライブラリです。

Clack は比較的新しくドキュメントも少なめで、Hunchentoot は事実上の標準です。そのため、このレシピでは後者に絞ります。もちろん貢献は歓迎です。

Web framework は web server の上に成り立ち、templating system、database へのアクセス、session management、REST api を組み立てる仕組みなど、Web 開発でよく使う機能を提供できます。

いくつかの web framework を挙げます。

Web 向けライブラリの完全な一覧は、awesome-cl の #network-and-internetCliki を参照してください。多機能な静的サイト generator を探しているなら Coleslaw を見てください。

インストール

使うライブラリをインストールします。

(ql:quickload '("hunchentoot" "caveman2" "spinneret"
                "djula" "easy-routes"))

Weblocks を試すには、そのドキュメントを参照してください。執筆時点の Quicklisp の Weblocks は、ここで扱いたいものではまだありません。

まずはローカルファイルを配信し、実行中のイメージで複数の local server を動かします。

シンプルな webserver

ローカルファイルを配信する

Hunchentoot

次のように webserver を作成して起動します。

(defvar *acceptor* (make-instance 'hunchentoot:easy-acceptor
                                  :port 4242))
(hunchentoot:start *acceptor*)

port 4242 に easy-acceptor のインスタンスを作って起動しています。これで http://127.0.0.1:4242/ にアクセスできます。ドキュメントへのリンク付きの welcome 画面が出て、console に log が出るはずです。

既定では、Hunchentoot はソースツリーの www/ ディレクトリからファイルを配信します。したがって、easy-acceptor のソース(Slime なら M-.)へ行くと、おそらく ~/quicklisp/dists/quicklisp/software/hunchentoot-v1.2.38/ にあり、そこに www/ ディレクトリが見つかります。内容は次のとおりです。

別のディレクトリを配信したいなら、easy-acceptor:document-root オプションを渡します。accessor で slot を設定することもできます。

(setf (hunchentoot:acceptor-document-root *acceptor*)
      #p"path/to/www")

まず index.html を作りましょう。現在のディレクトリ(Lisp REPL の場所)に新しい www/index.html を作って、次を入れます。

<html>
  <head>
    <title>Hello!</title>
  </head>
  <body>
    <h1>Hello local server!</h1>
    <p>
    We just served our own files.
    </p>
  </body>
</html>

別の port で新しい acceptor を起動してみましょう。

(defvar *my-acceptor* (make-instance 'hunchentoot:easy-acceptor
                                     :port 4444
                                     :document-root #p"www/"))
(hunchentoot:start *my-acceptor*)

http://127.0.0.1:4444/ に行って違いを見てください。

同じ Lisp image の中に、別 port の別 acceptor を作ったことに注意してください。これはもう十分に便利です。

インターネットから server にアクセスする

Hunchentoot

Hunchentoot なら特別なことは不要で、すぐにインターネットから server を見られます。

VPS 上で次を評価すると、

(hunchentoot:start (make-instance 'hunchentoot:easy-acceptor :port 4242))

server の IP ですぐに見えます。

止めるには (hunchentoot:stop *) を使います。

ルーティング

シンプルな route

Hunchentoot

既存の関数を route に結び付けるには、”prefix dispatch” を作って *dispatch-table* リストに push します。

(defun hello ()
   (format nil "Hello, it works!"))

(push
  (hunchentoot:create-prefix-dispatcher "/hello.html" #'hello)
  hunchentoot:*dispatch-table*)

regexp を使った route を作るには create-regex-dispatcher を使います。url-as-regexp には文字列、S 式、または cl-ppcre scanner を渡せます。

まだなら acceptor を作って server を起動してください。

(defvar *server* (make-instance 'hunchentoot:easy-acceptor :port 4242))
(hunchentoot:start *server*)

http://localhost:4242/hello.html にアクセスします。

We can see logs on the REPL:

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"

define-easy-handler allows to create a function and to bind it to an uri at once.

Its form follows

define-easy-handler (function-name :uri <uri> …) (lambda list parameters)

where <uri> can be a string or a function.

Example:

(hunchentoot:define-easy-handler (say-yo :uri "/yo") (name)
  (setf (hunchentoot:content-type*) "text/plain")
  (format nil "Hey~@[ ~A~]!" name))

Visit it at http://localhost:4242/yo and add parameters on the url: http://localhost:4242/yo?name=Alice.

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: http://localhost:4444/yo?name=Bob It works too ! In fact, define-easy-handler accepts an acceptor-names parameter:

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.

So, define-easy-handler has the following signature:

define-easy-handler (function-name &key uri acceptor-names default-request-type) (lambda list parameters)

It also has a default-parameter-type which we’ll use in a minute to get url parameters.

There are also keys to know for the lambda list. Please see the documentation.

Easy-routes (Hunchentoot)

easy-routes は Hunchentoot の上に乗る route 処理拡張です。提供するものは次のとおりです。

使うときは、server を hunchentoot:easy-acceptor ではなく easy-routes:easy-routes-acceptor で作ります。

(setf *server* (make-instance 'easy-routes:easy-routes-acceptor))

補足: routes-acceptor もあります。違いは、easy-routes-acceptor は easy-routes で route が見つからなかった場合に Hunchentoot の *dispatch-table* を順に見ていくことです。これにより、たとえば静的 content を Hunchentoot の通常方式で配信できます。

route は次のように定義します。

(easy-routes:defroute my-route-name ("/foo/:x" :method :get) (y &get z)
    (format nil "x: ~a y: ~a z: ~a" x y z))

route の signature は 2 つの部分から成ります。

("/foo/:x" :method :get) (y &get z)

ここで :x は path parameter を捕捉し、route 本体の x 変数に束縛します。y&get z は URL parameter を定義し、HTTP request body から取り出す &post parameter も使えます。

これらの parameter には、define-easy-handler と同じく :init-form:parameter-type オプションを指定できます。

では、Web アプリのロジックのもっと奥で、利用者を “/foo/3” へ redirect したいとしましょう。URL を直書きする代わりに、route 名から URL を生成 できます。easy-routes:genurl を次のように使います。

(easy-routes:genurl 'my-route-name :id 3)
;; => /foo/3

(easy-routes:genurl 'my-route-name :id 3 :y "yay")
;; => /foo/3?y=yay

decorators は route 本体の前に実行される関数です。装飾チェーンと route 本体の実行を続けるために、next 引数の関数を呼ぶ必要があります。例を示します。

(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)))

詳しくは easy-routes の README を参照してください。

Caveman

Caveman には route を定義する 2 つの方法があります。defroute マクロと、Python 風の annotation である @route です。

(defroute "/welcome" (&key (|name| "Guest"))
  (format nil "Welcome, ~A" |name|))

@route GET "/welcome"
(lambda (&key (|name| "Guest"))
  (format nil "Welcome, ~A" |name|))

URL parameter を持つ route です(url 内の :name に注意)。

(defroute "/hello/:name" (&key name)
  (format nil "Hello, ~A" name))

“wildcard” parameter を定義することもできます。splat キーを使います。

(defroute "/say/*/to/*" (&key splat)
  ; matches /say/hello/to/world
  (format nil "~A" splat))
;=> (hello world)

regexp を有効にするには :regexp t を付けます。

(defroute ("/hello/([\\w]+)" :regexp t) (&key captures)
  (format nil "Hello, ~A!" (first captures)))

GET と POST parameter にアクセスする

Hunchentoot

まず、query parameter はいつでも次のようにアクセスできます。

(hunchentoot:parameter "my-param")

これは、すべての handler に渡される既定の *request* object に対して動作します。

get-parameterpost-parameter もあります。

先ほど define-easy-handler のいくつかの keyword parameter を見ました。ここでは default-parameter-type を導入します。

次の handler を定義しました。

(hunchentoot:define-easy-handler (say-yo :uri "/yo") (name)
  (setf (hunchentoot:content-type*) "text/plain")
  (format nil "Hey~@[ ~A~]!" name))

name 変数は既定で string です。確認してみましょう。

(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)))

http://localhost:4242/yo?name=Alice に行くと、次が返ります。

Hey Alice you are of type (SIMPLE-ARRAY CHARACTER (5))

別の型へ自動的に束縛するには default-parameter-type を使います。次の単純型のどれかを指定できます。

または複合リストです。

ここで <type> は単純型です。

JSON request body にアクセスする

Hunchentoot

request body を読むには hunchentoot:raw-post-data を使います。:force-text t を付けると、octet の vector ではなく常に string を得られます。

それから、この string を好きな JSON ライブラリ(jzonshasht など)で parse できます。

(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
        …)))

エラー処理

どの framework でも、対話性の度合いは選べます。Web framework は 404 ページを返して REPL に output を出すこともできますし、対話 Lisp デバッガを起動することも、error を処理して HTML ページに Lisp の backtrace を表示することもできます。

Hunchentoot

エラー処理の挙動を選ぶには、次のグローバル変数を設定します。

(setf hunchentoot:*catch-errors-p* nil)

この挙動を細かく調整したいなら、汎用関数 maybe-invoke-debugger も参照してください。debug のために、特定の condition class に対して specialize したくなるかもしれません(後述)。

Hunchentoot には condition class があります。すべての condition の superclass は hunchentoot-condition です。error の superclass は hunchentoot-error で、これは hunchentoot-condition の subclass です。

ドキュメントも参照してください: https://edicl.github.io/hunchentoot/#conditions

Clack

Clack を使うなら、clack-errors middleware のような plugin が役立つでしょう: https://github.com/CodyReichert/awesome-cl#clack-plugins

The clack-errors plugin shows the error message, a legible backtrace and environment variables.

Weblocks - “JavaScript 問題” を解く ©

Weblocks は、widget ベースで server ベースの framework で、組み込みの ajax 更新機構を持っています。JavaScript を書かずに、また JavaScript に transpile される Lisp コードを書かずに、動的 web application を書けます。

Weblocks は、Slava Akhmechet、Stephen Compall、Leslie Polzer によって開発された古い framework です。しばらく落ち着いたあと、Alexander Artemenko による非常に活発な update、refactoring、rewrite が進んでいます。

もともとは continuation ベースでした(現在は除去されています)。そのため、Smalltalk の Seaside の Lisp 版とも言えます。Haskell の Haste、OCaml の Eliom、Elixir の Phoenix LiveView などとも比較できます。

Ultralisp の website は、CL コミュニティで知られる production の Weblocks サイトの例です。


Weblocks の作業単位は widget です。見た目は class definition のようです。

(defwidget task ()
   ((title
     :initarg :title
     :accessor title)
    (done
     :initarg :done
     :initform nil
     :accessor done)))

あとは、この widget に対する render method を定義するだけです。

(defmethod render ((task task))
  "Render a task."
  (with-html
        (:span (if (done task)
                   (with-html
                         (:s (title task)))
                 (title task)))))

既定では Spinneret template engine を使いますが、好きな別のものを bind することもできます。

ajax event を起こすには、完全な Common Lisp で lambda を書きます。

...
(with-html
  (:p (:input :type "checkbox"
    :checked (done task)
    :onclick (make-js-action
              (lambda (&key &allow-other-keys)
                (toggle task))))
...

The function make-js-action 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.

Is it appealing ? Carry on this quickstart guide here: http://40ants.com/weblocks/quickstart.html.

Templates

Djula - HTML markup

Djula is a port of Python’s Django template engine to Common Lisp. It has excellent documentation.

Caveman uses it by default, but otherwise it is not difficult to setup. まずテンプレートの置き場所を次のように宣言します。

(djula:add-template-directory (asdf:system-relative-pathname "webapp" "templates/"))

そのうえで、使うテンプレートを宣言・コンパイルします。例を示します。

(defparameter +base.html+ (djula:compile-template* "base.html"))
(defparameter +welcome.html+ (djula:compile-template* "welcome.html"))

Djula のテンプレートは次のようになります。{\% のバックスラッシュは Jekyll の制約です。

{\% extends "base.html" \%}
{\% block title %}Memberlist{\% endblock \%}
{\% block content \%}
  <ul>
  {\% for user in users \%}
    <li><a href=""></a></li>
  {\% endfor \%}
  </ul>
{\% endblock \%}

最後に、テンプレートを描画するには route の中で djula:render-template* を呼びます。

(easy-routes:defroute root ("/" :method :get) ()
  (djula:render-template* +welcome.html+ nil
                          :users (get-users)

効率のため、Djula は描画前にテンプレートをコンパイルします。

access ライブラリと並んで、Quicklisp で最もダウンロードされているライブラリの 1 つです。

Djula のフィルタ

フィルタは、変数の表示方法を変えるためのものです。Djula にはよくできた組み込みフィルタがあり、ドキュメントも充実していますtag と混同しないようにしてください。

見た目は `` のようになります。ここで lower は既存のフィルタで、テキストを小文字にします。

フィルタは引数を取ることもあります。たとえば `` は add フィルタを value と 2 で呼びます。

さらに、独自フィルタを定義するのも簡単です。やることは def-filter マクロを使うだけです。第 1 引数に変数を取り、追加の optional 引数も取れます。

一般形は次のとおりです。

(def-filter :myfilter-name (value arg) ;; arg is optional
   (body))

使い方は `` です。

add フィルタの定義例を示します。

(def-filter :add (it n)
  (+ it (parse-integer n)))

独自フィルタを書いたら、すぐにアプリ全体で使えます。

フィルタは、単純でない書式化やロジックをテンプレートからバックエンドへ移すのに便利です。

Spinneret - Lisp らしいテンプレート

Spinneret は “Lisp らしい” HTML5 生成器です。見た目は次のようになります。

(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*)))

作者は、より有名な cl-who よりも、HTML を別々の関数や macro に分けて組み立てるほうが簡単だと考えています。ただし、機能はそれだけではありません。

静的 asset を配信する

Hunchentoot

Hunchentoot では create-folder-dispatcher-and-handler prefix directory を使います。

例:

(push (hunchentoot:create-folder-dispatcher-and-handler
       "/static/" (merge-pathnames
                     "src/static" ; <-- starts without a /
                     (asdf:system-source-directory :myproject)))
      hunchentoot:*dispatch-table*)

これで、/path/to/myproject/src/static/ にあるプロジェクトの静的ファイルは /static/ プレフィックスで配信されます。

<img src="/static/img/banner.jpg" />

database に接続する

詳しくは databases の節 を見てください。Mito ORM は SQLite3、PostgreSQL、MySQL をサポートし、migration や DB schema の versioning などもあります。

Caveman では、database connection は Lisp session 中ずっと生きており、各 HTTP request で再利用されます。

利用者がログイン済みか確認する

framework には session を扱う方法が用意されています。ここでは、利用者がログイン済みか確認するために route を包む小さな macro を作ります。

Caveman では *session* は session のデータを表す hash table です。login と logout の関数は次のようになります。

(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))

単純な predicate を定義します。

(defun logged-in-p ()
  (gethash :user cm:*session*))

そして with-logged-in macro を定義します。

(defmacro with-logged-in (&body body)
  `(if (logged-in-p)
       (progn ,@body)
       (render #p"login.html"
               '(:message "Please log-in to access this page."))))

利用者がログインしていなければ session store には何もなく、login page を描画します。問題なければ macro 本体を実行します。使い方は次のとおりです。

(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*))))))

同様に使えます。

password を暗号化する

cl-bcrypt を使う

cl-bcrypt は password の hash 化と検証のための library です。使い方は次のとおり簡単です。

;; 12 ラウンドの password オブジェクトを作る
(defparameter *password* (bcrypt:make-password "test" :cost 12 :identifier "2a"))
;; ハッシュを生成する
(bcrypt:password-hash *password*)
;; #(249 97 146 214 147 168 142 174 40 17 15 74 150 236 240 184 72 175 74 206 160 168 22)
;; 文字列表現
(defparameter *password-string* (bcrypt:encode *password*))
;; 保存済み文字列と "test" を比べて password を検証する
(bcrypt:password= "test" *password-string*)
;; T
(bcrypt:password= "correct horse battery staple" *password-string*)
;; NIL

手動で (Ironclad を使う)

このレシピでは、暗号化と検証を自分で行います。デファクトスタンダードの Ironclad cryptographic toolkit と、Babel の文字コード encode/decode library を使います。

次のスニペットは、database に保存すべき password hash を作ります。Ironclad は string ではなく byte-vector を期待する点に注意してください。

(defun password-hash (password)
  (ironclad:pbkdf2-hash-password-to-combined-string
   (babel:string-to-octets password)))

pbkdf2RFC2898 で定義されています。pseudorandom function を使って、password に基づく安全な encryption key を導出します。

次の関数は、利用者が有効かどうかを確認し、入力された password を検証します。active で検証できたなら user-id を返し、それ以外は error が起きてもすべて nil を返します。自分の application に合わせて調整してください。

(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)))

次は database に password を設定する例です。password を保存するときは (password-hash password) を使っています。残りは web framework と DB library に依存する部分です。

(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))))))

Credit: /r/learnlisp/u/arvid.

実行とビルド

ソースから application を実行する

ソースから Lisp code を script として実行するには、実装の --load スイッチを使えます。

次を確認する必要があります。

次のような command を使えます。

;; run.lisp

(load "myproject.asd")

(ql:quickload "myproject")

(in-package :myproject)
(handler-case
    ;; START 関数が web server を起動します。
    (myproject::start :port (ignore-errors
                              (parse-integer
                                (uiop:getenv "PROJECT_PORT"))))
  (error (c)
    (format *error-output* "~&An error occurred: ~a~&" c)
    (uiop:quit 1)))

さらに、environment variable で application の port を設定できるようにしています。

ファイルは次のように実行できます。

sbcl --load run.lisp

project を読み込んだあと、web server は background で起動します。おなじみの Lisp REPL が使えるので、動いている application と対話できます。

自分の好きな editor から、離れた場所にある running application に接続し、editor での変更を動いている instance へコンパイルできます。後述の “remote Lisp image に接続する” を参照してください。

自己完結型実行ファイルを作る

他の Common Lisp application と同様に、Web app も asset を含めて 1 つの executable にまとめられます。配備はとても簡単です。server にコピーして実行するだけです。

$ ./my-web-app
Hunchentoot server が起動しました。
localhost:9003 で待ち受けます。

scripting#for-web-apps のレシピを参照してください。

Travis CI や GitLab CI で継続的 delivery を行う

testing#continuous-integration の節を見てください。

Electron によるマルチプラットフォーム配信

Web application の binary を作ったら、Electron window からそれを参照できます。

Ceramic は、その作業をまとめてやってくれる tool 群です。

使い方はこれだけです。

;; Ceramic とアプリを読み込む
(ql:quickload '(:ceramic :our-app))

;; Ceramic の初期化
(ceramic:setup)
(ceramic:interactive)

;; アプリを起動する(ここでは Lucerne ベース)
(lucerne:start our-app.views:app :port 8000)

;; ブラウザ window を開く
(defvar window (ceramic:make-window :url "http://localhost:8000/"))

;; Ceramic を起動する
(ceramic:show-window window)

これを Linux、Mac、Windows へ配布できます。

さらにあります。

Ceramic applications は native code にコンパイルされるため、性能を確保でき、閉源の商用 application も配布できます。

そのため、JS を minify する必要もありません。

Deployment

手動で deployment する

shell で executable を起動して background に回す(C-z bg)か、tmux session の中で実行できます。最良ではありませんが、動きます。

Systemd: daemon 化、クラッシュ時の再起動、log の扱い

これは実際には system 固有の作業です。自分の system でのやり方を確認してください。

今では多くの GNU/Linux distro に Systemd が入っているので、簡単な例を示します。

Systemd で application を配備するのは、設定 file を書くだけです。

$ sudo emacs -nw /etc/systemd/system/my-app.service
[Unit]
Description=systemd 上の Lisp app の例

[Service]
WorkingDirectory=/path/to/your/project/directory/
ExecStart=/usr/bin/make run  # or anything
Type=simple
Restart=on-failure

[Install]
WantedBy=network.target

すると、start する command が使えます。

sudo systemctl start my-app.service

service を install して、boot や reboot のあとに app を起動 する command もあります(それが “[Install]” 部分です)。

sudo systemctl enable my-app.service

status も確認できます。

systemctl status my-app.service

application の log も見られます(stdout や stderr に書けば、Systemd が logging します)。

journalctl -u my-app.service

-f オプションで log の更新をリアルタイム表示でき、その場合は -n 50--lines で表示行数を増やせます)

Systemd は crash を処理し、application を再起動 します。それが Restart=on-failure の行です。

ただし、いくつか注意点があります。

詳細: https://www.freedesktop.org/software/systemd/man/systemd.service.html

Docker を使う

Common Lisp 向けの Docker image はいくつかあります。たとえば次のものです。

Guix を使う

GNU Guix は transactional な package manager で、既存の OS の上に入れられるほか、declarative な system 設定をサポートする丸ごとの distro でもあります。system dependency を含む self-contained tarball を配布できます。例として Nyxt browser を見てください。

Nginx の背後で動かす

Lisp web app を Nginx の背後で動かすのに、CL 特有のことは何もありません。始めるための例を示します。

Lisp app が web server 上で、IP address 1.2.3.4、port 8001 で動いているとします。特別なことはありません。real domain name で app にアクセスしたいわけです(rate limiting など、Nginx の他の利点も使いたい)。domain name を買って、domain name を server の IP address に結び付ける A type の DNS record を作ったとします。

Nginx で server を設定し、”your-domain-name.org” から port 80 に来る接続を、ローカルで動く Lisp app に送るよう指示します。

新しい file /etc/nginx/sites-enabled/my-lisp-app.conf を作り、次の proxy directive を追加します。

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/;
    }
}

proxy_pass http://1.2.3.4:8001/; では server の public IP address を使っている点に注意してください。Hunchentoot のような Lisp webserver がその IP に直接 listen していることもよくありますが、security 上の理由から Lisp app を localhost で動かしたいかもしれません。

nginx を reload します(”reload” signal を送る)。

$ nginx -s reload

これで終わりです。http://www.your-domain-name.org から外部経由で Lisp app にアクセスできます。

Heroku や他の service へ deployment する

heroku-buildpack-common-lisp と、Kubernetes、OpenShift、AWS など向けの interface library が載っている Awesome CL#deploy を参照してください。

監視

Prometheus.cl を見ると、SBCL と Hunchentoot の metric(memory、thread、requests per second など)用の Grafana dashboard が分かります。

remote Lisp image に接続する

この節を参照してください: debugging#remote-debugging

hot reload

これは Quickutil の例です。実際には先ほどの節を自動化したものです。

Makefile の target があります。

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))

これは server 上で実行する必要があります(簡単な fabfile command で ssh 経由で呼べます)。その前に fab update で server 上に git pull 済みなので、新しい code はあるがまだ動いていない状態です。local swank server に接続し、新しい code を読み込み、app を止めてすぐ起動し直します。

関連項目

Credits

Page source: ja/web.md

T
O
C