ClojureScript

REPLと評価

このリファレンスは、何らかの理由でClojureからプログラム的にREPLを起動したいと考えている人向けです。REPLを起動する推奨方法は、クイックスタートに記載されています。.

ClojureScriptを作成した理由の1つは、JavaScriptの到達範囲にあります。JavaScriptは多くの興味深い環境で実行できます。これらの環境にはそれぞれ独自の特性があります。Clojureが優れた理由の1つは、開発者に可能な限り動的な開発エクスペリエンスを提供するREPLがあることです。私たちは、JavaScriptが実行されるすべての環境で、この動的な開発エクスペリエンスをサポートしたいと考えています。これを実現するために、JavaScript環境に関する抽象化を作成し、REPLを特定の実装から切り離しました。これにより、REPLはJavaScriptと同じ到達範囲を持ち、自動テストやクロス環境テストなどのために、評価環境の実装を独立して使用できるようになります。

ほとんどのプロジェクトは特定の環境をターゲットにします。これらの変更により、開発者はターゲット環境でREPLのメリットを最大限に活用できます。現在、複数の環境向けの実装があります。1つのプロトコルを実装することで、簡単に追加の環境をサポートできます。

REPLの使用

REPLの基本的な使い方は常に同じです。

  1. cljs.repl をrequireする

  2. 目的の評価環境を実装する名前空間をrequireする

  3. 新しい評価環境を作成する

  4. 作成した環境でREPLを起動する

REPLの使用感も各環境で同じです。フォームが入力され、結果が出力され、副作用は最も意味のある場所で発生します。

ブラウザを評価環境として使用

ブラウザ接続REPLは、通常のREPLとほぼ同じように動作します。フォームはコンソールから読み取られ、評価され、戻り値が出力されます。通常のREPLの使用との大きな違いは、すべての副作用がブラウザで発生することです。アラートを表示したり、DOMを操作したり、実行中のアプリケーションと対話したりできます。

samples/replの下に、最小限のブラウザ接続REPLのセットアップ方法を示すサンプルプロジェクトがあります。以下の例では、同じことをステップバイステップで行います。

最初のステップは、接続のブラウザ側を作成することです。これは、1つのファイルにrequireし、1行のコードを追加することで行われます。以下はfoo.cljsという名前のファイルの例です。

(ns foo
  (:require [clojure.browser.repl :as repl]))
(repl/connect "https://:9000/repl")

ブラウザ接続REPLの最も興味深いユースケースは、プロジェクトに接続し、REPLを使用して実行中のアプリケーションを駆動および開発することです。これを実現するには、上記のコードをプロジェクトの任意の名前空間に追加します。

次に、開発モードまたは単純な最適化を使用してファイルをコンパイルします。高度な最適化は行わないでください。

./bin/cljsc foo.cljs > foo.js

以下に示すような、index.htmlという名前のホストHTMLページを作成します。

<html>
  <head>
    <meta charset="UTF-8">
    <title>Browser-connected REPL</title>
  </head>
  <body>
    <div id="content">
      <script type="text/javascript" src="out/goog/base.js"></script>
      <script type="text/javascript" src="foo.js"></script>
      <script type="text/javascript">
        goog.require('foo');
      </script>
    </div>
  </body>
</html>

これは、他のブラウザベースのClojureScriptプロジェクトで行うことと何も変わりません。

上記の例のように、ブラウザを評価環境として使用してREPLを起動します。

