前言
磨刀不误砍柴工,本篇将介绍如何搭建Chrome插件的ClojureScript开发环境。
具体工具栈:vim(paredit,tslime,vim-clojure-static,vim-fireplace) + leiningen(lein-cljsbuild,lein-doo,lein-ancient) + com.cemerick/piggieback
写得要爽
首先抛开将cljs编译为js、调试、测试和发布等问题,首先第一要务是写得爽~
cljs中最让人心烦的就是括号
,过去我想能否改个语法以换行来代替括号呢?而paredit.vim正好解决这个问题。
- ()
安装
在.vimrc中添加
- Plugin 'paredit.vim'
在vim中运行
- :source %
- :PluginInstall
设置- <Leader>
键
- <Leader>
- " 设置<Leader>键
- let mapleader=','
- let g:mapleader=','"
用法
- 输入
、- (
、- [
和- {
,会自动生成- "
、- )
、- ]
和- }
,并且光标位于其中,vim处于insert状态;- "
- normal模式时,输入
会生成括号包裹住当前光标所在的表达式;- <Leader>+W
- normal模式时,输入
会生成- <Leader>+w+[
包裹住当前光标所在的表达式;- []
- normal模式时,输入
会生成- <Leader>+w+"
包裹住当前光标所在的表达式。- ""
更多用法就通过
查看paredit的文档即可。
- :help paredit
编译环境
cljs要被编译为js后才能被运行,这里我采用leiningen。
在shell中运行
- # 创建工程
- $ lein new crx-demo
- $ cd crx-demo
工程目录中的
就是工程文件,我们将其修改如下
- project.clj
- (defproject crx-demo "0.1.0-SNAPSHOT"
- :description "crx-demo"
- :urnl "http://fsjohnhuang.cnblogs.com"
- :license {:name "Eclipse Public License"
- :url "http://www.eclipse.org/legal/epl-v10.html"}
- :dependencies [[org.clojure/clojure "1.8.0"] ;; 通过dependencies声明项目依赖项
- [org.clojure/clojurescript "1.9.908"]]
- :plugins [[lein-cljsbuild "1.1.7"]] ;; 通过plugins声明leiningen的插件,然后就可以通过lein cljsbuild调用lein-cljsbuild这个插件了
- :jvm-opts ["-Xmx1g"] ;; 设置JVM的堆容量,有时编译失败是应为堆太小
- :cljsbuild {:builds
- [{:id "browser_action"
- :source-paths ["src/browser_action"]
- :compiler {:main browser-action.core
- :output-to "resources/public/browser_action/js/ignoreme.js"
- :output-dir "resources/public/browser_action/js/out"
- :asset-path "browser_action/js/out"
- :optimizations :none ;; 注意:为提高编译效率,必须将优化项设置为:none
- :source-map true
- :source-map-timestamp true}}
- {:id "content_scripts"
- :source-paths ["src/content_scripts"]
- :compiler {:main content-scripts.core
- :output-to "resources/public/content_scripts/js/content_scripts.js"
- :output-dir "resources/public/content_scripts/js/out"
- :asset-path "content_scripts/js/out"
- :optimizations :whitespace
- :source-map true
- :source-map-timestamp true}}}]}
- :aliases {"build" ["cljsbuild" "auto" "browser_action" "content_scripts"] ;; 设置别名,那么通过lein build就可一次性编译browser_action和content_scripts两个子项目了。
- })
上述配置很明显我是将browser_action和content_scripts作为两个独立的子项目,其实Chrome插件中Browser Action、Page Action、Content Scripts和Background等均是相对独立的模块相互并不依存,并且它们运行的方式和环境不尽相同,因此将它们作为独立子项目配置、编译和优化更适合。
然后新建resources/public/img目录,并附上名为icon.jpg的图标即可。
&esmp;然后在resources/public下新建manifest.json文件,修改内容如下
- {
- "manifest_version": 2,
- "name": "crx-demo",
- "version": "1.0.0",
- "description": "crx-demo",
- "icons":
- {
- "16": "img/icon.jpg",
- "48": "img/icon.jpg",
- "128": "img/icon.jpg"
- },
- "browser_action":
- {
- "default_icon": "img/icon.jpg",
- "default_title": "crx-demo",
- "default_popup": "browser_action.html"
- },
- "content_scripts":
- [
- {
- "matches": ["*://*/*"],
- "js": ["content_scripts/js/core.js"],
- "run_at": "document_start"
- }
- ],
- "permissions": ["tabs", "storage"]
- }
接下来新建
,并修改内容如下
- resources/public/browser_action.html
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title></title>
- </head>
- <body>
- <script src="browser_action/js/out/goog/base.js"></script>
- <script src="browser_action/js/out/goog/deps.js"></script>
- <script src="browser_action/js/out/cljs_deps.js"></script>
- <script src="browser_action.js"></script>
- </body>
- </html>
到这一步我们会发现哪来的
啊?先别焦急,这里涉及到Browser Action的运行环境与google closure compiler输出不兼容的问题。
- browser_action.js
Browser Action/Popup运行环境
这里有个限制,就是
所指向页面中不能存在内联脚本,而
- default_popup
时google closure compiler会输出如下东东到
- optimizations :none
中
- ignoreme.js
- var CLOSURE_UNCOMPILED_DEFINES = {};
- var CLOSURE_NO_DEPS = true;
- if (typeof goog == "undefined") document.write('<script src="resources/public/browser_action/js/out/goog/base.js"></script>');
- document.write('<script src="resources/public/browser_action/js/out/goog/deps.js"></script>');
- document.write('<script src="resources/public/browser_action/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>goog.require("crx_demo.core");</script>');
这很明显就是加入内联脚本嘛~~~所以我们要手工修改一下,新增一个
,然后添加如下内容
- resources/public/browser_action.js
- goog.require("process.env")
- goog.require("crx_demo.core")
这里我们就搞定Browser Action/Popup的编译运行环境了^_^。大家有没有发现
这一句呢?我们的命名空间名称不是
- goog.require("crx_demo.core")
吗?注意了,编译后不仅路径上
- crx-demo.core
会变成
- -
,连在goog中声明的命名空间名称也会将
- _
变成了
- -
。
- _
Content Scripts运行环境
由于content scripts是直接运行脚本,没有页面让我们如popup那样控制脚本加载方式和顺序,因此只能通过
将所有依赖打包成一个js文件了。也就是说编译起来会相对慢很多~很多~多~~~
- optimizations :whitespace
开发得爽
到这里我们似乎可写上一小段cljs,然后编译运行了。且慢,没有任何智能提示就算了,还时不时要上网查阅API DOC,你确定要这样开发下去?
在vim中查看API DOC
通过vim-fireplace我们可以手不离vim,查阅API文档,和查阅项目代码定义哦!
1.装vim插件
- Plugin 'tpope/vim-fireplace'
在vim中运行
- :source %
- :PluginInstall
2.安装nRepl中间件piggieback
nRepl就是网络repl,可以接收客户端的脚本,然后将运行结果回显到客户端。我们可以通过
启动Clojure的nRepl。 而fireplace则是集成到vim上连接nRepl的客户端,但默认启动的仅仅是Clojure的nRepl,所以要通过中间件附加cljs的nRepl。这是我们只需在project.clj中添加依赖即可。
- lein repl
- :dependencies [[com.cemerick/piggieback "0.2.2"]]
- :repl-options {:port 9000
- :nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}
在shell中更新依赖
3.设置fireplace监听端口 在项目目录下创建文件,
- lein deps
4.启动nRepl,
- echo 9000 > .nreplport
这时在vim中输入
- lein repl
就会看到
- :Source map
的定义,若不行则按如下设置:
- cljs.core/map
- :Connect
- Protocol: nrepl
- Host: localhost
- Port: 9000
- Scope connection to: ~/crx-dome
这样就设置好fireplace和nrepl间的链接了。
5.别开心太早
不知道是什么原因我们只能用fireplace中部分的功能而已,如通过
查看定义,
- :Source <symbol>
查看匹配的Docstring,但无法通过
- :FindDoc <keyword>
来查看某标识的Docstring。 另外若要通过
- :Doc <symbol>
查看当前项目的定义时,请先
- :Source <symbol>
将项目编译好,否则无法查看。另外一个十分重要的信息是,在
- lein build
不为
- optimizations
的项目下的文件是无法执行fireplace的指令的,所以在开发Content Scrpts时就十分痛苦了~~~
- :none
那有什么其他办法呢?不怕有tslime.vim帮我们啊!
tslime.vim
tslime.vim让我们可以通过快捷键将vim中内容快速地复制到repl中执行
1.安装vim插件
- Plugin 'jgdavey/tslime.vim'
在vim中运行
- :source %
- :PluginInstall
2..vimrc配置
- " 设置复制的内容自动粘贴到tmux的当前session和当前window中
- let g:tslime_always_current_session = 1 let g:tslime_always_current_window = 1
- vmap <C-c><C-c> <Plug>SendSelectionToTmux
- nmap <C-c><C-c> <Plug>NormalModeSendToTmux
- nmap <C-c>r <Plug>SetTmuxVars"
3.将clojure repl升级cljs repl
通过
我们建立了一个cljs nrepl供fireplace使用,但在终端中我们看到的是一个clojure的repl,而tslime恰好要用的就是这个终端的repl。那现在我们只要在clojure repl中执行
- lein repl
即可。
- (cemerick.piggieback/cljs-repl (cljs.repl.rhino/repl-env))
然后就可以在vim中把光标移动到相应的表达式上按
,那么这个表达式就会自动复制粘贴到repl中执行了。
- <C-c><C-c>
美化输出
由于cljs拥有比js更为丰富的数据类型,也就是说直接将他们输出到浏览器的console中时,显示的格式会不太清晰。于是我们需要为浏览器安装插件,但通过devtools我们就不用显式为浏览器安装插件也能达到效果(太神奇了!)
在project.clj中加入
- : dependencies[[binaryage / devtools "0.9.4"]];;在要格式化输出的compiler中添加: compiler {: preloads[devtools.preload] : external - config {: devtools / config {: features - to - install[: formatters: hints: async]
- }
- }
- }
然后在代码中通过
、
- js/console.log
等输出的内容就会被格式化的了。
- js/console.info
单元测试很重要
为了保证开发的质量,单元测试怎么能少呢?在project.clj中加入
- : plugins[[lein - doo "0.1.7"]]
然后在
下新建一个runner.cljs文件,并写入如下内容
- test/crx_demo
- (ns crx-demo.runner
- (:require-macros [doo.runners :refer [doo-tests]])
- (:require [crx-demo.content-scripts.util-test]))
- ;; 假设我们要对crx-demo.content-scripts.util下的函数作单元测试,而测试用例则写在crx-demo.content-scripts.util-test中
- (doo-tests 'crx-demo.content-scripts.util-test)
然后创建crx-demo.content-scripts.util-test.cljs测试用例文件
- (ns crx-demo.content-scripts.util-test
- (:require-macros [cljs.test :refer [deftest is are testing async]]
- (:require [crx-demo.content-scripts.util :as u]))
- (deftest test-all-upper-case?
- (testing "all-upper-case?"
- (are [x] (true? x)
- (u/all-upper-case? "abCd")
- (u/all-upper-case? "ABCD"))))
- (deftest test-all-lower-case?
- (testing "all-lower-case?"
- (is (true? (u/all-lower-case? "cinG")))))
- (deftest test-get-async
- (async done
- (u/get-async (fn [item]
- (is (seq item))
- (done)))))
然后再新增一个测试用的子项目
- {:id "test-proj"
- :source-paths ["src/content_scripts" "test/crx_demo"]
- :compiler {:target :nodejs ;;运行目标环境是nodejs
- :main crx-demo.runner
- :output-to "out/test.js"
- :output-dir "out/out"
- :optimizations :none
- :source-map true
- :source-map-timestamp true}}
然后在shell中输入
- lein doo node test-proj
发布前引入externs
辛苦开发后我们将
设置为
- optimizations
后编译优化,将作品发布时发现类似于如下的报错
- advanced
- Uncaught TypeError: sa.B is not a function
这究竟是什么回事呢?
开发时最多就是将
设置为
- optimizations
,这时标识符并没有被压缩,所以如
- simple
等外部定义标识符依然是原装的。但启用
- chrome.runtime.onMessage.addListener
编译模式后,由于上述外部标识符的定义并不纳入GCC的编译范围,因此GCC仅仅将调用部分代码压缩了,而定义部分还是原封不动,那么在运行时调用中自然而然就找不到相应的定义咯。Cljs早已为我们找到了解决办法,那就是添加extern文件,extern文件中描述外部函数、变量等声明,那么GCC根据extern中的声明将不对调用代码中同签名的标识符作压缩。 示例:chrome的extern文件chrome.js片段
- advanced
- /**
- * @constructor
- */
- function MessageSender() {}
- /** @type {!Tab|undefined} */
- MessageSender.prototype.tab
配置
1.访问https://github.com/google/closure-compiler/tree/master/contrib/externs,将chrome.js和chrome_extensions.js下载到项目中的externs目录下
2.配置project.clj文件
- :compiler {:externs ["externs/chrome.js" "externs/chrome_extensions.js"]}
总结
最后得到的project.clj为
- (defproject crx-demo "0.1.0-SNAPSHOT"
- :description "crx-demo"
- :urnl "http://fsjohnhuang.cnblogs.com"
- :license {:name "Eclipse Public License"
- :url "http://www.eclipse.org/legal/epl-v10.html"}
- :dependencies [[org.clojure/clojure "1.8.0"]
- [org.clojure/clojurescript "1.9.908"]
- [binaryage/devtools "0.9.4"]
- [com.cemerick/piggieback "0.2.2"]]
- :plugins [[lein-cljsbuild "1.1.7"]
- [lein-doo "0.1.7"]
- [lein-ancient "0.6.12"]] ;; 通过`lein ancient upgrade` 或 `lein ancient upgrade:plugins`更新依赖项
- :clean-targets ^{:protect false} [:target-path "out" "resources/public/background" "resources/public/content_scripts" "resources/public/browser_action"]
- :jvm-opts ["-Xmx1g"]
- :repl-options {:port 9000
- :nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}
- :cljsbuild {:builds
- [{:id "browser_action"
- :source-paths ["src/browser_action"]
- :compiler {:main browser-action.core
- :output-to "resources/public/browser_action/js/ignoreme.js"
- :output-dir "resources/public/browser_action/js/out"
- :asset-path "browser_action/js/out"
- :optimizations :none
- :source-map true
- :source-map-timestamp true
- :externs ["externs/chrome.js" "externs/chrome_extensions.js"]
- :preloads [devtools.preload]
- :external-config {:devtools/config {:features-to-install [:formatters :hints :async]}}}}
- {:id "content_scripts"
- :source-paths ["src/content_scripts"]
- :compiler {:main content-scripts.core
- :output-to "resources/public/content_scripts/js/content_scripts.js"
- :output-dir "resources/public/content_scripts/js/out"
- :asset-path "content_scripts/js/out"
- :optimizations :whitespace
- :source-map true
- :source-map-timestamp true
- :externs ["externs/chrome.js" "externs/chrome_extensions.js"]
- :preloads [devtools.preload]
- :external-config {:devtools/config {:features-to-install [:formatters :hints :async]}}}}]}
- :aliases {"build" ["cljsbuild" "auto" "browser_action" "content_scripts"]
- "test" ["doo" "node" "test-proj"]})