ClojureScript

REPL 和评估

本参考面向希望出于某些原因从 Clojure 以编程方式启动 REPL 的用户。推荐的启动 REPL 方法记录在 快速入门 中。.

创建 ClojureScript 的原因之一是 JavaScript 的可达性。JavaScript 可以运行在许多有趣的环境中。这些环境中的每一个都具有其独特的特点。Clojure 很棒的原因之一是它拥有一个 REPL,为开发人员提供尽可能动态的开发体验。我们希望在 JavaScript 运行的每个环境中都支持这种动态开发体验。为了实现这一点,我们在 JavaScript 环境周围创建了一个抽象层,并将 REPL 与任何特定实现分离。这使得 REPL 具有与 JavaScript 相同的可达性,并且允许独立使用评估环境实现,例如自动化测试和跨环境测试。

大多数项目将针对特定环境。这些更改将允许开发人员在其目标环境中充分利用 REPL。目前,已经存在多个环境的实现。通过实现一个协议,可以轻松地支持其他环境。

使用 REPL

REPL 的基本用法始终相同

  1. require cljs.repl

  2. 需要实现所需评估环境的命名空间

  3. 创建新的评估环境

  4. 使用创建的环境启动 REPL

在每个环境中使用 REPL 的感觉也相同;输入表单、打印结果,副作用发生在最有意义的地方。

使用浏览器作为评估环境

连接浏览器的 REPL 的工作方式与普通 REPL 非常相似:从控制台读取表单、进行评估并打印返回值。与普通 REPL 使用的一个主要且有用的区别是,所有副作用都发生在浏览器中。您可以显示警报、操作 DOM 并与正在运行的应用程序进行交互。

samples/repl 下有一个示例项目,展示了如何设置一个最小的连接浏览器的 REPL。下面的示例将逐步介绍如何执行相同操作。

第一步是创建连接的浏览器端。这是通过需要一个文件并添加一行代码来完成的,如下面的名为 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 启动后,您将看到消息“Starting server on port 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 表单来加载、需要和别名新的命名空间。可以使用 load-fileload-namespace 函数在任何环境中加载代码,并在下面详细介绍。

连接浏览器的 REPL 选项

目前有两个选项可用于配置浏览器评估环境。

  • :port 设置监听端口 - 默认值为 9000

  • :working-dir 设置编译 REPL 相关代码的工作目录 - 默认值为 ".repl"

加载代码

上面的代码展示了三种将代码加载到评估环境中的示例:load-fileload-namespacens 表单内部。load-file 是加载代码的最低级方法。它只能用于加载 ClojureScript 文件。它将编译这些文件并评估编译后的 JavaScript。load-namespace 加载任何文件(ClojureScript 或 JavaScript),以及其所有未加载的依赖项(按依赖关系顺序)。在 ns 表单中需要命名空间时,将使用 load-namespace 加载每个需要的命名空间。

这些函数在每个评估环境中都可用。

自动加载的用户代码

REPL 启动时,它会自动加载类路径上存在的任何 user.cljsuser.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 只能来自一个源域。这对连接浏览器的 REPL 来说是一个问题,因为 FireFox 和 Chrome 都将从文件系统打开文件和连接到 localhost:9000 视为不同的域。可能还需要连接到从完全不同的域提供的应用程序,这在所有浏览器中都是禁止的。

幸运的是,Google 也遇到了这个问题,并创建了一个名为 CrossPageChannel 的东西。不深入细节,这允许来自一个域(REPL)的 iframe 与来自另一个域(应用程序服务器)的父页面进行通信。这是以所有现代浏览器都支持的方式实现的。

在浏览器中加载代码

Google Closure 有一种加载依赖项的技术。它使用一个依赖项文件来创建依赖项图并将命名空间映射到文件。ClojureScript build 函数在开发模式下编译项目时创建这种依赖项文件。Google Closure 假设在应用程序启动时将知道有关依赖项的所有信息。这种假设在使用 REPL 时无效,并导致两个限制。

第一个限制是,所有依赖项都需要在应用程序启动之前包含在这些文件中。我们不能为我们想要使用的新的 ClojureScript 或 JavaScript 命名空间在以后添加新的依赖项。

另一个限制是,Google 的加载依赖项方法假设所有依赖项将在应用程序启动时加载。goog.writeScriptTag_ 的实现使用 document.write 将新的脚本标签添加到页面中。这在初始页面加载时使用时有效,但在页面加载后使用时,它将删除文档的内容。这意味着即使依赖项文件包含我们想要加载的依赖项,它也不能加载。这可以修复。请参见 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 库的功能尚未实现。