使用 ClojureScript 开发浏览器插件

栏目: 编程语言 · Clojure · 发布时间: 6年前

内容简介:随着 Firefox 57 的到来,之前维护的一个浏览器插件闲话少说,下面的内容依次会介绍 cljs 的工作机制、开发环境,如何让 cljs 适配浏览器插件规范,以及重写 gooreplacer 时的一些经验。本文的读者需要对 Clojure 语言、浏览器插件开发一般流程有基本了解,并且完成 ClojureScript 的

随着 Firefox 57 的到来,之前维护的一个浏览器插件 gooreplacer 必须升级到 WebExtensions 才能继续使用,看了下之前写的 JS 代码,毫无修改的冲动,怕改了这个地方,那个地方突然就 broken 了。因此,这次选择了 cljs,整体下来流程很顺利,除了迁移之前的功能,又加了更多功能,希望能成为最简单易用的重定向插件 :-)

闲话少说,下面的内容依次会介绍 cljs 的工作机制、开发环境,如何让 cljs 适配浏览器插件规范,以及重写 gooreplacer 时的一些经验。

本文的读者需要对 Clojure 语言、浏览器插件开发一般流程有基本了解,并且完成 ClojureScript 的 Quick Start 。对于 Clojure,我目前在 sf 上有 一套视频课程 ,供参考。

ClojureScript 工作机制

ClojureScript 是使用 Clojure 编写,最终编译生成 JS 代码的一个 编译器 ,在编译过程中使用 Google Closure Compiler 来优化 JS 代码、解决 模块化引用 的问题。整体工作流程如下:

使用 ClojureScript 开发浏览器插件

Cljs 还提供 与原生 JS 的交互集成 第三方类库 的支持,所以,只要能用 JS 的地方,都能用 cljs,

开发环境准备

开发 cljs 的环境首选 lein + figwheel ,figwheel 相比 lein-cljsbuild 提供了热加载的功能,这一点对于开发 UI 很重要!

对于一般的 cljs 应用,基本都是用一个 script 标签去引用编译后的 js 文件,然后这个 js 文件再去加载其他依赖。比如:

<html>
    <body>
        <scripttype="text/javascript"src="js/main.js"></script>
    </body>
</html>

js/main.js 是 project.clj 里面指定的输出文件,它会去加载其他所需文件,其内容大致如下:

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>');

消除 inline script

对于一般的 Web 项目,只引用这一个 js 文件就够了,但是对于浏览器插件来说,有一些问题,浏览器插件出于安全因素考虑,是 不让执行 incline script ,会报如下错误

使用 ClojureScript 开发浏览器插件

为了去掉这些错误,手动加载 js/main.js 里面动态引入的文件,require 所需命名空间即可,修改后的 html 如下:

<html>
    <body>
        <scriptsrc="js/out/goog/base.js"></script>
        <scriptsrc="js/out/cljs_deps.js"></script>
        <scriptsrc="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");

这样就可以正常在浏览器插件环境中运行了。

对于 background page/option page/popup page 这三处都可采用这种措施,但是 content script 没法指定 js 脚本加载顺序,采用的做法是设置 cljsbuild 的 optimizations 为 whitespace,得到一个文件,然后引用这一个就可以了, 这个方法不是很完美 ,采用 whitespace 一方面使编译时间更长,另一方面使失去了热加载的功能,gooreplacer 里面只使用了 background page 与 option page,所以这个问题也就避免了。

区分 dev 与 release 模式

这里的 dev 是指正常的开发流程,release 是指开发完成,准备打包上传到应用商店的过程。

在 dev 过程中,推荐设置 cljsbuild 的 optimizations 为 none,以便得到最快的编译速度;

在 release 过程中,可以将其设置为 advanced ,来压缩、优化 js 文件,以便最终的体积最小。

为了在两种模式中复用使用的图片、css 等资源,gooreplacer 采用了软链的方式,供大家参考:

