ClojureScript

JavaScript 模块(Alpha)

本指南要求 ClojureScript 1.10.238 或更高版本,并假设您熟悉 快速入门.

本页面介绍如何将现代 JavaScript 源文件无缝地混合到现有的 ClojureScript 项目中。所述功能应被视为 alpha 质量,可能会发生变化。

动机

ClojureScript 最初发布时,编译到 JavaScript 仍然是一个新奇的事物,除了缩小之外的源代码转换也很少见。从那时起,JavaScript 的源代码到源代码编译变得越来越流行,无论是像 React JSX 这样的嵌入式 HTML DSL,还是 新的 ECMAScript 标准,这些标准解决了 JavaScript 的许多旧缺陷。但是将这些新型源文件集成到 ClojureScript 项目中需要依赖 JavaScript 构建工具,而这些工具仍然缺乏 Google Closure 编译器更高级的功能,比如精确的死代码消除和代码分割。

幸运的是,Google Closure 不仅跟上了 JavaScript 语言的各种改进,而且还提供了从各种流行的 JavaScript 模块格式(CommonJS、AMD、ES6)到 Google Closure 命名空间约定的转换。ClojureScript 现在公开了所有这些功能,并且在 Java 8 的 Nashorn JavaScript 引擎 的帮助下,可以相对轻松地提供最前沿的 JavaScript 源代码转换。

此外,Google Closure 现在支持 Node.js 解析算法。ClojureScript 编译器现在可以构建想要使用来自 NPM 的依赖项的项目。

先决条件

快速入门 一样,本指南假设您已安装最新的 JDK 8Node.js >= 6.9.4 和 rlwrap 版本。本指南只使用 Leiningen 来管理依赖项,并且可以轻松地适应 BootMaven

JavaScript 模块

首先让我们看看如何将 JavaScript 模块作为构建的一部分。

mkdir -p hello-es6
cd hello-es6
touch project.clj

创建一个包含以下内容的 build.edn 文件

{:output-to    "main.js"
 :output-dir   "out"
 :main         hello-es6.core
 :target       :nodejs
 :foreign-libs [{:file "src"
                 :module-type :es6}]
 :verbose      true})

请注意,:foreign-libs 条目现在可以为 :file 指定目录。在这种情况下,ClojureScript 编译器将递归搜索此目录中的 .js 文件,并自动为您创建 :foreign-libs 条目,并使用提供的选项。每个条目的 :provides 命名空间将根据目录结构自动计算。

让我们创建主要的 ClojureScript 命名空间

mkdir -p src/hello_es6
touch src/hello_es6/core.cljs

编辑此文件,使其看起来像这样

(ns hello-es6.core
  (:require [cljs.nodejs :as nodejs]
            [js.hello :as hello]))

(nodejs/enable-util-print!)

(defn -main [& args]
  (hello/sayHello))

(set! *main-cli-fn* -main)

请注意,我们的 JavaScript 文件可以像任何其他 Google Closure 命名空间一样导入。

让我们编写 JavaScript 代码

mkdir -p src/js
touch src/js/hello.js

JavaScript 文件不声明命名空间,因此 ClojureScript 编译器将根据条目的位置计算一个命名空间。由于 :foreign-libs 条目指定了 "src",因此此 JavaScript 文件从 ClojureScript 中使用的命名空间将是 js.hello

编辑此文件,使其看起来像这样

export var sayHello = function() {
    console.log("Hello, world!");
};

让我们检查我们的监视脚本是否有效

cljs -m cljs.main -co build.edn -w -c

您可以通过在 main.js 上调用 Node 来验证脚本是否按预期工作

node main.js
Hello world!

使用 REPL

由于 JavaScript 模块只是被编译成 Google Closure 命名空间,因此所有通用的 ClojureScript REPL 功能都正常工作。例如,如果您想要自动热加载 ES6 源文件,只需使用 Figwheel

我们将使用标准的 Node.js REPL 演示手动热加载。

启动 REPL

clj -M -m cljs.main -co build.edn -r

加载 js.hello 命名空间并试用它

