推广一下 ClojureScript 入门指南 http://cljs-book.clj.im/

得益于最近 ClojureScript(简称 cljs) 社区的发展, 运行和编译 cljs 已经越来越方便.
刷一篇文章来展示一下如何用 ClojureScript 来模仿前端写法运行 React.

执行 ClojureScript 代码

如果你只是想执行一下 cljs 代码熟悉语法, 可以直接安装 Lumo.
Lumo 是一个基于 V8 开发的 cljs 运行环境, 支持 Node.js API.
你可以通过多种方式安装 Lumo:

$ npm install -g lumo-cljs
$ brew install lumo

安装完成之后可以从命令行直接启动:

$ lumo
Lumo 1.5.0
ClojureScript 1.9.542
Node.js v7.10.0
 Docs: (doc function-name-here)
       (find-doc "part-of-name-here")
 Source: (source function-name-here)
 Exit: Control+D or :cljs/quit or exit

cljs.user=> (println "Hello world!")
Hello world!
nil
cljs.user=>

或者也可以用把代码贴到一个文件里, 然后通过 -i 这个参数来运行文件:

lumo -i main.cljs

你可以把这个文件保存下来试着用 Lumo 运行, 注意依赖的几个 React 模块:

(ns demo.server-render)

(def React (js/require "react"))
(def ReactDOM (js/require "react-dom/server"))
(def create-class (js/require "create-react-class"))

(def comp-demo
  (create-class
    #js {:displayName "demo"
         :render (fn []
                    (.createElement React "div" nil))}))

(println "This is only a demo.")
(println
  (.renderToString ReactDOM (.createElement React comp-demo nil)))

运行 React

用 cljs 来写网页会复杂一些, 因为涉及到编译, 也涉及到引用 npm 模块.
不过现在已经好多了, 新版的 cljs 编译器加上 shadow-cljs 解决了这个麻烦.
shadow-cljs 是一个基于 npm 发布的 cljs 编译器, 所以直接用 npm 就能安装.

npm install shadow-cljs

cljs 编译器有 JVM 依赖, 需要看下系统是否安装了 Java, 如果没有也可以用 node-jre 代替.

shadow-cljs 支持将 cljs 编译到 CommonJS 格式的代码, 所以原理上很简单.
我们要做的就是配置好 shadow-cljs 的编译流程, 能够生成代码.
完整的代码我放在 GitHub 上, 可以直接下载通过 yarn 运行:
https://github.com/clojure-ch...

项目结构

首先来看一下文件结构:

=>> tree -I "node_modules|target"
.
├── README.md
├── dist
│   └── index.html
├── entry
│   ├── main.css
│   └── page.js
├── package.json
├── shadow-cljs.edn
├── src
│   └── app
│       └── main.cljs
├── webpack.config.js
└── yarn.lock

4 directories, 9 files
配置 shadow-cljs 编译器

除了我们熟悉的 Webpack 开发常用的文件, 还有这样一些文件:

  • shadow-cljs.edn 编译工具的配置文件

  • src/app/main.cljs 这个就是我们的 ClojureScript 代码

shadow-cljs.edn 配置非常清晰:

{:source-paths ["src"]
 :dependencies [[mvc-works/hsl "0.1.2"]]
 :builds {:app {:target :npm-module
                :output-dir "target/"}}}

这是常用的编译到 npm 模块的配置

  • source-paths 编译的源码所在的文件夹

  • dependencies cljs 依赖, 不过这个依赖只是个示例

  • builds 编译配置的集合, 其中 app 就是一种编译配置

  • target 编译目标, 这里的 :npm-module 表示 npm 模块, 也可以是 :browser 或者更多

  • output-dir 生成的文件输出到哪里

然后我们通过 npm 本地安装 shadow-cljs 就可以通过命令行工具启动编译器了:

  "scripts": {
    "watch": "webpack-dev-server --hot-only",
    "compile-cljs": "shadow-cljs -b app --once",
    "watch-cljs": "shadow-cljs -b app --dev"
  },
  "devDependencies": {
    "css-loader": "^0.28.4",
    "shadow-cljs": "^0.9.5",
    "style-loader": "^0.18.2",
    "webpack": "^2.6.1",
    "webpack-dev-server": "^2.4.5"
  },