.
├── css
│   └── main.css
├── dev
│   ├── background
│   │   ├── index.html
│   │   └── init.js
│   ├── manifest.json -> ../manifest.json
│   └── option
│       ├── css -> ../../css
│       ├── img -> ../../img
│       ├── index.html
│       └── init.js
├── img
│   ├── 16.png
│   ├── 32.png
│   ├── 48.png
│   └── gooreplacer.ico
├── manifest.json
└── release
    ├── background
    │   ├── index.html
    │   └── main.js
    ├── manifest.json -> ../manifest.json
    └── option
        ├── css -> ../../css
        ├── img -> ../../img
        ├── index.html
        └── main.js

其次,为了方便开启多个 figwheel 实例来分别编译 background、option 里面的 js,定义了多个 lein 的 profiles,来指定不同环境下的配置,具体可参考 gooreplacer 的 project.clj 文件。

externs

在 optimizations 为 advanced 时,cljs 会充分借用 Google Closure Compiler 来压缩、混淆代码,会把变量名重命名为 a b c 之类的简写,为了不使 chrome/firefox 插件 API 里面的函数混淆,需要加载它们对应的 externs 文件,一般只需要这两个 chrome_extensions.jschrome.js

测试环境

cljs 自带的 test 功能比较搓,比较好用的是 doo ,为了使用它,需要先提前安装 phantom 来提供 headless 环境,写好测试就可以执行了:

lein doo phantom {build-id} {watch-mode}

非常棒的一点是它也能支持热加载,所以在开发过程中我一直开着它。

<html>
    <body>
        <scripttype="text/javascript"src="out/main.js"></script>
    </body>
</html>

re-agent

re-agent 是对 React 的一个封装,使之符合 cljs 开发习惯。毫无夸张的说,对于非专业前端 程序员 来说,要想使用 React,cljs 比 jsx 是个更好的选择, Hiccup-like 的语法比 jsx 更紧凑,不用再去理睬 webpackbabel 等等层出不穷的 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 开了就不关了,直到电脑重启。

使用 ClojureScript 开发浏览器插件

IDE

Clojure 里面采用 Emacs + Cider 的开发环境非常完美,但是到了 cljs 里面,开发流程没有那么平滑,总是有些磕磕绊绊,也给 cider 提了个 issue ,貌似一直没人理,支持确实不好,不过有了 figwheel,在一定程度上能弥补这个缺陷。在 Emacs 里面配置 repl 可参考:

Cider 默认会使用 rhino 作为 repl 求值环境,这个在开发浏览器插件时功能很有限,但是对于查看函数定义还是可以的。可以根据需要换成 figwheel。

总结

ClojureScript 可以算是 Clojure 语言的一个 杀手级 应用 ,React 使得后端程序员也能快速作出美观实用的界面。ClojureScript + React,用起来不能再开心啦!

JS 社区里面层出不穷的框架每次都让跃跃欲试的我望而却步,有了 cljs,算是把 Lisp 延伸到了更宽广的“领土”。最近看到这么一句话,与大家分享:

也许 Lisp 不是解决所有问题最合适的语言,但是它鼓励你设计一种最合适的语言来解决这个难题。

出处忘记了,大体是这么个意思。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

一个人的电商

一个人的电商

许晓辉 / 电子工业出版社 / 2015-5-1 / CNY 59.00

首次披露电商的运营与操盘内幕,徐小平、梁宁作序,雷军、陈彤、张向东、刘韧、王峰力荐! 这个时代在经历前所未有的转型甚至颠覆,任何行业都将与互联网无缝融合,成为“互联网+”。有很多写电商的书,大多都用浓墨重彩阐释互联网转型的必要性,而讲到如何落地实操则浅尝即止,令人心潮澎拜之后不知如何下手。于是有了这本既有方法论,更重视实操细节的书。 许晓辉,在知名电商公司凡客诚品做过高管,有海......一起来看看 《一个人的电商》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

MD5 加密
MD5 加密

MD5 加密工具