如果你用 Electron 跑过命令行工具,大概率迟早会撞上这个报错:Cannot find module '...\cli'。更诡异的是------同样的命令,在你本地终端能跑,丢进自动化脚本里就挂。我和这个错纠缠了好几轮,最后发现锅在一个叫 ELECTRON_RUN_AS_NODE 的环境变量上。
一、这个变量到底是干嘛的
ELECTRON_RUN_AS_NODE 是 Electron 预留的一个开关。当它存在 (值无所谓,哪怕是空字符串)时,Electron 启动时不会走自己的运行时,而是退化成纯 node:
- 入口不再是
electron的 main 流程,而是把第一个参数当成 node 脚本来执行; - 顶层的
require、process行为都按 node 来,但模块解析路径却还是 Electron 的打包结构; - 于是它去找
...\cli这个"node 模块",自然找不到------因为那本来是 Electron 自己加载的入口,不是 npm 包。
一句话:这个变量打开了,Electron 就不再是 Electron,而是一个认不全自家路径的 node。
二、为什么它会"凭空出现"
最坑的地方在于,它不是你亲手 export 的。很多宿主环境(CI、某些沙箱、部分终端封装)会预先注入这个变量,目的是让 Electron 应用里的子进程能当 node 用。
问题就出在"继承"上:
bash
# 宿主环境已经 export 了 ELECTRON_RUN_AS_NODE=1
# 你在这里启动一个 Electron 工具
./mmcli.cmd publish-article -p juejin ...
子进程通过 fork / spawn 启动时,默认继承父进程的全部环境变量 。父进程里有 ELECTRON_RUN_AS_NODE,子进程也就带着它------于是 Electron 一启动就退化,去 require 一个不存在的模块,报错退出。
你本地终端能跑,是因为你那台机器没设这个变量;脚本跑挂,是因为它跑在带这个变量的宿主里。同样的命令,不同的环境继承链,结果天差地别。
三、复现的最小样例
不需要整个发布工具,几行就能复现退化现象:
bash
# 设置一个值,然后启动任意 Electron 应用
export ELECTRON_RUN_AS_NODE=1
./your-electron-app
# 现象:应用不加载自己的 main,而是尝试把第一个参数当脚本跑
# 若参数是个内部入口(如 cli),直接报 Cannot find module
把 export 换成 unset 再跑一次,应用立刻恢复正常。对照实验一做,嫌疑变量就锁定了。
四、治本:在调用前清掉它
临时排查用 unset,但真正落地要在封装层解决,而不是每次手敲:
bash
# 发布前先清掉,再调 CLI(关键一步,漏了就退化)
unset ELECTRON_RUN_AS_NODE
# 输出重定向到文件,父子进程解耦,避免前台被沙箱杀掉
MMCLI_OUT="$LOCALAPPDATA/Temp/mmcli-juejin.txt" \
./mmcli.cmd publish-article \
-p juejin --phone "不可能片场" \
-t "文章标题" --category 开发工具 \
--file "article.md"
我们最后是做了一个薄封装脚本(mmcli.cmd 的调用壳),在 spawn 之前显式删除这个变量,把清环境变成默认动作,而不是可选项。这样无论宿主有没有注入,子进程拿到的都是干净的 env。
五、几个容易混淆的点
- 它不是你代码的 bug。 很多人先去翻自己的
require路径,其实路径没错,是运行时被换掉了。先看环境变量,再看代码。 unset要作用在子进程可见的作用域。 在 bash 里unset之后同一条命令链里 spawn 的进程就干净了;但如果你是用别的进程管理器拉起,得确认它没在更外层重新注入。- 别全局永久删。 有些宿主依赖这个变量跑别的 Electron 子进程,盲目从系统环境删掉可能误伤。只在"要启动这个特定 CLI"的作用域里清,最稳。
六、排查同类问题的通用思路
这次踩坑给我一个可复用的排查套路:
- 同样的命令,本地能跑、脚本里挂 → 先怀疑环境继承差异,不是代码差异;
- 把宿主环境的
env打出来,和本地env做 diff,重点看带ELECTRON/NODE字样的变量; - 用"设 / 不设"对照实验,单个变量排除,别一次改一堆;
- 确认根因后,把"清理变量"塞进封装层,让正确配置成为默认。
环境变量这类"看不见的继承",往往是本地 / 线上不一致的根源。下次再遇到"我机器上明明能跑",第一反应应该是:不是代码变了,是环境传进来了什么。