Slime と Swank-js でサクサク JavaScript 開発

この記事は JavaScript2 Advent Calendar 2018 の 25 日目の記事として書かれました.

今回は,Emacs エディタからリモート上の Node やブラウザに接続して,エディタの編集画面から直接プログラムをホットスワップする開発方法を紹介します.Lisp 系言語の開発者にはお馴染みの Slime を用いた開発です.JavaScript 用の Swank サーバ(swank-js)は随分前から開発されていましたが,長い間更新が滞り最新の Emacs 環境では使うことができませんでした.今回,最新の Emacs 環境で使えるように修正したので,導入方法と使い方を紹介します.

Slime,Swank とは?

Slime とは Emacs エディタの The Superior Lisp Interaction Mode の略で,リモート上で稼働する Lisp 処理系に Emacs から接続し,エディタ上から開発を可能にする Emacs エディタのプラグイン機能です.Slime からの接続をリモートで受付けるサーバが Swank サーバです.Common Lisp は稼働中のプログラムを停止することなく書き換える(ホットスワップする)ことができるので,このような開発環境がデフォルトでした.

今回紹介するのは,JavaScript 用に拡張された SlimeJS とリモートの Node 上で稼働する Swank-js サーバです.

Slime を使った開発方法が優れているのは,エディタ上のコード編集画面から直接コードを実行・評価することができる点です.Atom や Vscode の場合,コードの断片を実行して試したい場合,付属のターミナル画面で Node REPL を起動しそこで実行するか,ブラウザのコンソールに移動するしかありません.Bracket や Webpack DevServer のようにファイル変更を watch する Web サーバを起動すれば,ファイル変更に合わせてリアルタイムにブラウザで実行を確認できますが,エディタ画面から直接コードを実行しているわけではありません.

Slime の場合,コードの編集画面から直接コードが実行できます.正確には,エディタのショートカットキーを使って JavaScript コードをリモートの Swank-js サーバーに送り,評価結果をサーバから受け取ります.実際の様子は,以下の動画を見てみてください.エディタ上からコードを実行しているのが見て取れるはずです.

残念ながらこの記事を書いている段階では,ブラウザの接続機能の修正が完了していないので,上の動画のようにリモートの Web ページに接続することはできません.本稿では NodeJS に接続して開発する方法のみを説明します.