user> (require '[js.hello :as hello])
true
user> (hello/sayHello)
Hello world!

不要退出 REPL,将 src/js/hello.js 编辑为以下内容

export var sayHello = function() {
    console.log("Hello, world!");
};
export var sayThings = function(xs) {
    for(let x of xs) {
        console.log(x);
    }
};

重新加载您的 JavaScript 模块并尝试新功能

user> (require '[js.hello :as hello] :reload)
true
user> (hello/sayThings ["ClojureScript", "+", "JavaScript", "Rocks!"])
ClojureScript
+
JavaScript
Rocks!

由于 ClojureScript 向量支持 ES6 迭代协议,因此 ES6 for…​of 正常工作。

虽然 Google Closure 可以处理 ES6,但您可能希望使用 JavaScript 生态系统中的其他预处理器 - 例如 Babel 的 JSX 转换。在这种情况下,我们希望利用 Nashorn。

Babel 转换

将您的 deps.edn 文件更改为以下内容

{:deps {org.clojure/clojurescript {:mvn/version "1.9.854"}
        cljsjs/react {:mvn/version "15.4.2-0"}
        cljsjs/react-dom {:mvn/version "15.4.2-0"}
        cljsjs/react-dom-server {:mvn/version "15.4.2-0"}
        cljsjs/babel-standalone {:mvn/version "6.18.1-3"}}}

将您的 build.edn 更改为以下内容

{:output-to    "main.js"
 :output-dir   "out"
 :main         hello-es6.core
 :target       :nodejs
 :foreign-libs [{:file "src"
                 :module-type :es6
                 :preprocess cljsjs.babel-standalone/transform}] ;; CHANGED
 :verbose      true})

Babel-standalone 包来自 Cljsjs,它提供了必要的 JavaScript 文件和一个可以用作 :preprocess 处理程序的函数。该函数使用 Nashorn JS 引擎来运行 Babel 并处理外部库。可以通过将 :cljsjs.babel-standalone/babel-opts 属性添加到外部库映射来提供 Babel 的选项。

让我们将 React JSX 组件添加到 src/js/hello.js

export var sayHello = function() {
    console.log("Hello, world!");
};
export var sayThings = function(xs) {
    for(let x of xs) {
        console.log(x);
    }
};
export var reactHello = function() {
    return <div>Hello world!</div>
};

让我们更改我们的 ClojureScript

(ns hello-es6.core
  (:require [cljsjs.react]
            [cljsjs.react.dom]
            [cljsjs.react.dom.server]
            [cljs.nodejs :as nodejs]
            [js.hello :as hello]))

(nodejs/enable-util-print!)

(defn -main [& args]
  (hello/sayHello)
  (println (.renderToString js/ReactDOMServer (hello/reactHello))))

(set! *main-cli-fn* -main)

运行监视脚本

lein trampoline run -m clojure.main watch.clj

构建完成后,运行代码

node main.js

您应该看到类似于以下内容的输出

Hello, world!
<div data-reactroot="" data-reactid="1" data-react-checksum="1334186935">Hello world!</div>

您可能已经注意到,我们的 ES6 文件没有通过 import 声明其对 React、ReactDOM 或 ReactDOMServer 的依赖关系。正确处理此问题取决于 Google Closure 的一个待处理补丁,以支持 ES6 源文件的 Node.js 模块解析。当此更改发布后,本指南将更新。

但是,Node.js 解析的 CommonJS 支持今天已经可以使用了。下一节将涵盖此主题,并最终也将适用于 ES6 文件。

自定义 JavaScript 转换

在前面的示例中,Babel 转换函数由 Cljsjs 包提供。如果您需要使用不同的转换,您可以编写自己的预处理函数。Babel 转换可以像这样实现,无需 Cljsjs 包

从您的 project.clj 中删除 cljsjs/babel-standalone 依赖项。

babel.min.js 下载到您的项目目录中

curl -O https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.18.1/babel.min.js

创建一个新的 src/hello_es6/babel.clj 文件

(ns hello-es6.babel
  (:require [clojure.java.io :as io]
            [cljs.build.api :as b])
  (:import javax.script.ScriptEngineManager))

(def engine
  (doto (.getEngineByName (ScriptEngineManager.) "nashorn")
    (.eval (io/reader (io/file "babel.min.js")))))

(defn transform-jsx [js-module opts]
  (let [code (str (gensym))]
    (.put engine code (:source js-module))
    (assoc js-module :source
      (.eval engine (str "Babel.transform("code", {presets: ['react', 'es2016']}).code")))))

将您的 build.edn 更改为以下内容并重建

{:output-to    "main.js"
 :output-dir   "out"
 :main         hello-es6.core
 :target       :nodejs
 :foreign-libs [{:file "src"
                 :module-type :es6
                 :preprocess 'hello-es6.babel/transform-jsx}] ;; CHANGED
 :verbose      true})

原始作者:David Nolen