zoukankan      html  css  js  c++  java
  • Chrome Extension in CLJS —— 搭建开发环境

    前言

     磨刀不误砍柴工,本篇将介绍如何搭建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=','
    

    用法

    1. 输入([{",会自动生成)]}",并且光标位于其中,vim处于insert状态;
    2. normal模式时,输入<Leader>+W会生成括号包裹住当前光标所在的表达式;
    3. normal模式时,输入<Leader>+w+[会生成[]包裹住当前光标所在的表达式;
    4. 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 ActionPage ActionContent ScriptsBackground等均是相对独立的模块相互并不依存,并且它们运行的方式和环境不尽相同,因此将它们作为独立子项目配置、编译和优化更适合。
      然后新建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.logjs/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

  • 相关阅读:
    VBA基础四:数据库链接(WPS2019)
    VBA基础三:循环(DO...LOOP,)
    随机多人红包
    概率抽奖
    七步轻松实现大数据库表的数据转储
    SQL Server中的行列倒置技巧
    把对应表的字段跨表赋值
    sql内日期格式化输出
    事务的用法
    女孩,你为什么不沉住气奋斗
  • 原文地址:https://www.cnblogs.com/fsjohnhuang/p/7622745.html
Copyright © 2011-2022 走看看