用 ClojureScript 语法运行 React

Posted SegmentFault

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了用 ClojureScript 语法运行 React相关的知识,希望对你有一定的参考价值。

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

执行 ClojureScript 代码

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

 
   
   
 
  1. $ npm install -g lumo-cljs

  2. $ brew install lumo

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

 
   
   
 
  1. $ lumo

  2. Lumo 1.5.0

  3. ClojureScript 1.9.542

  4. Node.js v7.10.0

  5. Docs: (doc function-name-here)

  6.       (find-doc "part-of-name-here")

  7. Source: (source function-name-here)

  8. Exit: Control+D or :cljs/quit or exit

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

  10. Hello world!

  11. nil

  12. cljs.user=>

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

 
   
   
 
  1. lumo -i main.cljs

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

 
   
   
 
  1. (ns demo.server-render)

  2. (def React (js/require "react"))

  3. (def ReactDOM (js/require "react-dom/server"))

  4. (def create-class (js/require "create-react-class"))

  5. (def comp-demo

  6.  (create-class

  7.    #js {:displayName "demo"

  8.         :render (fn []

  9.                    (.createElement React "div" nil))}))

  10. (println "This is only a demo.")

  11. (println

  12.  (.renderToString ReactDOM (.createElement React comp-demo nil)))

运行 React

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

 
   
   
 
  1. npm install shadow-cljs

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

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

项目结构

首先来看一下文件结构:

 
   
   
 
  1. =>> tree -I "node_modules|target"

  2. .

  3. ├── README.md

  4. ├── dist

  5. │   └── index.html

  6. ├── entry

  7. │   ├── main.css

  8. │   └── page.js

  9. ├── package.json

  10. ├── shadow-cljs.edn

  11. ├── src

  12. │   └── app

  13. │       └── main.cljs

  14. ├── webpack.config.js

  15. └── yarn.lock

  16. 4 directories, 9 files

配置 shadow-cljs 编译器

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

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

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

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

 
   
   
 
  1. {:source-paths ["src"]

  2. :dependencies [[mvc-works/hsl "0.1.2"]]

  3. :builds {:app {:target :npm-module

  4.                :output-dir "target/"}}}

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

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

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

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

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

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

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

 
   
   
 
  1.  "scripts": {

  2.    "watch": "webpack-dev-server --hot-only",

  3.    "compile-cljs": "shadow-cljs -b app --once",

  4.    "watch-cljs": "shadow-cljs -b app --dev"

  5.  },

  6.  "devDependencies": {

  7.    "css-loader": "^0.28.4",

  8.    "shadow-cljs": "^0.9.5",

  9.    "style-loader": "^0.18.2",

  10.    "webpack": "^2.6.1",

  11.    "webpack-dev-server": "^2.4.5"

  12.  },

注意 shadow-cljs 的常用参数:

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

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

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

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

 
   
   
 
  1. yarn watch-cljs

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

编写 React 代码

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

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

 
   
   
 
  1. (ns app.main

  2.  (:require ["react" :as React]

  3.            ["react-dom" :as ReactDOM]

  4.            ["create-react-class" :as create-class]))

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

 
   
   
 
  1. (def container

  2.  (create-class

  3.    #js {:displayName "container"

  4.         :render

  5.           (fn []

  6.            (.createElement React "div" nil

  7.              (.createElement React "span" nil "Hello world!")))}))

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

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

  2. (defn render! []

  3.  (.render ReactDOM (.createElement React container nil) mount-point))

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

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

 
   
   
 
  1. (defn main []

  2.  (render!)

  3.  (println "App loaded."))

  4. (defn reload []

  5.  (render!)

  6.  (println "App reloaded."))

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

 
   
   
 
  1. var $CLJS = require("./cljs_env");

  2. require("./cljs.core.js");

  3. require("./shadow.npm.react.js");

  4. require("./shadow.npm.react_dom.js");

  5. require("./shadow.npm.create_react_class.js");

  6. var cljs=$CLJS.cljs;

  7. var shadow=$CLJS.shadow;

  8. var goog=$CLJS.goog;

  9. var app=$CLJS.app || ($CLJS.app = {});

  10. goog.dependencies_.written["app.main.js"] = true;

  11. goog.provide('app.main');

  12. goog.require('cljs.core');

  13. goog.require('cljs.core');

  14. goog.require('shadow.npm.react');

  15. goog.require('shadow.npm.react_dom');

  16. goog.require('shadow.npm.create_react_class');

  17. app.main.mount_point = document.querySelector("#app");

  18. app.main.container = (function (){var G__10849 = ({"displayName": "container", "render": (function (){

  19. return shadow.npm.react.createElement("div",null,shadow.npm.react.createElement("span",null,"Hello world!"));

  20. })});

  21. return shadow.npm.create_react_class(G__10849);

  22. })();

  23. app.main.render_BANG_ = (function app$main$render_BANG_(){

  24. return shadow.npm.react_dom.render(shadow.npm.react.createElement(app.main.container,({})),app.main.mount_point);

  25. });

  26. app.main.main = (function app$main$main(){

  27. app.main.render_BANG_();

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

  29. });

  30. app.main.reload = (function app$main$reload(){

  31. app.main.render_BANG_();

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

  33. });

  34. module.exports = app.main;

  35. //# sourceMappingURL=app.main.js.map

Webpack 配置

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

 
   
   
 
  1. require('./main.css');

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

  3. if (module.hot) {

  4.  module.hot.accept('../target/app.main', function() {

  5.    require('../target/app.main').reload();

  6.  });

  7. }

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

启动

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

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

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

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

 
   
   
 
  1. yarn # 安装依赖

  2. yarn watch-cljs # 启动 cljs 的编译器

  3. # 再开个终端

  4. yarn watch # 启动 Webpack 的开发环境

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

更多

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

学习 ClojureScript 是个比较麻烦的过程, 这个语言大家平时都不习惯, 有兴趣的话倒是可以循着这些链接继续翻下去: 

https://github.com/shaunlebron/ClojureScript-Syntax-in-15-minutes 

https://github.com/clojure-china/cljs-book

http://www.braveclojure.com/clojure-for-the-brave-and-true/

如果你想了解 shadow-cljs 编译器更多的用法, 可以查看 Wiki: 

https://github.com/thheller/shadow-cljs/wiki 

我也写了几个常用功能的例子, 又可以直接 clone 到本地通过 yarn 运行: 

https://github.com/minimal-xyz?utf8=%E2%9C%93&q=shadow&type=&language=

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

内容简介

React 社区发展从 ClojureScript 社区吸取了不少的灵感,

  • ClojureScript 语言初步介绍

  • React 从 ClojureScript 的借鉴点

  • ClojureScript 当中不可变数据简介

  • Global App State 的概念

  • Atom 和 Redux 的对比


报名学习


长按上图中二维码或点击左下角 “阅读原文” 进行报名。报名后即可加入题叶老师交流群,和题叶及其他小伙伴共同讨论学习。如有更多问题需要咨询,可扫描下方二维码添加管理员微信好友,发送 “讲堂”,由管理员邀请进入讲堂交流群。



以上是关于用 ClojureScript 语法运行 React的主要内容,如果未能解决你的问题,请参考以下文章

ClojureScript + React-Native - 嵌入视频

Etudes for ClojureScript

在 clojurescript 中实现 ajax 调用

自动完成问题(Material UI + React + Reagent/ClojureScript)

Clojurescript 中的 Javascript 互操作分配

ClojureScript Is Not CoffeeScript