セットアップ

  1. swank-js サーバを動かすために NodeJS をインストールしてください.

  2. swank-js を GitHub からインストールします.クローンしたローカルのディレクトリをこれ以降 <swank-js-dir> で参照します.

    git clone https://github.com/wshito/swank-js

    上のリポジトリは 公式プロジェクト から Fork したリポジトリ からさらに Fork したものです.時間ができたら上流にプルリクしますが,公式プロジェクト はプルリクにここ数年応えていません.

  3. 最新の SLIME をインストールしてください.Emacs のパッケージ管理ツール ELPA や MELPA を利用すると良いでしょう.現時点で SLIME 2.22 で動作確認しています.SLIME のインストール先ディレクトリをこれ以降 <slime-dir> で参照します.ちなみに Roswell ユーザは ~/.roswell/lisp/slime/ 以下に最新の SLIME がインストール済みなのでそれを使います.

  4. <swank-js-dir> 以下にある slime-js.el ファイルを <slime-dir>/contrib ディレクトリ内に配置します.下の設定例ではシンボリック・リンクを作成しています.

    cd <slime-dir>/contrib
     ln -s <swank-js-dir>/slime-js.el ./
  5. .emacsinit.elslime-js.el を有効にする設定を追加します.

    (add-to-list 'load-path "/your-slime-directory/slime")
    (slime-setup  '(slime-fancy slime-js))

    Roswell ユーザは上の設定の代わりに ~/.roswell/helper.el 内の roswell-slime-contribs 変数の定義に slime-js を追加します.例: (defvar roswell-slime-contribs '(slime-fancy slime-js))

  6. Elpa 等を使って Emacs に js2-mode をインストールします.slime-js.el が js2-mode に依存しています.

  7. js2-mode の設定を .emacs に追加します.

    (require 'js2-mode)
    (add-to-list 'auto-mode-alist '("\\.js\\'" . js2-mode))

    私は,js2-mode のフックとして以下の設定を追加しています.最低限必要な設定は以下のうち (slime-js-minor-mode 1) です.

    (add-hook 'js2-mode-hook (lambda ()
    			   (slime-js-minor-mode 1)
    			   #'js2-imenu-extras-mode
    			   #'rainbow-delimiters-mode
    			   (setq js2-basic-offset 2)))

使い方

まずは Emacs から NodeJS に接続してみましょう.基本手順は,1. swank-jsサーバを起動,2. EmacsでSlimeを起動,だけです.

swank-js サーバは NodeJS を使って起動します.コードの実行環境は NodeJS の環境に依存するため,使用するライブラリ等がインストールされた Node のプロジェクトディレクトリから swank-js サーバを起動します.

cd <node-project-dir>  # npmで予めnode_modulesがインストールされたディレクトリ
node <swank-js-dir>/swank.js

次に Emacs から Slime を使って swank-js サーバに接続します.M-x slime-connect を実行してください.ミニバッファで localhost とポート 4005 を指定してください.すると REPL がバッファ上に現れます.

NODE>

これでエディタから swank-js サーバを通じて NodeJS と接続できました.プロンプトで存分に JavaScript を実行してください.

ソースファイルからの実行

前節の手順で Emacs から swank-js サーバに接続していれば,通常のソースファイルの編集画面から JS コードを評価実行できます.評価したいコード上で C-c C-c をタイプすると,カーソル位置を含むトップレベル式が swank-js サーバに送られます.

Lisp の Slime では C-c C-p で評価結果をミニバッファ上に表示できますが,slime-js では実装されていません(時間ができたら実装したいと思います).評価結果を見たいときには,Node REPL のプロンプトが表示されているバッファに inspect 関数を使って評価結果を表示させます.

以下,チュートリアル形式でやってみましょう.まず,前節の手順で swank-js サーバに接続しておいてください.

  1. C-x C-f test.jstest.js ファイルを作成し,以下を入力してください.

    function double (x) {
      return 2 * x;
    }
    
    inspect([1, 2, 3, 4, 5].map(double));
  2. C-x 2 で画面を 2 つに分割し,C-x C-z で Node プロンプトのバッファを表示させます.カーソルもプロンプト上のバッファに移動します.

  3. C-x o でカーソルを test.js のソースバッファ上に戻します.そこで C-c C-b を実行すると,バッファ全体が swank-js サーバに送られます.inspect 関数の実行によって Node プロンプトのバッファ上に結果が表示されます.

通常の開発では console.log を使うほうが多いので,環境に応じて自動的に inspect 関数へのエイリアスが設定されるように,以下の式をソースの先頭で設定しておくと良いでしょう.

// setting for swank-js
console.log = (typeof inspect === "undefined") ? console.log : inspect;

それに合わせて test.js を以下のように書き換えます.

// setting for swank-js
console.log = (typeof inspect === "undefined") ? console.log : inspect;

function double (x) {
  return 2 * x;
}

console.log([1, 2, 3, 4, 5].map(double));  // console.log に変更

slime-js モードでは以下のショートカット・キーが設定されています.

  1. C-c C-c カーソル位置のトップレベル・フォームを swank-js サーバに送信(slime-js-send-defun).

  2. C-c C-r リージョンで選択部分を swank-js サーバに送信(slime-js-send-region).

  3. C-c C-b バッファ全体を swank-js サーバに送信(slime-js-send-buffer).

  4. C-c C-z Nodeプロンプトのバッファに移動(slime-switch-to-output-buffer).

開発上の注意点

swank-js サーバにコードを送信するときに注意しなければいけない点は,let で定義した変数を再定義できないことです.普通にプログラミングしていても,ソース上に同じ変数名で let 宣言すると already been declared とシンタックスエラーが出るように,swank-js にも let 宣言を複数回送信することはできません.この場合,ソースから let だけ削除して変数の代入として評価するか,let を除いた部分をリージョン指定して C-c C-r で送信する必要があります.

特に関数定義を const double = x ⇒ 2 * x の様に書く習慣がある人は,const 宣言すると名前を変えるか,swank-js を再起動しない限り,関数定義を変更できないので注意する必要があります.私は,テストケースに通るまで let で関数定義を書き,代入で修正を繰り返し,実装が完成したら const に変更しています.

ただし,関数定義を function キーワードを使って定義した場合は,Node ではコードを再評価することにより関数定義を上書きすることができました.

ブラウザへの接続

リポジトリREADME.md ファイルにはブラウザの接続方法が書いてありますが,FireFox への接続,それも http://localhost:8009/swank-js/test.html への接続しか確認していません.

この記事を書きながら,任意のリモートページへの接続を確認してみたところ,うまく機能しませんでした.README.md に書かれている「,target-url」を指定する接続です.仕組みは,FireFox から swank-js のプロキシサーバーに接続すると,swank-js が target-url のページを FireFox に返し,Slime から FireFox に接続することによって Web ページを操作できるようにしているようです.時間ができたらこれも動くようにしたいと思いますが,皆さんも Fork して修正してみてください.

おわりに

なかなかブログを更新しないものだから,今年は動機付けに Advent Calendar に 3 回もエントリしてしまいました.お陰様でなんとか書き上げることができました.これを機に,アウトプットを継続していけたらなと思います.最後まで読んでくださりありがとうございました.

メリークリスマス!そして良いお年を!

wshito

Read more posts by this author.

Itoshima, Japan http://diary.wshito.com