ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

在QuickJS上运行ClojureScript:Cherry编译器与轻量级运行时实践

在QuickJS上运行ClojureScript:Cherry编译器与轻量级运行时实践 最近在 Hacker News 上看到一个很有意思的项目Choq它把 Cherry一个 ClojureScript 编译器直接运行到了 QuickJS 上。乍看像一组技术名词的排列组合但组合背后的思路非常实用——在没有 JVM、没有 Node.js、甚至只有几 MB 可用空间的设备上依然可以用 ClojureScript 编写逻辑并运行起来。如果你之前接触过 ClojureScript应该知道它平时绕不开 Java 虚拟机无论是 IDE 热加载、Clojure CLI 还是 lein 命令背后都依赖 JVM。而 QuickJS 是一个极轻量的 JavaScript 引擎启动快、内存占用小非常适合嵌入场景。Cherry 则让 ClojureScript 的编译过程不再依赖 JVM。Choq 把两者合在一起等于把一条完整的 ClojureScript 工具链搬进了轻量级 JavaScript 运行时里。本文会围绕 Choq 项目展开先讲清楚 QuickJS、Cherry、Choq 各自是什么再解释“ClojureScript 如何变成 JavaScript、JavaScript 又如何被 QuickJS 执行”的整套链路最后给出一个可复制的实战流程并整理常见问题与工程建议。无论你是 ClojureScript 爱好者还是对轻量级脚本引擎感兴趣的后端、嵌入式开发者这篇文章都能帮你少走弯路。1. 背景与核心概念1.1 QuickJS一个能嵌进任何地方的小型 JS 引擎QuickJS 是一个用 C 语言编写的 JavaScript 引擎由法国程序员 Fabrice Bellard 开发。它的最大特点是小而快。相对于 V8Chrome 和 Node.js 使用的引擎QuickJS 的二进制体积小得多启动时间也短得多因此特别适合嵌入式设备、边缘计算、IoT 场景或者作为应用程序内置的脚本引擎使用。QuickJS 的另一个优势是它支持 ES2020 标准日常用到的Promise、async/await、解构赋值、箭头函数、模板字符串等特性都能正常使用。它提供了 C 语言 API方便把 JavaScript 嵌进原生应用同时也提供了一个独立的qjs命令行程序可以直接运行.js文件操作方式类似node xxx.js。简单理解如果 Node.js 是一个“什么都带上的重型工具箱”那 QuickJS 就是一把“锋利的折叠刀”。你不需要把整个 Node 运行时塞进设备只要带上 QuickJS 的二进制就能执行 JavaScript 代码。1.2 Cherry去掉 JVM 的 ClojureScript 编译器ClojureScript 是 Clojure 语言在前端领域的方言最终会被编译成 JavaScript。传统的 ClojureScript 工具链很强大但有一个让很多初学者头疼的问题重度依赖 JVM。不管是cljs.main还是 shadow-cljs安装和运行前都要先装好 Java 环境内存占用也经常被吐槽。Cherry 是一个致力于改变这种状况的编译器。它的目标是让 ClojureScript 的编译过程不依赖 JVM直接在 JavaScript 环境中完成编译从而把编译产物输出为 JavaScript。这带来了几个直接好处环境准备简单不需要安装 JDK。编译过程可以在 Node.js、浏览器、甚至嵌入式 JavaScript 引擎中运行。适合在资源受限的环境中做轻量级脚本开发。需要说明的是Cherry 并不是 Clojure 语言的完整企业级实现它更接近一个可用的子集实现核心语法和常用数据结构的处理已经比较完善但生态和兼容性仍处于快速演进阶段。1.3 Choq把 Cherry 装进 QuickJS 的实验Choq 的价值在于它不再依赖 Node.js 来运行 Cherry而是让 Cherry 编译器本身跑在 QuickJS 上。换句话说Choq 把一个可用的 ClojureScript 开发环境沉淀到了一个极小体积的运行时里。这里的核心链路是你编写.cljs文件。Cherry 把.cljs编译成 JavaScript 文件。QuickJS 负责执行生成的 JavaScript。如果浏览器是 ClojureScript 的前端阵地Node.js 是服务端阵地那 QuickJS 就是嵌入式、边缘设备、资源受限环境里的一块新阵地。Choq 让开发者在这块阵地上可以用 ClojureScript 语法编写逻辑而不必引入笨重的运行时。2. 环境准备与版本说明动手之前先准备环境。因为 Choq、Cherry、QuickJS 都处于迭代期具体版本号很容易变化所以我这里不写死某个版本而是给出通用准备思路。大家在操作时以你实际拉取到的项目文档和版本号为准。2.1 操作系统与基础工具本文示例在 Linux/macOS 环境下编写Windows 用户可以通过 WSL 或 Git Bash 完成大部分操作。你需要准备一个终端命令行工具。git用于拉取项目源码。可选node和npm用来运行基于 JavaScript 的辅助脚本。如果只有 QuickJS没有 Node.js也不影响最终目标。很多步骤可以完全脱离 Node.js 完成。2.2 安装 QuickJSQuickJS 官方仓库在 GitHub 上可以直接编译安装。大致流程如下# 拉取 QuickJS 源码 git clone https://github.com/bellard/quickjs.git cd quickjs # 编译 make # 生成可执行文件 qjs ls qjs如果机器上有包管理器也可以直接搜索安装。例如在 macOS 上可以用 Homebrew 安装brew install quickjs安装完成后执行以下命令验证qjs --version如果能看到版本号输出说明 QuickJS 已经可用。需要注意的是不同平台上的qjs命令名称可能略有差异。有的发行版叫qjs有的叫quickjs还有的可能直接安装在系统路径里。按实际环境调整即可。2.3 获取 Cherry 与 ChoqCherry 项目的源码托管在 GitHub 上搜索rumble-shack/cherry通常就能找到。Choq 项目则可以从它的发布页或仓库获取。获取方式通常是# 以 Cherry 为例拉取源码 git clone https://github.com/rumble-shack/cherry.git cd cherry # 按项目文档安装依赖或构建 # 具体命令需要参照 READMEChoq 作为把 Cherry 和 QuickJS 整合的项目一般会提供明确的构建与运行说明。建议优先读 README再动手操作。这类新项目往往更新很快README 里的一行命令可能就是最新可用状态。2.4 版本确认与依赖关系我在写这篇文章时不打算给出一组“精确到小版本”的依赖组合原因很简单这些工具的迭代速度太快。更好的做法是建立一个检查清单QuickJS 版本是否支持 ES2020大部分较新版本都支持。Cherry 构建时需要的工具链是否已经具备Choq 项目是否明确说明它对应的 Cherry 版本版本兼容问题在开源新项目里非常常见如果你遇到编译报错首先要检查的就是版本是否匹配。3. 核心运行原理3.1 ClojureScript 的编译模型要理解 Choq得先理解 ClojureScript 的编译模型。ClojureScript 源代码是.cljs文件语法与 Clojure 高度一致大量使用括号表达式。例如(defn greet [name] (str Hello, name !)) (println (greet QuickJS))这段代码不能直接运行因为 JavaScript 引擎不理解defn、str、println这些 Clojure 语法。编译器的任务就是把它们翻译成等价的 JavaScript 代码。标准 ClojureScript 编译器会经历几个阶段读取 Reader把.cljs文件解析成数据结构。宏展开处理defmacro等宏定义。分析对代码进行语义分析。代码生成输出 JavaScript 代码。传统工具链中这些步骤运行在 JVM 环境里而 Cherry 用纯 JavaScript 重新实现了这套流程因此在 Node.js 或浏览器环境中也能完成编译。3.2 Cherry 与标准 ClojureScript 的差异Cherry 作为轻量实现不可能 100% 覆盖标准 ClojureScript 的所有功能。你需要重点关注的差异包括标准库范围不同。一些依赖 Java 平台能力的命名空间在 Cherry 中可能没有实现。宏系统实现深度不同。与 Google Closure 编译器的集成程度不同。标准 ClojureScript 默认依赖 Google Closure 做模块打包和优化Cherry 的实现路径更直接产物更接近普通 JavaScript。因此用 Cherry 编写代码时建议优先使用基础语法和数据结构比如defn、let、map、vector、str、js/console.log等。涉及复杂高级宏时需要先确认 Cherry 是否支持。3.3 QuickJS 如何执行编译产物Cherry 编译产物是 JavaScript 文件本质上是普通脚本。QuickJS 执行它时并不关心它来自 ClojureScript 还是手写 JavaScript只要语法符合 ES2020 规范即可。但有一个关键点需要留意ClojureScript 编译产物可能会使用 CommonJS 模块、动态eval或者在运行时检查宿主对象。QuickJS 对这些特性的支持与 Node.js 有差异。比如require模块系统不是 QuickJS 的默认行为。某些全局对象如Buffer在 QuickJS 中不存在。Promise和async/await支持良好但性能与 V8 有差距。如果你在 Node.js 里能跑通拿到 QuickJS 里却报错优先排查这三个方向模块加载方式、全局对象依赖、ES 语法版本。3.4 Choq 的整合思路Choq 做的事情可以理解成一个“装配层”把 Cherry 编译器先通过其自身编译为 JavaScript 文件。让 QuickJS 加载这个 JavaScript 文件。在 QuickJS 内部调用 Cherry 编译接口把.cljs源码编译成 JavaScript。再执行编译结果。这样做的好处是部署简单只需准备一个 QuickJS 可执行文件加上 Cherry 的编译产物文件就拥有了一套 ClojureScript 编写和运行环境。对于嵌入式系统这意味着可以把动态脚本能力集成进一个非常小的运行时里。4. 完整实战在 QuickJS 上编译并运行 ClojureScript下面我们来做一次完整的实操。为了不依赖某个不稳定命令我会把流程拆成三个部分编写.cljs源码、使用 Cherry 编译、使用 QuickJS 运行。命令细节以你实际获取的工具为准整体思路是一致的。4.1 创建项目结构先创建一个项目目录用于存放源码和编译产物mkdir choq-demo cd choq-demo mkdir src mkdir out目录结构如下choq-demo/ ├── src/ # 存放 .cljs 源码 └── out/ # 存放编译产物 .js4.2 编写 ClojureScript 源码在src目录下创建一个main.cljs文件内容是一个简单的递归求和函数;; 文件路径src/main.cljs (ns demo.main) (defn sum-to [n] (if (zero? n) 0 ( n (sum-to (dec n))))) (defn -main [] (let [result (sum-to 100)] (js/console.log sum 1..100 result)))这段代码做了以下几件事定义命名空间demo.main。用defn定义递归函数sum-to。在-main函数中调用sum-to计算 1 到 100 的和。使用js/console.log打印结果。注意js/console.log的写法ClojureScript 通过js/前缀访问 JavaScript 全局对象这与在 Node.js 里直接写console.log是一个意思。4.3 使用 Cherry 编译Cherry 编译命令的形式可能随版本变化。通常思路是拉起一个 JS 入口传入.cljs文件路径指定输出目录。假设你已经在 Cherry 项目中构建完成命令可能类似于# 这是一个参考命令具体参数以 Cherry 项目 README 为准 node cli.js -c src/main.cljs -o out/main.js如果 Choq 提供了封装好的入口也可能简化为# Choq 封装命令示例按项目实际说明执行 choq src/main.cljs -o out/main.js执行后把编译产物放到out/main.js。编译产物的 JavaScript 大概会是这样的思路// 文件路径out/main.js示意产物实际输出取决于编译器版本 function sumTo(n) { if (n 0) { return 0; } return n sumTo(n - 1); } function main() { var result sumTo(100); console.log(sum 1..100 result); } main();如果你看到的产物不是这种形式也没关系。关键是它是一段可以被 JavaScript 引擎直接执行的代码。4.4 使用 QuickJS 运行编译好之后用qjs运行它qjs out/main.js预期输出sum 1..100 5050如果能看到这个结果说明从 ClojureScript 源码到 QuickJS 执行的整条链路已经打通了。这一步验证了最核心的能力ClojureScript 代码可以脱离 JVM 和 Node.js在极轻量的 QuickJS 上运行。4.5 带异步操作的示例为了验证 QuickJS 对异步能力的支持我们再写一个稍微进阶的例子使用Promise模拟异步任务;; 文件路径src/async-demo.cljs (ns demo.async) (defn delayed-sum [a b] (js/Promise. (fn [resolve reject] (js/setTimeout (fn [] (resolve ( a b))) 100)))) (defn -main [] (- (delayed-sum 20 22) (.then (fn [result] (js/console.log async result result)))))这个例子中js/Promise.创建了一个 Promise 对象。js/setTimeout模拟异步延迟。-是 Clojure 的线程宏简化链式调用。.then注册回调函数。编译运行后预期输出async result 42这个案例说明QuickJS 不仅支持同步计算也支持基于事件循环的异步任务。不过要注意QuickJS 的事件循环机制与 Node.js 不完全相同setTimeout的行为在一些边缘场景下可能有差异。5. 常见问题与排查思路5.1 qjs 命令找不到问题现象常见原因解决思路qjs: command not foundQuickJS 未编译或未加入 PATH检查编译目录确认qjs文件存在用绝对路径执行系统报错缺少组件编译 QuickJS 时缺少 C 编译器安装 gcc/clang 后重新 make在 Linux 上很多发行版的 QuickJS 包名叫quickjs执行命令可能是quickjs而不是qjs。可以这样检查which qjs which quickjs5.2 编译产物在 QuickJS 中报 SyntaxErrorClojureScript 或 Cherry 生成的 JavaScript 可能包含较新的语法比如?.可选链、??空值合并、class字段等。如果 QuickJS 版本较旧可能不支持。排查步骤查看报错文件与行号。判断是哪种语法不支持。升级 QuickJS 到最新版本。或者调整 Cherry 编译目标让它生成更保守的 JavaScript 代码。5.3 require 模块加载失败Cherry 编译产物如果依赖 CommonJS 的require在 QuickJS 中可能无法直接运行因为 QuickJS 默认不提供 Node.js 风格的模块加载器。解决思路有几种在编译阶段把依赖打包进单一文件。手动给 QuickJS 提供对应的require实现。使用 QuickJS 自带的模块支持例如import语法但需要文件扩展名和模块路径符合要求。这通常是嵌入式场景中最容易踩的坑建议尽量编译成单文件产物。5.4 递归或循环过大导致栈溢出QuickJS 的栈空间与 V8 不同深层递归更容易触发栈溢出。ClojureScript 风格里常见的递归写法在 QuickJS 中可能跑不了太深的深度。优化方向改用迭代算法。使用loop/recur这种尾递归形式但需要确认 Cherry 和 QuickJS 是否做了尾调用优化。对于明确有边界的递归控制参数规模。5.5 console 对象不可用如果js/console.log在 QuickJS 里报console is not defined说明当前 QuickJS 编译版本没有启用 console 扩展。开发者版qjs一般自带console但某些嵌入版本需要你自己提供。解决方案是在运行前注入一个最简单的 console 对象qjs -e globalThis.console{log:print,warn:print,error:print}; out/main.jsQuickJS 自带print()函数可以临时替代console.log。6. 最佳实践与工程建议6.1 把 Choq 定位成“嵌入式脚本引擎”而不是 Node 替代品Choq 的意义在于给资源受限环境提供 ClojureScript 能力而不是和 Node.js 比拼生态。如果项目本身体积不受限、依赖复杂直接用标准 ClojureScript 工具链或 Node.js 更稳妥。Choq 适合的场景是嵌入式设备需要动态更新业务逻辑。设备中没有 JVM也不想为一个小功能引入完整 Node.js 运行时。希望用 ClojureScript 统一前后端甚至设备端开发语言。6.2 保持源码、编译产物和运行时的分离在工程组织上建议把.cljs源码、编译产出的.js文件、QuickJS 运行时三者分开管理。源码进版本库编译产物由 CI 流程生成运行时通过发行包或镜像管理。这样可以避免在设备上直接修改源码提高可维护性。6.3 谨慎依赖运行时全局对象ClojureScript 代码中访问js/全局对象时要先确认目标环境是否存在。console、setTimeout、Promise这些在 QuickJS 中通常可用但Buffer、process、global等 Node.js 专有对象就不一定了。建议在编码规范中约定只使用 ES2020 标准全局对象。不直接访问 Node.js 专有 API。确实需要平台能力时通过注入的方式由宿主提供。6.4 通过 CI 验证两次运行实际项目中建议在 CI 中同时跑两遍验证在 Node.js 中编译并运行验证 ClojureScript 代码逻辑正确。使用 QuickJS 运行同一份编译产物验证目标环境兼容。原因很简单开发者的本地环境通常有 Node.js而目标设备上是 QuickJS。两遍验证可以尽早发现模块加载、全局对象、语法兼容性问题。6.5 编译产物要记录版本信息Choq、Cherry、QuickJS 都处于快速迭代阶段。建议在编译产物的文件头加入版本注释或者在构建脚本中把版本信息写入输出文件方便后续排错。例如// generated by cherry 0.x.x on 2025-01-01 // target: quickjs 2024-xx-xx这样做能在运行异常时快速定位问题是源码逻辑、编译器版本还是运行时版本导致的。7. 总结与学习路线通过阅读本文你应该已经理解了 Choq 的核心链路ClojureScript 源码通过 Cherry 编译为 JavaScript再交给 QuickJS 执行。这条链路让 ClojureScript 脱离 JVM 和 Node.js进入轻量级运行时领域为嵌入式、边缘计算和低资源设备上的动态脚本需求提供了新的思路。如果你准备深入学习建议按以下顺序推进先掌握 ClojureScript 的基础语法特别是defn、let、loop/recur、map和reduce。自己动手编译一次 Cherry观察它生成的 JavaScript 产物加深对编译过程的理解。阅读 QuickJS 的 C API 文档了解如何把qjs嵌入到你的原生应用里。关注 Choq 的后续更新看它如何解决模块加载、宏兼容、调试体验等问题。尝试做一个真实的小项目比如在开发板上用 ClojureScript 控制 GPIO 或读取传感器数据把整条链路用起来。这类项目目前还处于早期阶段功能和稳定性与传统工具链有差距它的价值更多在于探索一种“更轻的 ClojureScript 运行方式”。如果你对资源受限环境下的脚本化开发感兴趣这会是一个很值得投入的研究方向。
返回列表