(ns foo
(:require [clojure.browser.repl :as repl]))
(repl/connect "https://127.0.0.1:9000/repl")
本參考文件適用於因某些原因想要從 Clojure 以程式方式啟動 REPL 的人。建議的啟動 REPL 方式記錄在快速入門中.
創建 ClojureScript 的原因之一是 JavaScript 的廣泛應用。有許多有趣的環境可以執行 JavaScript。每個環境都有其獨特之處。Clojure 受歡迎的原因之一是它具有 REPL,這為開發人員提供了最動態的開發體驗。我們希望在 JavaScript 執行的每個環境中都支援這種動態開發體驗。為了實現這一點,我們在 JavaScript 環境周圍創建了一個抽象,並將 REPL 與任何特定的實作分離。這使得 REPL 具有與 JavaScript 相同的廣泛應用,並允許求值環境實作獨立用於自動測試和跨環境測試等用途。
大多數專案將針對特定環境。這些變更將使開發人員在其目標環境中充分利用 REPL 的優勢。目前,有多個環境的實作。透過實作一個協定,可以輕鬆支援其他環境。
REPL 的基本用法始終相同
require cljs.repl
require 實作所需求值環境的命名空間
建立新的求值環境
使用已建立的環境啟動 REPL
在每個環境中使用 REPL 的感覺也相同;輸入表單,列印結果,並在最合適的地方發生副作用。
連接到瀏覽器的 REPL 的運作方式與正常的 REPL 非常相似:從主控台讀取表單、求值並列印傳回值。與正常 REPL 用法的主要且有用的區別在於,所有副作用都發生在瀏覽器中。您可以顯示警示、操作 DOM 並與正在執行的應用程式互動。
在 samples/repl
下有一個範例專案,顯示如何設定最基本的瀏覽器連線 REPL。以下範例將逐步說明如何執行相同的操作。
第一步是建立連線的瀏覽器端。這是透過 require 一個檔案並新增一行程式碼來完成的,如下面名為 foo.cljs
的檔案中所示。
(ns foo
(:require [clojure.browser.repl :as repl]))
(repl/connect "https://127.0.0.1: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://127.0.0.1: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
來載入任何環境的程式碼,並在下面更詳細地說明。
如果您想要處理此程式碼,則以下關於實作的注意事項將會很有幫助。
若要建立新的環境,請實作 IJavaScriptEnv 協定。
(defprotocol IJavaScriptEnv
(-setup [this opts])
(-evaluate [this filename line js])
(-load [this ns url])
(-tear-down [this]))
setup
和 tear-down
會執行建立和銷毀 JavaScript 求值環境所需的任何工作。這些函式將具有副作用,並將傳回 nil。
evaluate
會採用檔案名稱、行號和 JavaScript 字串,並求值該字串,傳回具有索引鍵 :status
和 :value
的對應。狀態值可以是 :success
、:error
或 :exception
。:value
將是傳回值或錯誤訊息。在例外狀況的情況下,可能會出現包含堆疊追蹤的 :stacktrace
索引鍵。
load
函式會採用由 JavaScript 檔案提供的命名空間清單,以及該檔案的 URL,並將 JavaScript 從給定的 URL 載入到環境中。實作並不負責確保每個命名空間僅載入一次,因為這是由基礎架構管理。
為了建立瀏覽器連線 REPL 並實現上述目標,我們使用長輪詢和 Google 的 CrossPageChannel。長輪詢允許我們將瀏覽器視為伺服器,而 CrossPageChannel 可協助我們繞過同源策略。
瀏覽器連線 REPL 的模型是 REPL 是用戶端,而瀏覽器是求值 JavaScript 程式碼的伺服器。我們如何在不使用 WebSockets 的情況下實作此模型?如果我們將連線視為在瀏覽器和 REPL 之間傳遞的一系列訊息,並忽略從瀏覽器傳送的第一則訊息,那麼我們就擁有所需的內容。當瀏覽器最初連線時,REPL 將保持該連線,直到它有某些內容要傳送以進行求值。讀取並編譯下一個表單後,它將使用該已儲存的連線傳送到瀏覽器。瀏覽器將對其進行求值,並使用新的連線傳送結果。然後循環重複...
瀏覽器對 JavaScript 程式碼強制執行同源策略。這表示在頁面中求值的 JavaScript 只能來自一個來源網域。這對於瀏覽器連線 REPL 來說是一個問題,因為 FireFox 和 Chrome 都將從檔案系統開啟檔案並連線到 localhost:9000 視為不同的網域。想要連線到從完全不同網域提供的應用程式也可能是有效的用例,這將在所有瀏覽器中被禁止。
幸運的是,Google 也遇到了這個問題,並建立了一個名為 CrossPageChannel 的東西。在不深入探討細節的情況下,這允許從一個網域 (REPL) 提供的 iframe 與從另一個網域 (應用程式伺服器) 提供的父頁面進行通訊。這是以所有現代瀏覽器都支援的方式完成的。
Google Closure 有一種載入相依性的技術。它使用相依性檔案來建立相依性圖表,並將命名空間對應到檔案。當在開發模式下編譯專案時,ClojureScript 的 build
函式會建立這種相依性檔案。Google Closure 假設應用程式啟動時,所有需要知道的相依性都會已知。當使用 REPL 時,這個假設並不成立,並導致兩個限制。
第一個限制是,所有相依性都需要在應用程式啟動前包含在這些檔案中。我們無法在稍後為我們想要使用的新 ClojureScript 或 JavaScript 命名空間新增新的相依性。
另一個限制是,Google 的相依性載入方法假設所有相依性都會在應用程式啟動時載入。goog.writeScriptTag_
的實作使用 document.write
來將新的 script 標籤新增到頁面。這在初始頁面載入期間使用時有效,但如果在頁面載入後使用,它將會移除文件的內容。這表示即使相依性檔案包含我們想要載入的相依性,也無法載入它。這可以被修正。請參閱 https://github.com/ibdknox/brepl/blob/master/out/brepl.js 以取得範例。
ClojureScript REPL 已經有一個 load-file
函式,可以用來載入單個 ClojureScript 檔案。這個函式不會考慮相依性,也不能用來載入第三方 JavaScript 檔案。
這表示我們需要一種統一的方式來載入任何我們可能想要載入的東西。load-namespace
函式就是為此目的而建立的。它使用建置系統來計算給定命名空間的所有相依性。這包括任何我們目前可以建置到專案中的東西:ClojureScript 檔案、JavaScript 檔案,以及第三方的 ClojureScript 和 JavaScript。然後,每個相依性都會依相依性順序傳遞到 -load
函式。-load
函式負責判斷命名空間是否已經載入,如果尚未載入,則評估 JavaScript。
當 REPL 編譯一個命名空間表單時,它會檢查所需的命名空間,並對每個命名空間呼叫 load-namespace
。
注意:將 :libs 選項傳遞給 REPL 以便它可以找到第三方 JavaScript 函式庫尚未實作。