本文首发于我的博客javascript
随着 Firefox 57 的到来,以前维护的一个浏览器插件 gooreplacer 必须升级到 WebExtensions 才能继续使用,看了下以前写的 JS 代码,毫无修改的冲动,怕改了这个地方,那个地方忽然就 broken 了。所以,此次选择了 cljs,总体下来流程很顺利,除了迁移以前的功能,又加了更多功能,但愿能成为最简单易用的重定向插件 :-)css
闲话少说,下面的内容依次会介绍 cljs 的工做机制、开发环境,如何让 cljs 适配浏览器插件规范,以及重写 gooreplacer 时的一些经验。
本文的读者须要对 Clojure 语言、浏览器插件开发通常流程有基本了解,而且完成 ClojureScript 的 Quick Start。对于 Clojure,我目前在 sf 上有一套视频课程,供参考。html
为了方便你们使用 cljs 开发插件,我整理了一份模板,供你们参考。gooreplacer 完整代码在这里,技术栈为 ClojureScript + Reagent + Antd + React-Bootstrap。前端
ClojureScript 是使用 Clojure 编写,最终编译生成 JS 代码的一个编译器,在编译过程当中使用 Google Closure Compiler 来优化 JS 代码、解决模块化引用的问题。总体工做流程以下:java
Cljs 还提供 与原生 JS 的交互、集成第三方类库的支持,因此,只要能用 JS 的地方,都能用 cljs,node
开发 cljs 的环境首选 lein + figwheel,figwheel 相比 lein-cljsbuild 提供了热加载的功能,这一点对于开发 UI 很重要!react
对于通常的 cljs 应用,基本都是用一个 script 标签去引用编译后的 js 文件,而后这个 js 文件再去加载其余依赖。好比:webpack
<html>
<body>
<script type="text/javascript" src="js/main.js"></script>
</body>
</html>复制代码
js/main.js 是 project.clj 里面指定的输出文件,它会去加载其余所需文件,其内容大体以下:git
var CLOSURE_UNCOMPILED_DEFINES = {};
var CLOSURE_NO_DEPS = true;
if(typeof goog == "undefined") document.write('<script src="js/out/goog/base.js"></script>');
document.write('<script src="js/out/goog/deps.js"></script>');
document.write('<script src="js/out/cljs_deps.js"></script>');
document.write('<script>if (typeof goog == "undefined") console.warn("ClojureScript could not load :main, did you forget to specify :asset-path?");</script>');
document.write('<script>goog.require("process.env");</script>');
document.write("<script>if (typeof goog != \"undefined\") { goog.require(\"figwheel.connect.build_dev\"); }</script>");
document.write('<script>goog.require("hello_world.core");</script>');复制代码
对于通常的 Web 项目,只引用这一个 js 文件就够了,可是对于浏览器插件来讲,有一些问题,浏览器插件出于安全因素考虑,是不让执行 incline script,会报以下错误程序员
为了去掉这些错误,手动加载 js/main.js 里面动态引入的文件,require 所需命名空间便可,修改后的 html 以下:
<html>
<body>
<script src="js/out/goog/base.js"></script>
<script src="js/out/cljs_deps.js"></script>
<script src="js/init.js"></script>
</body>
</html>复制代码
其中 init.js 内容为:
// figwheel 用于热加载,这里的 build_dev 实际上是 build_{build_id},默认是 dev
goog.require("figwheel.connect.build_dev");
// 加载为 main 的命名空间
goog.require("hello_world.core");复制代码
这样就能够正常在浏览器插件环境中运行了。能够在 DevTools 中观察到全部引用的 js 文件
在左下角能够看到,总共有 92 个文件。
对于 background page/option page/popup page 这三处均可采用这种措施,可是 content script 无法指定 js 脚本加载顺序,能够想到的一种方式是:
"content_scripts": [{
"matches": ["http://*/*", "https://*/*"],
"run_at": "document_end",
"js": ["content/js/out/goog/base.js", "content/js/out/cljs_deps.js", "content/init.js"]
}]复制代码
这里的 content 的目录与 manifest.json 在同一级目录。采用这种方式会报以下的错误
根据错误提示,能够看出是 base.js 再去动态引用其余 js 文件时,是以访问网站为相对路径开始的,所以也就找不到正确的 JS 文件了。
解决方法是设置 cljsbuild 的 optimizations
为 :whitespace
,把全部文件打包到一个文件,而后引用这一个就能够了,这个方法不是很完美,采用 whitespace 一方面使编译时间更长,在我机器上须要12s;另外一方面是没法使用 figwheel,会报 A Figwheel build must have :compiler > :optimizations default to nil or set to :none 的错误,所以也就没法使用代码热加载的功能。
gooreplacer 里面只使用了 background page 与 option page,因此这个问题也就避免了。
这里的 dev 是指正常的开发流程,release 是指开发完成,准备打包上传到应用商店的过程。
在 dev 过程当中,推荐设置 cljsbuild 的 optimizations
为 none,以便获得最快的编译速度;
在 release 过程当中,能够将其设置为 advanced
,来压缩、优化 js 文件,以便最终的体积最小。
为了在两种模式中复用使用的图片、css 等资源,可采用了软链的来实现,resources 目录结构以下:
.
├── css
│ └── option.css
├── dev
│ ├── background
│ │ ├── index.html
│ │ └── init.js
│ ├── content
│ ├── manifest.json -> ../manifest.json
│ └── option
│ ├── css -> ../../css/
│ ├── images -> ../../images/
│ ├── index.html
│ └── init.js
├── images
│ ├── cljs.png
│ ├── cljs_16.png
│ ├── cljs_32.png
│ └── cljs_48.png
├── manifest.json
└── release
├── background
│ ├── index.html
│ └── js
│ └── main.js
├── content
│ └── js
│ └── main.js
├── manifest.json -> ../manifest.json
└── option
├── css -> ../../css/
├── images -> ../../images/
├── index.html
└── js
└── main.js复制代码
其次,为了方便开启多个 figwheel 实例来分别编译 background、option 里面的 js,定义了多个 lein 的 profiles,来指定不一样环境下的配置,具体可参考 模板的 project.clj 文件。
在 optimizations 为 advanced 时,cljs 会充分借用 Google Closure Compiler 来压缩、混淆代码,会把变量名重命名为 a b c 之类的简写,为了避免使 chrome/firefox 插件 API 里面的函数混淆,须要加载它们对应的 externs 文件,通常只须要这两个 chrome_extensions.js、chrome.js。
cljs 自带的 test 功能比较搓,比较好用的是 doo,为了使用它,须要先提早安装 phantom 来提供 headless 环境,写好测试就能够执行了:
lein doo phantom {build-id} {watch-mode}复制代码
很是棒的一点是它也能支持热加载,因此在开发过程当中我一直开着它。
re-agent 是对 React 的一个封装,使之符合 cljs 开发习惯。毫无夸张的说,对于非专业前端程序员来讲,要想使用 React,cljs 比 jsx 是个更好的选择,Hiccup-like 的语法比 jsx 更紧凑,不用再去理睬 webpack,babel 等等层出不穷的 js 工具,更重要的一点是 immutable 在 cljs 中无处不在,re-agent 里面有本身维护状态的机制 atom,不在须要严格区分 React 里面的 props 与 state。
了解 re-agent 的最好方式就是从它官网给出的示例开始,而后阅读 re-frame wiki 里面的 Creating Reagent Components,了解三种不一样的 form 的区别,98% gooreplacer 都在使用 form-2。若是对原理感兴趣,建议也把其余 wiki 看完。
re-agent 还有一点比较实用,提供了对 React 原生组件的转化函数:adapt-react-class,使用很是简单:
(def Button (reagent/adapt-react-class (aget js/ReactBootstrap "Button")))
[:div
[:h2 "A sample title"]
[Button "with a button"]]复制代码
这样就不用担忧 React 的类库不能在 cljs 中使用的问题了。
说到 re-agent,就不能不提到 om.next,这两个在 cljs 社区里面应该是最有名的 React wrapper,om.next 理念与使用难度均远高于 re-agent,初学者通常不推荐直接用 om.next。感兴趣的能够看看这二者之间的比较:
cljs 里面加载宏的机制有别于 Clojure,通常须要单独把宏定义在一个文件里面,而后在 cljs 里面用(:require-macros [my.macros :as my])
这样的方式去引用,并且宏定义的文件名后缀必须是 clj 或 cljc,不能是 cljs,这一点坑了我很久。。。
因为宏编译与 cljs 编程在不一样的时期,因此若是宏写错了,就须要把 repl 杀掉重启来把新的宏 feed 给 cljs,这点也比较痛苦,由于 repl 的启动速度实在是有些慢。这一点在 Clojure 里面虽然也存在,可是 Clojure 里面通常 repl 开了就不关了,直到电脑重启。
Clojure 里面采用 Emacs + Cider 的开发环境很是完美,可是到了 cljs 里面,开发流程没有那么平滑,老是有些磕磕绊绊,也给 cider 提了个 issue,貌似一直没人理,支持确实很差,不过有了 figwheel,在必定程度上能弥补这个缺陷。在 Emacs 里面配置 repl 可参考:
Cider 默认会使用 rhino 做为 repl 求值环境,这个在开发浏览器插件时功能颇有限,可是对于查看函数定义仍是能够的。能够根据须要换成 figwheel。
ClojureScript 能够算是 Clojure 语言的一个杀手级应用,React 使得后端程序员也能快速做出美观实用的界面。ClojureScript + React,用起来不能再开心啦!
JS 社区里面层出不穷的框架每次都让跃跃欲试的我望而却步,有了 cljs,算是把 Lisp 延伸到了更宽广的“领土”。最近看到这么一句话,与你们分享:
也许 Lisp 不是解决全部问题最合适的语言,可是它鼓励你设计一种最合适的语言来解决这个难题。
出处忘记了,大致是这么个意思。