(require '[cljs.repl :as repl])
(require '[cljs.repl.browser :as browser])  ;; require the browser implementation of IJavaScriptEnv
(def env (browser/repl-env)) ;; create a new environment
(repl/repl env) ;; start the REPL

REPLが起動すると、「ポート9000でサーバーを起動しています」というメッセージが表示されます。この時点で、https://:9000にアクセスしてHTMLページを開き、接続を完了します。ページが開き、接続されると、REPLプロンプトが表示されます。

ポート9000はデフォルトです。上記のクライアントコードで、このポートをブラウザに指定していることに注意してください。異なるポートを使用するには、新しい評価環境を作成するときに`:port`オプションを渡します。

(def env (browser/repl-env :port 8090)) ;; listen on port 8090

何か面白いことが思いつかない場合のために、いくつかのアイデアを紹介します。

;; the basics
(+ 1 1)
(:a {:a :b})
(reduce + [1 2 3 4 5])
(defn sum [coll] (reduce + coll))
(sum [2 2 2 2])

;; load a ClojureScript file and use it
(load-file "clojure/string.cljs")
(clojure.string/reverse "ClojureScript")

;; browser specific
(js/alert "I am an evil side-effect")

(ns test.dom (:require [clojure.browser.dom :as dom]))
(dom/append (dom/get-element "content")
            (dom/element "ClojureScript is all up in your DOM."))

;; load and use goog code we haven't used yet
(ns test.crypt (:require [goog.crypt :as c]))
(c/stringToByteArray "ClojureScript")

(load-namespace 'goog.date.Date)
(goog.date.Date.)

現在require関数は存在しませんが、nsフォームを使用して、新しい名前空間を読み込み、requireし、エイリアスを設定できます。load-file関数とload-namespace関数は、任意の環境でコードを読み込むために使用でき、以下で詳しく説明します。

ブラウザ接続REPLオプション

現在、ブラウザ評価環境の設定に使用できるオプションは2つあります。

  • :port リッスンするポートを設定します - デフォルトは9000

  • :working-dir REPL関連コードをコンパイルする作業ディレクトリを設定します - デフォルトは".repl"

コードの読み込み

上記のコードは、評価環境にコードを読み込む3つの方法の例を示しています。load-fileload-namespace、およびnsフォーム内です。load-fileは、コードを読み込む最も低レベルの方法です。ClojureScriptファイルを読み込むためだけに使用できます。これらをコンパイルし、コンパイルされたJavaScriptを評価します。load-namespaceは、依存関係のないClojureScriptファイルまたはJavaScriptファイルを、依存関係の順序で読み込みます。nsフォームで名前空間をrequireすると、requireされた各名前空間はload-namespaceを使用して読み込まれます。

これらの関数は、すべての評価環境で使用できます。

自動読み込みされるユーザーコード

REPLが起動すると、クラスパスにある任意のuser.cljsまたはuser.cljcファイルが自動的に読み込まれます。これは、開発時に便利なコードを配置するのに最適な場所です。

ファイルには、必要に応じてnsフォームを含めることができ、必要な名前空間を読み込んだり、ファイルに表示されるdefフォームの名前空間を設定したりできます。

名前空間が指定されていない場合は、cljs.userが想定されます。

実装

このコードに取り組みたい場合は、実装に関する次のメモが役立ちます。

目標

  • 追加の依存関係なし

  • すべてのブラウザで**今すぐ**動作する必要があります

  • セキュリティは目標ではありません。これは開発とテストのためです

IJavaScriptEnvプロトコル

新しい環境を作成するには、IJavaScriptEnvプロトコルを実装します。

(defprotocol IJavaScriptEnv
  (-setup [this opts])
  (-evaluate [this filename line js])
  (-load [this ns url])
  (-tear-down [this]))

setuptear-downは、JavaScript評価環境の作成と破棄に必要な作業を行います。これらの関数は副作用を持ち、nilを返します。

evaluateは、ファイル名、行番号、JavaScript文字列を受け取り、文字列を評価して、キー:status:valueを持つマップを返します。statusの値は、:success:error、または:exceptionになります。:valueは戻り値またはエラーメッセージになります。例外の場合、スタックトレースを含む:stacktraceキーがある場合があります。

load関数は、JavaScriptファイルによって提供される名前空間のリストとファイルのURLを受け取り、指定されたURLからJavaScriptを環境に読み込みます。実装は、各名前空間が一度だけ読み込まれることを保証する責任はありません。これはインフラストラクチャによって管理されます。

ブラウザを評価環境として

上記で説明した目標を満たすブラウザ接続REPLを作成するために、ロングポーリングとGoogleのCrossPageChannelを使用します。ロングポーリングを使用すると、ブラウザをサーバーとして扱うことができ、CrossPageChannelは同一オリジンポリシーを回避するのに役立ちます。

ブラウザ接続REPLのモデルは、REPLがクライアントであり、ブラウザがJavaScriptコードを評価するサーバーであることです。WebSocketsに頼らずにこれを実装するにはどうすればよいでしょうか?ブラウザとREPLの間で渡されるメッセージのシーケンスとして接続を考え、ブラウザから送信される最初のメッセージを無視すれば、必要なものが得られます。ブラウザが最初に接続すると、REPLはその接続を保持し、評価のために送信するデータがあるまで待ちます。次のフォームが読み取られ、コンパイルされると、その保存された接続を使用してブラウザに送信されます。ブラウザはそれを評価し、新しい接続で結果を送信します。そして、サイクルが繰り返されます…

ブラウザは、JavaScriptコードに対して同一オリジンポリシーを適用します。つまり、ページで評価されるJavaScriptは、1つのオリジンドメインからのみ取得できます。これは、ブラウザ接続REPLにとって問題となります。なぜなら、FireFoxとChromeはどちらも、ファイルシステムからファイルを開くこととlocalhost:9000に接続することを異なるドメインと見なすからです。完全に異なるドメインから提供されるアプリケーションに接続したいという有効なユースケースもあるかもしれません。これはすべてのブラウザで禁止されます。

幸いなことに、Googleもこの問題に遭遇し、CrossPageChannelと呼ばれるものを作成しました。詳細には触れませんが、これにより、1つのドメイン(REPL)から提供されるiframeが、別のドメイン(アプリケーションサーバー)から提供された親ページと通信できます。これは、すべての最新のブラウザでサポートされる方法で行われます。

ブラウザでのコード読み込み

Google Closureには、依存関係を読み込むための手法があります。依存関係ファイルを使用して依存関係グラフを作成し、名前空間をファイルにマッピングします。ClojureScriptのbuild関数は、開発モードでプロジェクトをコンパイルするときに、この種の依存関係ファイルを作成します。Google Closureは、アプリケーションの起動時に依存関係に関する必要な情報がすべてわかっているという前提を立てています。この前提は、REPLを使用する場合は無効であり、2つの制限につながります。

最初の制限は、アプリケーションの開始前に、すべての依存関係をこれらのファイルに含める必要があることです。ClojureScriptやJavaScriptの新しい名前空間を使用したい場合、後で新しい依存関係を追加することはできません。

もう1つの制限は、Googleの依存関係のロード方法は、すべての依存関係がアプリケーションの開始時にロードされると仮定していることです。goog.writeScriptTag_の実装は、新しいスクリプトタグをページに追加するためにdocument.writeを使用しています。これは、初期ページロード中に使用される場合は機能しますが、ページがロードされた後に使用すると、ドキュメントの内容が削除されます。つまり、依存関係ファイルにロードしたい依存関係が含まれていても、ロードできません。これは修正可能です。例については、https://github.com/ibdknox/brepl/blob/master/out/brepl.jsを参照してください。

ClojureScript REPLには、単一のClojureScriptファイルをロードするために使用できるload-file関数が既にあります。この関数は依存関係を考慮しておらず、サードパーティのJavaScriptファイルをロードするために使用できません。

これは、ロードしたいものすべてで機能する、統一されたロード方法が必要であることを示唆しています。この目的のためにload-namespace関数が作成されました。これは、ビルドシステムを使用して、指定された名前空間のすべての依存関係を計算します。これには、現在プロジェクトにビルドできるものすべてが含まれます。ClojureScriptファイル、JavaScriptファイル、およびサードパーティのClojureScriptとJavaScriptです。次に、各依存関係は、依存関係の順序で-load関数に渡されます。-load関数は、名前空間が既にロードされているかどうかを判断し、ロードされていない場合はJavaScriptを評価する役割を担います。

REPLが名前空間フォームをコンパイルすると、必要な名前空間をチェックし、それぞれに対してload-namespaceを呼び出します。

注:サードパーティのJavaScriptライブラリを見つけられるようにREPLに`:libs`オプションを伝える機能はまだ実装されていません。