前言
磨刀不誤砍柴工,本篇將介紹如何搭建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>鍵
let mapleader=','
let g:mapleader=','
用法
- 輸入
(
、[
、{
和"
,會自動生成)
、]
、}
和"
,並且游標位於其中,vim處於insert狀態; - normal模式時,輸入
<Leader>+W
會生成括號包裹住當前游標所在的表示式; - normal模式時,輸入
<Leader>+w+[
會生成[]
包裹住當前游標所在的表示式; - normal模式時,輸入
<Leader>+w+"
會生成""
包裹住當前游標所在的表示式。
更多用法就通過:help paredit
檢視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.js
啊?先別焦急,這裡涉及到Browser Action的執行環境與google closure compiler輸出不相容的問題。
Browser Action/Popup執行環境
這裡有個限制,就是default_popup
所指向頁面中不能存在內聯指令碼,而optimizations :none
時google closure compiler會輸出如下東東到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那樣控制指令碼載入方式和順序,因此只能通過optimizations :whitespace
將所有依賴打包成一個js檔案了。也就是說編譯起來會相對慢很多~很多~多~~~
開發得爽
到這裡我們似乎可寫上一小段cljs,然後編譯執行了。且慢,沒有任何智慧提示就算了,還時不時要上網查閱API DOC,你確定要這樣開發下去?
在vim中檢視API DOC
通過vim-fireplace我們可以手不離vim,查閱API文件,和查閱專案程式碼定義哦!
1.裝vim外掛
Plugin 'tpope/vim-fireplace'
在vim中執行
:source %
:PluginInstall
2.安裝nRepl中介軟體piggieback
nRepl就是網路repl,可以接收客戶端的指令碼,然後將執行結果回顯到客戶端。我們可以通過lein repl
啟動Clojure的nRepl。
而fireplace則是整合到vim上連線nRepl的客戶端,但預設啟動的僅僅是Clojure的nRepl,所以要通過中介軟體附加cljs的nRepl。這是我們只需在project.clj中新增依賴即可。
:dependencies [[com.cemerick/piggieback "0.2.2"]]
:repl-options {:port 9000
:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}
在shell中更新依賴lein deps
3.設定fireplace監聽埠
在專案目錄下建立檔案,echo 9000 > .nreplport
4.啟動nRepl,lein repl
這時在vim中輸入:Source map
就會看到cljs.core/map
的定義,若不行則按如下設定:
:Connect
Protocol: nrepl
Host: localhost
Port: 9000
Scope connection to: ~/crx-dome
這樣就設定好fireplace和nrepl間的連結了。
5.別開心太早
不知道是什麼原因我們只能用fireplace中部分的功能而已,如通過:Source <symbol>
檢視定義,:FindDoc <keyword>
檢視匹配的Docstring,但無法通過:Doc <symbol>
來檢視某標識的Docstring。
另外若要通過:Source <symbol>
檢視當前專案的定義時,請先lein build
將專案編譯好,否則無法檢視。另外一個十分重要的資訊是,在optimizations
不為:none
的專案下的檔案是無法執行fireplace的指令的,所以在開發Content Scrpts時就十分痛苦了~~~
那有什麼其他辦法呢?不怕有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
通過lein repl
我們建立了一個cljs nrepl供fireplace使用,但在終端中我們看到的是一個clojure的repl,而tslime恰好要用的就是這個終端的repl。那現在我們只要在clojure repl中執行(cemerick.piggieback/cljs-repl (cljs.repl.rhino/repl-env))
即可。
然後就可以在vim中把游標移動到相應的表示式上按<C-c><C-c>
,那麼這個表示式就會自動複製貼上到repl中執行了。
美化輸出
由於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"]]
然後在test/crx_demo
下新建一個runner.cljs檔案,並寫入如下內容
(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
等外部定義識別符號依然是原裝的。但啟用advanced
編譯模式後,由於上述外部識別符號的定義並不納入GCC的編譯範圍,因此GCC僅僅將呼叫部分程式碼壓縮了,而定義部分還是原封不動,那麼在執行時呼叫中自然而然就找不到相應的定義咯。Cljs早已為我們找到了解決辦法,那就是新增extern檔案,extern檔案中描述外部函式、變數等宣告,那麼GCC根據extern中的宣告將不對呼叫程式碼中同簽名的識別符號作壓縮。
示例:chrome的extern檔案chrome.js片段
/**
* @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"]})
隨著對cljs的應用的深入,我會逐步完善上述配置^_^
尊重原創,轉載請註明來自:http://www.cnblogs.com/fsjohnhuang/p/7622745.html ^_^肥仔John
參考
http://astashov.github.io/blog/2014/07/30/perfect-clojurescript-development-environment-with-vim/
https://github.com/emezeske/lein-cljsbuild
https://nvbn.github.io/2014/12/07/chrome-extension-clojurescript/
https://github.com/binaryage/cljs-devtools/blob/master/docs/configuration.md
https://clojurescript.org/tools/testing
https://github.com/google/closure-compiler/wiki/Externs-For-Common-Libraries