« ^ »

xwidget-webkitでJavaScriptの実行結果を取得する

所要時間: 約 6分

Emacsには xwidget-webkit という機能がある。これは端的にいうとWebブラウザの機能で、Emacsのバッファ上にWebブラウザを配置できる。EmacsのWebブラウザは他にも eww などあるけれど、こちらはどちらかというテキストベースのWebブラウザで、JavaScriptエンジンもないし、レンダリングエンジンも自前の独自実装だったりする。 xwidget-webkitwebkit と名前に入っている通り、 Webkit をEmacsのバッファに Xwidget として組み込む。その正体は本物の Webkit であるため、レンダリングエンジンやJavaScriptエンジンも Webkit に内蔵されているものを使う。例えばGoogle ChromeやSafariのようなWebブラウザと同じように動作する。

Google ChromeやSafariのようなWebブラウザにはWebkit以外の所で実装されている機能があるが、 xwidget-webkit にはそれらが無いし、セキュリティ的な問題で不安になるような点もあるので、インターネット上のリソースを手放しで読み込む事はできない。しかし自分が管理するリソースを読み込んだり、検証用のツールとしては使える。今回はこの機能を少し使いながら、考え事をしてみようと思う。

テスト用に以下のファイルを作成する。

main.html

<html>
  <body>
    <h1 id="testing">Hello world!</h1>
  </body>
</html>

このHTMLを xwidget-webkit で表示するには xwidget-webkit-browse-url を使う。

(xwidget-webkit-browse-url
  (format "file://%s" (expand-file-name "./main.html")))

Emacsは *xwidget-webkit: * というバッファを作成し、このHTMLをレンダリングした状態でそのバッファに表示する。 xwidget-webkit はこの小さなWebブラウザを操作する為の関数をいくつか用意している。

ページを再読み込みするには xwidget-webkit-reload を使う。

(xwidget-webkit-reload)

この式を評価すると *xwidget-webkit: * バッファに表示されているWebページが再読み込みされる。他にも画面のズームを操作する xwidget-webkit-zoom-inxwidget-webkit-zoom-out 、画面をスクロールする xwidget-webkit-scroll-downxwidget-webkit-scroll-up など実装されている。

eww はJavaScriptエンジンを搭載していない。だから最近のフロントエンドの技術(ReactやVueのような)で実装されたフロントエンドは正しく表示できない。 xwidget-webkitWebkit を利用しており、 Webkit はJavaScriptエンジンを搭載しているから、JavaScriptは当然動くしReactやVueで書かれたフロントエンドも正しく表示できる。そうなるとEmacsからWebkitへJavaScriptを送って実行したい。 xwidget-webkit は、その関数として xwidget-webkit-execute-script を提供している。

先頭のh1タグの文字色を変更するJavaScriptを送って実行してみる。

(xwidget-webkit-execute-script 
 (xwidget-webkit-last-session)
 "document.querySelector('h1').style.color = 'red';")

これを実行すると、先頭のh1タグの文字色が赤に変わる。

実行したJavaScriptの結果をEmacs側で取得したい事もある。JavaScriptで実装された関数を呼び出し、その戻り値をEmacs Lispで利用したい場合などがそうだ。 xwidget-webkit-execute-script は第3引数にコールバック関数を渡す事ができる。このコールバック関数は、第2引数で渡したJavaScriptを評価した後、最後に評価された値を引数として呼び出される。

次のようなコールバック関数を考える。

(defun testing-callback-fn (args)
   (setq testing-args args))

渡された引数を testing-args にsetqするという単純な処理を行う。この関数を xwidget-webkit-execute-script のコールバックとして指定する。処理を単純にするため、評価するJavaScriptは数字のリテラルを評価する。

(xwidget-webkit-execute-script 
 (xwidget-webkit-last-session)
 "1;"
 #'testing-callback-fn)

この式を評価してもnilが返される。ただし正常に実行できていれば、コールバック関数が呼び出され testing-args に値が設定されている。確認してみよう。

testing-args
1.0

変換処理が入るため整数が浮動小数になってはいるが 1.0 が返却されている。

次に整数と文字列の要素からなるリストを評価してみよう。

(xwidget-webkit-execute-script 
 (xwidget-webkit-last-session)
 "[1, 'aaa'];"
 #'testing-callback-fn)

これは正常に完了する。JavaScriptのリストはEmacs Lispのvectorに変換される。

testing-args
[1.0 aaa]

結果だけを見るとaaaは一見シンボルに見えなくもないけれど、これは文字列だ。

(type-of (seq-elt  testing-args 1))
string

JavaScriptのリストは評価できたが、Objectは評価できない。以下の式は何かのエラーを発する訳ではないけれど、コールバック関数は呼び出されない。

(xwidget-webkit-execute-script 
 (xwidget-webkit-last-session)
 "{'aaa': 1, 'bbb': 2};"
 #'testing-callback-fn)

もしJavaScriptのObjectをEmacs Lispに戻したいのなら JSON.stringify() を使い文字列化すると良い。

(xwidget-webkit-execute-script 
 (xwidget-webkit-last-session)
 "JSON.stringify({'aaa': 1, 'bbb': 2});"
 #'testing-callback-fn)

この式は成功し、コールバックが呼び出される。

testing-args
{"aaa":1,"bbb":2}

型を確認すると文字列が設定されている。

(type-of testing-args)
string

この結果をEmacs Lispの json-read-from-string に渡せば、JSON形式の文字列を連想リストに変換できるだろう。

(json-read-from-string testing-args)
((aaa . 1) (bbb . 2))

当然、自前で作成した関数も呼び出す事ができる。

(xwidget-webkit-execute-script 
 (xwidget-webkit-last-session)
 "function func_yay() {
     return 'YAY!!';
  }

  func_yay();
 "
 #'testing-callback-fn)

事前に func_yay 関数を定義し、それを呼び出す。 func_yay 関数は YAY!! という文字列を返却するため、最終的に評価された結果は YAY!! という文字列となり、それがコールバックに渡される。

testing-args
YAY!!

ここまで来ると、もうJavaScriptにできる事は概ねできるだろうと想像が付く。当然だがDOM操作も行える。h1タグを取得し、その内部の文字列を取得してみよう。

(xwidget-webkit-execute-script 
 (xwidget-webkit-last-session)
 "document.querySelector('h1').textContent;"
 #'testing-callback-fn)

評価すると Hello world! という文字列が取得できる。

testing-args
Hello world!

気を付けないといけない事は、JavaScriptがエラーしても、それを補足する方法が無い事だ。送信しているのはJavaScriptの構文で記述した文字列であるため、例えば構文的に不正な状態で送信したとしても、 xwidget-webkit-execute-script が例外を出力する訳でもなく、エラーメッセージがどこかに出力される訳でもない。Webkitのコンソールログには何かしら出力されているようにも思うけれど、それを取得する方法を見付ける事ができなかった。

またJavaScript側からEmacsを操作する方法もない。例えばJavaScriptでイベントリスナーを登録し、そのイベント発火時にEmacs Lispを呼び出すといった事はできない。逆にこれはできてしまうと、セキュリティ的に重大な問題になりそうでもある。

もし同様の事をしたいのであれば、JavaScript側のイベントリスナーではWebkitで管理しているオブジェクトの状態を変更するまでに留め、Emacs側からタイマーやアイドルタイマーを使い、その値をポーリングする事で似たような事を実現できると思う。

今回はEmacsのxwidget-webkitの機能について少し考えた。この機能の安全性を考えながら、用法容量を守って使っていきたいと思う。