注意 shadow-cljs 的常用参数:

  • -b 实际上是 --build 的缩写, 表示选择哪个编译配置, 这里选了 app

  • --once 告诉编译器只要编译一次

  • --dev 告诉编译器编译之后继续监视文件, 当文件改变时自动编译

经过这样的配置, 就可以通过命令启动了:

yarn watch-cljs

编译结果会输出在 target/ 当中, 是很多个 .js 文件.
其中 target/app.main.js 是我们想要的代码的入口文件.

编写 React 代码

我们用 cljs 写 React 用到的是 JavaScript Interop.
也就是用 cljs 的语法去写 js 语法的代码, 编译器会生成 js 代码.
下面我们看一遍具体怎样实现, 完整的文件可以看:
https://github.com/clojure-ch...

首先需要在命名空间当中定义 React 的依赖, 主要是三个模块.
文件的命名空间是 app.main, 跟 src/app/main.cljs 的路径相对应.
其中 src/ 前面在配置 source-paths 已经写过了.
在最新的 cljs 当中可以这样引用 npm 模块:

(ns app.main
  (:require ["react" :as React]
            ["react-dom" :as ReactDOM]
            ["create-react-class" :as create-class]))

然后可以用 create-class 这个函数来调用 React.createClass 定义组件,
其中 #js {} 表示这个 HashMap 会转成 JavaScript Object,
cljs 语法里键的位置用关键字语法, 也会被自动转成 JavaScript 属性,
然后还有 React.createElement 这个方法的调用, 是特别的写法:

(def container
  (create-class
    #js {:displayName "container"
         :render
           (fn []
            (.createElement React "div" nil
              (.createElement React "span" nil "Hello world!")))}))

然后是挂载组件. 通过 ReactDOM.render 方法来挂载.
注意在 cljs 当中直接引用浏览器 API 需要借助 js/ 这个命名空间.

(def mount-point (.querySelector js/document "#app"))

(defn render! []
  (.render ReactDOM (.createElement React container nil) mount-point))

刚才的代码用到了很多 React without JSX 的写法, 可以参考官方文档:
https://facebook.github.io/re...

然后我们定义一下初始化的代码, main 在后面的代码呗调用,
还有 reload 时重新绘制界面. 因为我们的例子要支持代码热替换:

(defn main []
  (render!)
  (println "App loaded."))

(defn reload []
  (render!)
  (println "App reloaded."))

这个文件编译之后是一个很难看到 js 文件, 可以看到是支持 CommonJS 规范的.
而且 cljs 一般也会有 SourceMaps 支持, 实际开发当中可以不看编译出来的 js 代码.

var $CLJS = require("./cljs_env");
require("./cljs.core.js");
require("./shadow.npm.react.js");
require("./shadow.npm.react_dom.js");
require("./shadow.npm.create_react_class.js");
var cljs=$CLJS.cljs;
var shadow=$CLJS.shadow;
var goog=$CLJS.goog;
var app=$CLJS.app || ($CLJS.app = {});
goog.dependencies_.written["app.main.js"] = true;

goog.provide('app.main');
goog.require('cljs.core');
goog.require('cljs.core');
goog.require('shadow.npm.react');
goog.require('shadow.npm.react_dom');
goog.require('shadow.npm.create_react_class');
app.main.mount_point = document.querySelector("#app");
app.main.container = (function (){var G__10849 = ({"displayName": "container", "render": (function (){
return shadow.npm.react.createElement("div",null,shadow.npm.react.createElement("span",null,"Hello world!"));
})});
return shadow.npm.create_react_class(G__10849);
})();
app.main.render_BANG_ = (function app$main$render_BANG_(){
return shadow.npm.react_dom.render(shadow.npm.react.createElement(app.main.container,({})),app.main.mount_point);
});
app.main.main = (function app$main$main(){
app.main.render_BANG_();

return cljs.core.println.cljs$core$IFn$_invoke$arity$variadic(cljs.core.array_seq(["App loaded."], 0));
});
app.main.reload = (function app$main$reload(){
app.main.render_BANG_();

return cljs.core.println.cljs$core$IFn$_invoke$arity$variadic(cljs.core.array_seq(["App reloaded."], 0));
});

