用仓颉写桌面应用:从 npx 建工程到串口实时示波器

发布时间:2026/10/14 2:21:04
用仓颉写桌面应用:从 npx 建工程到串口实时示波器 上周我把cj-tauri仓库里第 17 个示例收尾了——一个串口实时波形示波器选好设备与波特率点连接样本流进来页面上的波形立刻开始滚它自己算速率、上穿次数和丢帧跑完给自己一个结论。这件事在浏览器里做不成。Web 平台至今没有通用的串口 APIWeb Serial 只在部分 Chromium 内核里落地还得用户每次点授权而「采集到的东西实时变成波形」恰恰是串口类桌面工具最直观的门面形态。所以拿它当引子正合适这类应用正是混合桌面框架存在的理由——界面交给 Web 技术浏览器碰不到的那部分交给原生后端。cj-tauri项目地址https://atomgit.com/qq8864/cj-tauricj-tauri的skills技能https://gitcode.com/qq8864/cj-tauri/tree/main/skills这篇文章分两半前半说清 cj-tauri 是什么、怎么用 npm 在几分钟内起一个工程后半用这个示波器把「从零到实机取证」的闭环完整走一遍。cj-tauri 是什么一句话用华为仓颉语言写的类 Tauri 框架——前端是系统 WebView 里的 HTML/CSS/JS后端是仓颉静态编译出的原生可执行文件中间靠一套 IPC 协议和一张能力白名单连起来。它没有捆绑 Chromium。Linux 用的是系统里的 WebKitGTKWindows 用的是系统里的 WebView2Win11 自带所以产物是几 MB 级的 exe跟动辄上百 MB 的 Electron 应用不是一个量级。这张图从上往下就是一次invoke的完整路径中间有三处最容易看错的地方我在图上直接标了授权是在调用线程上同步拒绝的。invoke一个没注册或没放行的命令当场就回错误command not registered/command not allowed不会先排进队列再说通过校验的命令才被扔到 worker 线程执行。所以慢命令跑子进程、开原生对话框不会把窗口冻住同一个命令被并发调用不保证执行顺序前端按 promise id 配对自己的结果结果和事件是异步回投的图右侧那条虚线。事件是后端主动推给页面的走的是另一条路。最底下那个native/core/bridge_core.c是跨平台共享层桥接脚本的注入、报文解析、能力校验都在这儿跟 GTK-free 的平台实现分开——平台差异只允许出现在仓颉侧的When条件编译和native/platforms/平台/两处上层代码一份。平台状态我按实测说不按计划说平台状态WindowsWebView2已实机跑通菜单栏、拖放、文本框/剪贴板这类宿主能力都有实机证据LinuxWebKitGTK已实机跑通CI 与本机自检都在这个平台上跑鸿蒙ArkWeb架构预留目录与导出契约在能力位如实上报 0没有实机验证过仓库里现在有 17 个可直接运行的示例、8 个官方插件文件、对话框、子进程、串口、键值存储、剪贴板、通知、菜单加上三个脚手架模板。够撑起「工具类桌面应用」这一档的日常需求。用 npm 起一个工程真要说上手成本最省事的是这一条npx cj-tauri create myapp# 免安装直接跑cdmyapp npx cj-tauri dev# 构建 起窗口Vue/React 模板还会接管 Vite dev server前置条件就三样仓颉 SDK1.0.5 以上我用 1.2.0 验证与配套的stdxWindows 侧另需 mingw gcc 和 WebView2 RuntimeWin11 自带Linux 侧需libwebkit2gtk-4.1-dev、gcc 与 pkg-config。SDK 从仓颉官网下载下载页需要登录华为账号装好后把CANGJIE_HOME指向 SDK 根目录即可剩下的环境变量拼装 CLI 会替你做。create出来的工程是这几个文件跟「前端工程」的心智模型差别不大myapp/ ├── cjpm.toml # 仓颉包描述名字、版本、依赖、链接选项 ├── capabilities/ │ └── default.json # 能力清单哪些命令/事件对页面放行 ├── src/ │ └── main.cj # 后端注册命令、装配窗口、启动 └── ui/ └── index.html # 前端页面零 Node 版本就是这一个文件模板里的main.cj不长骨架就三件事——装配、注册、启动main():Int64{letappTauriApp().window(WindowConfig(cj-tauri 应用,1000,700)).register(greet,GreetCommand())// 命令页面 invoke(greet, {...}).register(timer,TimerCommand())// 定时器后端主动 emit(tick, ...).plugin(MenuPlugin())// 插件一行接入原生菜单栏.setMenu(demoMenu())// 菜单栏要在启动前定稿lethtmlString.fromUtf8(File.readFrom(ui/index.html))app.run(html)// 阻塞到窗口关闭return0}前端这一侧只有两个动词invoke和listenwindow.__CJ_TAURI__.invoke(greet,{name:仓颉}).then(dconsole.log(d));window.__CJ_TAURI__.listen(tick,pconsole.log(p.n));// 后端每秒推一次这里有个新手最容易踩的点桥不是页面自己加载的是宿主在页面脚本之前注入的。所以页面里不要一上来就在模块顶层读window.__CJ_TAURI__等它出现再用模板里那个waitForBridge就是干这个的示波器页面也一样functionwaitForBridge(cb){if(window.__CJ_TAURI__window.__CJ_TAURI__.serial){cb();return;}setTimeout(function(){waitForBridge(cb);},25);}npm 这个包装里到底做了什么npx免安装是真的但仓颉 SDK 是硬前置——应用的后端就是仓颉代码没有 SDK 连编译都做不了。所以 npm 包里的入口脚本bin/cj-tauri.js干的主要是环境这件事按三级顺序找 CLI 本体包内预编译prebuilt/平台-架构/cj-tauri——打 tarball 的机器上有产物就带上这一档用户什么都不用装除了 SDK本机缓存~/.cache/cj-tauri/版本/平台-架构/——第一次构建过就复用源码构建——把包里的cli/拷到缓存目录再cjpm build。产物刻意不写进包的安装目录因为全局安装的 node_modules 可能只读。顺带把 PATH /LD_LIBRARY_PATH拼好Linux 上仓颉运行时的动态库不查 PATH少了LD_LIBRARY_PATH会以「找不到libcangjie-runtime.so」直接退出再把退出码原样传回来。没装 SDK 的时候它不会甩你一句cjpm: command not found而是告诉你缺什么、CANGJIE_HOME该怎么设、去哪看安装说明。有意思的是npx cj-tauri --version在没装 SDK的机器上也照样能打出 0.8.0——版本号直接读package.json不经过仓颉侧。如果你连 SDK 都不想装还有一条旁路仓库随版本分发免安装、免 SDK 的单文件 exe比如 Modbus 调试工具 5.6 MB、观影应用 9.5 MB。这四个产物我逐个量过PE 导入表里只有 Windows 系统 DLL没有任何第三方运行时。口径与复核方式都写在docs/分发形态实测.md里有兴趣可以自己跑一遍。第一件事先跑 doctor新机器上我会建议先跑这个npx cj-tauri doctor它逐行给结论每一条都自带「缺了怎么办」——框架版本、工程产物、stdx、SDK、cjpm、C 桥谁没解析到都在这一屏里。另一条自检命令是npx cj-tauri info --json它输出机器可读的插件装配清单用起来更像给 CI 的接口。实战串口实时波形示波器examples/serial-waveform。先说为什么值得再做一个串口示例仓库里已经有一个串口调试助手收发日志和一个 Modbus 调试工具轮询取值可这两个页面一行 canvas 都没有——它们都是文本工具。「采集到的东西实时变成波形」这一档是空白而它恰好是串口类桌面应用最直观的门面一眼看懂且浏览器打不开串口这是纯 Web 技术碰不到的活。后端接入插件是一行后端就干三件事——接插件、注册两个命令、把页面读进来交给run()main():Int64{letappTauriApp().window(WindowConfig(串口实时波形 · cj-tauri,1180,760)).plugin(SerialPlugin())// 串口能力一行接入.register(report,ReportCommand())// 页面把每行诊断回投到 stderrapp.register(quit,QuitCommand(app))// 要引用 app 本身所以没串在链上lethtmlloadUi()// 读 ui/index.htmlapp.run(html)return0}ReportCommand那 15 行不是凑数的诊断一律走 stderr仓颉println的 stdout 有缓冲进程被强杀时日志会丢自动化脚本要抓的「页面自评」就是从这儿取证的。QuitCommand则让探针跑完一轮能自己退掉脚本不用靠kill收尾。权限插件只声明「我提供什么」能力清单就一行{commands:[report,quit,system:version],permissions:[serial:default]}serial:default是个命名权限集一次放行open/read/write/close四条命令。这里没用更保守的serial:readonly——示波器要先给设备发SCAN让它开始吐样本写再连续读只读不够用。三条规矩值得记住插件只声明「我提供什么」不自动放行。清单里不写serial:default页面调serial.open就是command not allowed命令放行 ≠ 参数放行。设备路径最终要过白名单这个校验在C 桥 core里不在页面命名集和逐条写命令名是同权的混用也没问题——report/quit是应用自己的命令就直接写明文。协议真实仪器里最常见的那种设备侧只认三个报文方向报文说明应用 → 设备SCAN\n开始连续输出应用 → 设备STOP\n暂停设备 → 应用序号,值\n值域 -1.0…1.0序号用来算丢帧页面不预设设备型号换真实传感器只要改解析函数。读法也不是一次长阻塞读——serial:read单次上限 4096 字节、timeoutMs到期就返回空页面用一个短读循环拿数据、再按行拼functionser(){returnwindow.__CJ_TAURI__.serial;}varbinFrames0;// 只有 hex、没有 text 的帧数二进制设备流如实计数不硬解析functionreadOnce(){if(handle0){return;}ser().read({handle:handle,maxBytes:4096,timeoutMs:120}).then(function(r){if(r.count0){if(r.text!undefinedr.text!null){parseChunk(r.text);}else{binFrames;}}if(handle!0){setTimeout(readOnce,0);}}).catch(function(e){report([wave] serial:read FAIL e.message);setHint(读取出错e.message);disconnect(读取失败);});}这个循环的形状是被 IPC 契约逼出来的命令跑在 worker 线程上、本身不流式所以「一直读」只能在页面侧用短读循环表达而这也顺带让「设备不出数」不会把页面卡住timeoutMs到期返回空循环接着下一轮。另外切在半行上的残行要留着等下一帧补齐——这是串口解析最容易错的地方parseChunk里那个rxText就是干这个的。页面canvas 自绘指标自己算ui/index.html是独立文件不是内联进字符串——这一页有 canvas 绘制、控制面板和统计内联会踩两个坑三引号字符串会先吃掉 JS 的反斜杠转义${...}又和 JS 模板字面量同形。独立文件后两个坑都不存在。界面上该有的都有设备路径、波特率、量程自动/±1/±2/±5/±10、显示点数500–3000、触发自动/正常/单次 电平 上升/下降沿底下是 canvas 波形区再底下一行统计状态、样本数、速率、最新/最小/最大/均值、滚动丢弃、帧率、画布尺寸。这里有个我刻意做的选择指标全部由页面自己算不看对端脸色。速率是按收到样本的时间戳算出来的不是抄对端报的「我按 400 Hz 发」丢帧是靠报文里的序号连不连续判断的上穿次数是直接在样本序列上数的。对端说自己健康没有意义页面自己收到的样子才算数。探针让窗口自己跑一轮自检图形应用的自动化测试一直被当成麻烦事我的办法是把「配置」塞进页面、把「结论」打到 stderrCJ_SERIAL_PROBE_PATH/dev/pts/3CJ_SERIAL_PROBE_MS2500./target/release/bin/main给了设备路径之后后端就把探针配置经document-start 预执行脚本注入页面早于页面脚本执行所以页面一启动就看得见letjswindow.__WAVE_PROBE__ { path: JsonString(probePath).toJsonString(), baud: JsonString(baud).toJsonString(), ms: JsonString(ms).toJsonString(), quit: JsonString(quit).toJsonString() };\nhost.addInitScript(js)字面量特意用 JSON 编码器拼而不是手写字符串——Windows 的设备路径长这样\\.\COM3反斜杠交给编码器转义人不用管。页面侧拿到window.__WAVE_PROBE__就自动跑一轮连接 → 发SCAN→ 收 N 毫秒 → 打形状结论与统计 → 默认自退。环境变量一共四个CJ_SERIAL_PROBE_PATH/_BAUD默认 115200/_MS默认 2500/_QUIT默认 1。失败也会照打结论行——自动化脚本不该因为「没打日志」而空等到超时。实机跑一轮数据长这样下面这些数字是 Linux 实机上量出来的Xvfb 显示 对端按 400 Hz 出样本不是我写在文档里的目标值指标实测它证明什么速率404.2 /s对端按 400 Hz 出样本速率是页面按自己收到的时间戳算的上穿次数8正弦过零 8 次与画出来的波形对得上丢帧0报文序号连续短读循环没丢样本判定verdictok速率在容差内且无丢帧页面自评探针据此退出规模样本 2002 / 画布 1148x566画布尺寸与视口一致布局问题另有截图人眼验只信页面自己报的数还不够所以还有一条进程外证据应用发出的SCAN必须出现在对端落盘的文件里对端起在自己指的文件上。「页面说它写了」证明不了它真写到了线路上这条由 E2E 入口断言。在示例目录里一条命令就能复跑整轮cdexamples/serial-waveformbashrun.sh# pty 对端 应用 探针自检跑完打印结论它是转发到仓内 E2E 入口scripts/e2e/test-linux-serial-waveform.sh的10 条断言全绿汇总跑bash scripts/e2e/run-all.sh是通过 2 / 跳过 0 / 失败 0证据 JSON 落在dist/e2e-evidence/linux-linux-serial-waveform.json。口径、断言和证据都在 E2E 那边维护——示例目录不另立一份标准这点我刻意坚持否则实测数据会慢慢变成两套互相打架的版本。没有真设备也能跑串口示例最烦的就是「没设备怎么试」。这里用 python3 的pty造一对内核虚拟串口不需要 socat本机没装也照样跑对端脚本还负责按 400 Hz 吐正弦样本、并把收到的字节落盘python3 serial-wave-peer.py --write-rx /tmp/wave-rx.bin# 它会打印自己用的 /dev/pts/N./target/release/bin/main# 窗口里填 /dev/pts/N → 连接想省手就设CJ_SERIAL_PROBE_PATH走探针模式连接、发指令、收样本、打统计、退出全自动。几个我自己踩过的坑写这篇文章的时候顺手把踩过的坑也交代一下都是真疼过的三引号字符串会先吃掉 JS 的反斜杠。最初把整个页面内联进字符串里面的\n被仓颉先转义成真换行把 JS 字符串和单行注释拆断——整段script语法错误页面里一行都不执行而 stderr 毫无提示。所以这个示例的页面是独立文件。桥必须在 document-start 注入。Linux 侧用「文档末尾注入」时会晚于发行态单文件里内联的script typemodule页面读window.__CJ_TAURI__拿到undefined。现在两个平台都对齐到「页面脚本执行前」但前端仍然要waitForBridge。自检全绿不等于界面没问题。布局缺陷只能抓图人眼看有个示例的三栏 flex 里标题默认参与收缩中间一栏一窄就把「热门歌曲」折成「热门歌 / 曲」——那一轮 10 条页面自检断言全绿。所以本文这套证据里截图和数据是并列的不是谁替代谁。统计体积要按真实路径去重。我量分发产物时/lib和/usr/lib下同一份libssl.so.3被算了两遍报出来 12.0 MB实际 6.0 MB另外「按名字LoadLibrarydlopen加载」的组件压根不在导入表里统计脚本看不见它们。这些坑都沉淀在仓库的AGENTS.md§4 和 skills 里了新同学改代码前看那一节能省不少时间。边界如实说鸿蒙是架构预留目录、契约 README、导出命名都留好了能力位如实上报 0没实机验证过托盘不做Linux 侧要依赖libayatana-appindicator3这个三方库我暂时不想让框架背所以直接标为暂缓而不是「计划中」这个示波器是入门形态只画滚动窗口内的样本没有缩放、游标、FFT要当分析仪用得自己接着写它还没接单文件打包缺两个资源存根文件所以暂时不产serial-waveform.exe。我把这些写出来不是自曝其短而是**「自检全绿」和「实机跑通」是两件事**——一个框架如果连自己没验过的平台都说支持那它的支持列表就没有信息量了。上手值不值如果你要做的是工具类桌面应用——串口/CAN 调试、Modbus 之类的协议工具、本地数据查看器、播放器、给孩子做的练习软件——而你又不想背 Chromium 的体积和内存cj-tauri 这条路值得看一眼后端静态编译、产物几 MB、原生能力串口、子进程、剪贴板、通知、原生菜单、文件拖放都是现成插件权限由清单统一收口。不适合的场景我也列一下要打包移动端没做、重度依赖完整 Web API 生态系统 WebView 的差异仍在尤其 Linux 的 WebKitGTK 有自己的脾气、要常驻托盘的暂缓。仓库AtomGithttps://atomgit.com/qq8864/cj-tauriMIT 许可当前版本0.8.0npmhttps://www.npmjs.com/package/cj-taurinpx cj-tauri create myapp即可起步上手先读docs/使用文档.md排障看AGENTS.md§4 的踩坑清单示例逐个跑examples/名字/README.md17 个示例里我最推荐按这个顺序往下走hello最小闭环→todo_check命令 事件 能力清单→plugin-serial插件与权限对照→serial-waveform本文这个→modbus-poll真项目形态。走完这五个框架该会的你基本都会了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询