module.exports = app.main;

//# sourceMappingURL=app.main.js.map
Webpack 配置

然后还需要一个入口文件来处理一下启动的功能, 比如 CSS 和热替换.
Webpack 提供了 module.hot API 用来手动处理热替换.
我们接受整个 app.main.js 依赖的文件更新, 重新执行 require, 并且调用前面的 reload 函数.

require('./main.css');

window.onload = require('../target/app.main').main;

if (module.hot) {
  module.hot.accept('../target/app.main', function() {
    require('../target/app.main').reload();
  });
}

Webpack 的完整配置我就不重复了, 整个链接我贴在这里.
https://github.com/clojure-ch...
开发环境是可以支持 SourceMaps 的, 但是由于性能不理性, 我关掉了.

启动

最后加一个给 Webpack 启动的入口文件, 也就注意一下加载顺序:

<div id="app"></div>

<script type="text/javascript" src="main.js"></script>

最后就可以启动整个应用了, 前面写在了 npm scripts 里边:

yarn # 安装依赖
yarn watch-cljs # 启动 cljs 的编译器
# 再开个终端
yarn watch # 启动 Webpack 的开发环境

再打开 http://localhost:8080/ 就可以看到 React 渲染的 "Hello world!" 了.

更多

这篇文章介绍的只是很基础的在 cljs 里调用 React 的写法.
实际开发当中会用类库来写, 写起来更简单, 而且能做一些优化,
目前推荐的类库有 ReagentRum, 会涉及一些高级的 cljs 语法.

学习 ClojureScript 是个比较麻烦的过程, 这个语言大家平时都不习惯,
有兴趣的话倒是可以循着这些链接继续翻下去:
https://github.com/shaunlebro...
https://github.com/clojure-ch...
http://www.braveclojure.com/c...

如果你想了解 shadow-cljs 编译器更多的用法, 可以查看 Wiki:
https://github.com/thheller/s...
我也写了几个常用功能的例子, 又可以直接 clone 到本地通过 yarn 运行:
https://github.com/minimal-xy...

另外我最近(06-13)在 SegmentFault 有个简短分享, 感兴趣的话可以来评论.
ClojureScript 带给 React 项目的借鉴意义

如果觉得我的文章对你有用,请随意赞赏

你可能感兴趣的文章

c4605 · 6月12日

之前一直都没有注意到 cljs 可以用

(:require ["react" :as React]
            ["react-dom" :as ReactDOM]
            ["create-react-class" :as create-class])

的方式加载模块了,这个赞!

回复

0

最近个把月才发生的事情.
Lumo https://anmonteiro.com/2017/0...
shadow-cljs https://github.com/thheller/s...

题叶 作者 · 6月12日
0

@题叶 嗯,我知道是最近个把月,我订阅了 Mike Fikes 的博客,他竟然完全没有提到这件事情……我果然还是应该再订阅一下 António 的博客……之前一直给忘了……

c4605 · 6月13日
0

@题叶 我还用 Feed43 转了 shadow-cljs 的 changelog ,也没注意到这个改动……刚刚查了一下,好像就只有很简单的一句话直接就带过了……我还一直傻乎乎地用 (def a (js/require ... ……

c4605 · 6月13日
knightuniverse · 6月13日

语法相当特别啊

回复

0

Lisp 系的语法大家比较不熟悉 https://en.wikipedia.org/wiki...

题叶 作者 · 6月13日
载入中...
题叶 题叶

15.5k 声望

发布于专栏

题叶

ClojureScript 爱好者.

401 人关注