跳到主要内容

2 篇博文 含有标签「Electron」

查看所有标签

Electron 打包后 .env 读不到?配置在 userData 目录而非项目根

· 阅读需 5 分钟

Electron 应用开发时配置读得好好的,用 electron-builder 打包安装后,所有 process.env.XXX 全部变 undefined,依赖配置的模块接连报错。

在为客户构建数据采集工具时遇到此问题,记录根因与解法。

TL;DR​

打包后 .env 不在 process.cwd() 里,而在 app.getPath('userData') 目录。

三步解法:

  1. 主进程启动时检测 userData/.env 是否存在,没有就从包内 .env.example 复制一份(播种)
  2. 把 userData/.env 的完整路径写进 process.env.ENV_PATH
  3. 配置模块用 dotenv.config({ path: process.env.ENV_PATH || '.env' }) 读取

问题现象​

开发阶段(electron .)一切正常;打包安装后(双击图标或开始菜单启动),配置全空:

undefined
undefined
TypeError: Cannot read properties of undefined (reading 'xxx')

process.cwd() 跟着启动方式走:从哪个目录启动就是哪个目录。所以从应用自身目录手动启动时恰好能读到,双击图标启动就读不到——同一个包,启动方式不同行为不同。

根因:process.cwd() 在打包后不可依赖​

dotenv.config() 不传参数时读的是 process.cwd()/.env。process.cwd() 是「进程启动时所在的目录」,不是「应用安装的目录」——两者只在开发阶段恰好相同:

启动方式process.cwd()
开发:项目根目录跑 electron .项目根目录 ✓ .env 在这
Windows 双击 exeexe 所在目录
macOS 双击 .app/(根目录)

打包安装后,.env 想跟用户交互只有两条路,一条都不通:

  • asar 归档只读:electron-builder 把应用资源打進 app.asar,运行时只读。就算把 .env 打进去,用户也改不了,重新打包才能更新配置
  • 安装目录不可写:Program Files、/Applications 这类目录写文件需要提权,普通运行写不进去

Electron 为「属于这个用户的运行时数据」提供了规范目录:app.getPath('userData')。它按平台落在用户目录下(macOS ~/Library/Application Support/<应用名>、Windows %APPDATA%\<应用名>、Linux ~/.config/<应用名>),保证存在、保证可写、卸载重装不丢。.env 应该住这里。

解法:播种 + ENV_PATH 指路​

改动集中在两个文件,共十几行。

第一步:主进程启动时播种(electron/main.js,放在创建窗口、启动 Express 之前):

const fs = require('fs');
const path = require('path');
const { app } = require('electron');

// ── .env path setup ──
const userDataPath = app.getPath('userData');
const envPath = path.join(userDataPath, '.env');
// rootDir = 应用打包根目录,取 app.getAppPath() 或按项目结构定义
const envExamplePath = path.join(rootDir, '.env.example');

if (!fs.existsSync(envPath) && fs.existsSync(envExamplePath)) {
fs.copyFileSync(envExamplePath, envPath);
console.log(`[electron] .env copied to ${envPath}`);
}
process.env.ENV_PATH = envPath;

逻辑:userData 下没有 .env 就从包内的 .env.example 复制一份(.env.example 在 asar 里只读没关系,它只是模板),然后把最终路径挂到 process.env.ENV_PATH。

第二步:配置模块按指路读取(src/config.js):

import dotenv from 'dotenv';
dotenv.config({ path: process.env.ENV_PATH || '.env' });

|| 后面的 '.env' 兜底非 Electron 场景(比如同一份代码直接用 Node 跑),开发时行为不变。

第三步:用户改配置零重装。 配置错了或者要换环境,直接编辑 userData 目录里的 .env,完全退出应用再启动即可——dotenv 只在进程启动时读一次文件,改完不重启不生效。

这套「userData 存运行时状态」的模式还能装下别的:登录态、缓存、日志,都放这里。我们同一个工具里 Cookie 登录态的存放也用了它,那次的完整排查见 Puppeteer 被反爬检测拦截?从 Chrome CDP 到 Electron 的替代方案。

注意事项

process.env.ENV_PATH 必须在配置模块被 import 之前设置好——dotenv.config() 只在调用那一刻读文件。主进程入口文件的第一段就做播种,别放进 app.whenReady() 回调里:如果有模块在更早的 import 链上就 require('dotenv').config(),会读不到路径。

常见问题​

Electron 打包后配置文件放哪里?​

放 app.getPath('userData') 目录——macOS 是 ~/Library/Application Support/<应用名>,Windows 是 %APPDATA%\<应用名>,Linux 是 ~/.config/<应用名>。这是 Electron 保证的用户级可写目录;打包产物里的 asar 归档是只读的,项目根目录在用户机器上也不存在。

Electron 打包成安装包后 .env 为什么读不到?​

dotenv 默认读 process.cwd() 下的 .env,开发时 cwd 是项目根所以正常;打包安装后 cwd 是启动器所在目录(macOS 双击启动时甚至是 /),应用资源又被打进只读的 asar 归档,.env 根本不在读取路径上。解法是主进程启动时把 .env 播种到 userData,再把路径交给 dotenv.config({ path })。

.env.example 需要打包进应用吗?​

需要。它作为首次运行的播种模板随包分发(在 asar 里只读正好),主进程检测到 userData 下没有 .env 时复制一份过去,用户直接编辑 userData 里的 .env 就能改配置,不用重新安装。

CCLEE

独立开发者,24年电商行业实战经验,专注将AI能力落地于真实商业场景。

合作咨询

Puppeteer 被反爬检测拦截?从 Chrome CDP 到 Electron 的替代方案

· 阅读需 7 分钟

在为客户构建数据采集工具时遇到此问题,记录从 Puppeteer 到 Electron 的完整排查过程。

TL;DR​

Puppeteer stealth plugin 无法绕过高级验证码反爬系统。切换到 Chrome CDP 远程调试方案后,又遇到 WSL2 网络隔离和 Chrome 单实例限制两个坑。最终用 Electron BrowserWindow 加载目标网站,用户手动登录后通过 session.cookies.get() 自动提取 Cookie,彻底解决了反爬检测和跨平台问题。

场景一:Puppeteer 被 Anti-Bot 拦截​

问题现象​

使用 puppeteer-extra + puppeteer-extra-plugin-stealth 自动登录目标网站,浏览器启动后立即触发验证码拦截。即使通过了验证码,后续页面也会再次检测到自动化环境并强制退出。

import puppeteer from 'puppeteer-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';

puppeteer.use(StealthPlugin());

const browser = await puppeteer.launch({
headless: false,
args: [
'--disable-blink-features=AutomationControlled',
'--no-sandbox',
'--disable-infobars',
],
});

const page = await browser.newPage();
await page.goto('https://target-site.com/login');
// 验证码系统检测到自动化环境,页面被拦截

根因​

高级反爬验证码系统不只检查 navigator.webdriver 等基础指纹。它通过多个维度判断自动化环境:Chromium 编译特征、Canvas/WebGL 渲染差异、鼠标轨迹模式、甚至 DevTools Protocol 调用栈。stealth plugin 能修复已知的指纹泄露点,但无法消除 Puppeteer Chromium 与正常 Chrome 的底层差异。

经过 8 轮排查(移除超时、监听断开事件、排查环境差异、stealth plugin 补全等),确认无论怎么配置都无法绕过。

解法​

放弃 Puppeteer 自动登录,改用 Chrome DevTools Protocol (CDP) 连接用户真实浏览器提取 Cookie。

场景二:WSL2 连不上 Windows Chrome CDP​

问题现象​

在 WSL2 中运行 Node.js 脚本,通过 chrome-remote-interface 连接 Windows Chrome 的调试端口,连接超时:

import CDP from 'chrome-remote-interface';

const client = await CDP({
host: 'localhost',
port: 9222,
});
// Error: connect ECONNREFUSED 127.0.0.1:9222

Windows 端启动 Chrome 的命令:

chrome.exe --remote-debugging-port=9222

在 Windows PowerShell 中 curl localhost:9222/json 正常返回,但 WSL2 内无法连接。

根因​

Chrome --remote-debugging-port=9222 默认绑定 127.0.0.1,即 Windows 的本地回环地址。WSL2 和 Windows 有各自独立的网络栈——WSL2 内的 localhost 指向 Linux 的回环地址,不是 Windows 的。所以从 WSL2 访问 localhost:9222 实际访问的是 Linux 的 9222 端口,而非 Windows Chrome。

解法​

启动 Chrome 时加 --remote-debugging-address=0.0.0.0,让 CDP 监听所有网卡:

chrome.exe --remote-debugging-port=9222 --remote-debugging-address=0.0.0.0

或者用 Windows netsh 配置端口转发:

netsh interface portproxy add v4tov4 listenport=9222 listenaddress=0.0.0.0 connectport=9222 connectaddress=127.0.0.1

注意事项

--remote-debugging-address=0.0.0.0 会将 CDP 端口暴露给局域网,存在安全风险。仅在内网开发环境使用,生产环境务必配合防火墙规则限制访问来源。

场景三:Chrome 忽略 --remote-debugging-port​

问题现象​

Chrome 已经在运行,带 --remote-debugging-port=9222 参数重新启动,参数被静默忽略。Chrome 只是在已有窗口中打开新标签页,CDP 端口没有开启:

# Chrome 已在运行
chrome.exe --remote-debugging-port=9222
# 没有报错,但 9222 端口并未监听

根因​

Chrome 设计为单实例应用。检测到已有 Chrome 进程时,新启动的 Chrome 会将启动参数中的 URL 转发给已有进程,然后自行退出。--remote-debugging-port 等参数只在进程首次创建时生效,已有进程不会动态加载。

解法​

先关闭所有 Chrome 进程,再带参数重启:

# Windows
taskkill /F /IM chrome.exe
chrome.exe --remote-debugging-port=9222

# macOS
pkill -f "Google Chrome"
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222

也可以用 --user-data-dir 指定独立配置目录,避免与日常使用的 Chrome 冲突:

chrome.exe --remote-debugging-port=9222 --user-data-dir="C:\chrome-debug-profile"

三个场景的踩坑说明了一个事实:依赖外部 Chrome 进程做 Cookie 提取,在 WSL2 + 反爬检测 + 进程管理的组合下太脆弱。最终方案是用 Electron 的 BrowserWindow 替代外部 Chrome。

为什么 Electron 有效​

  1. 不触发反爬检测:Electron 内置的 Chromium 与 Chrome 共享渲染引擎,反爬系统不将其标记为自动化环境
  2. 无外部 Chrome 依赖:不需要管理 Chrome 进程、CDP 端口、profile 目录
  3. 无 WSL2 网络问题:Electron 直接运行在目标 OS 上,不存在跨系统网络隔离
  4. 无单实例冲突:Electron 创建独立的 BrowserWindow,不与用户日常 Chrome 冲突

完整实现​

打开登录窗口并轮询 Cookie:

import { BrowserWindow } from 'electron';

let cookieWindow = null;

function openLoginWindow() {
cookieWindow = new BrowserWindow({
width: 1000,
height: 700,
title: '登录目标平台',
webPreferences: {
// 关键:使用独立 session,不影响主窗口
partition: 'cookie-login',
contextIsolation: true,
nodeIntegration: false,
},
});

cookieWindow.loadURL('https://target-site.com/login');

// 轮询检测目标 Cookie
const interval = setInterval(async () => {
const cookies = await cookieWindow.webContents.session.cookies.get({
domain: '.target-site.com',
});

const sessionCookie = cookies.find((c) => c.name === 'session_token');
if (sessionCookie) {
clearInterval(interval);

// 拼接完整 Cookie 字符串
const cookieStr = cookies
.map((c) => c.name + '=' + c.value)
.join('; ');

// 写入环境变量
process.env.SESSION_COOKIE = cookieStr;
updateEnvFile('SESSION_COOKIE', cookieStr);

cookieWindow.close();
}
}, 2000);

cookieWindow.on('closed', () => {
clearInterval(interval);
cookieWindow = null;
});
}

preload 脚本暴露 IPC 接口:

import { contextBridge, ipcRenderer } from 'electron';

contextBridge.exposeInMainWorld('electronAPI', {
extractCookie: () => ipcRenderer.invoke('extract-cookie'),
onCookieExtracted: (callback) =>
ipcRenderer.on('cookie-extracted', (_, data) => callback(data)),
});

渲染进程调用:

// 检测是否在 Electron 环境
if (window.electronAPI) {
document.getElementById('extractBtn').addEventListener('click', () => {
window.electronAPI.extractCookie();
});

window.electronAPI.onCookieExtracted((data) => {
console.log('Cookie 已提取:', data);
});
}

注意事项

  • partition: 'cookie-login' 创建隔离 session,登录窗口的 Cookie 不会污染主窗口。如果需要共享登录状态,去掉 partition 参数或使用相同 partition 名
  • 轮询间隔 2 秒是平衡体验和性能的经验值,不要用 while + await 替代 setInterval,那会阻塞渲染进程
  • session.cookies.get() 只能获取当前 session 的 Cookie,无法跨 partition 读取

常见问题​

Puppeteer stealth plugin 仍然被反爬检测到怎么办?​

stealth plugin 只能绕过基础指纹检测(如 navigator.webdriver),高级反爬系统通过浏览器行为和底层 API 特征识别自动化环境。检测维度包括 Chromium 编译特征、Canvas 渲染差异、鼠标轨迹模式等,stealth plugin 无法完全覆盖。改用 Electron BrowserWindow 加载目标网站,用户手动登录后通过 session.cookies.get() 提取 Cookie 即可。

Chrome --remote-debugging-port 为什么不生效?​

Chrome 采用单进程架构,已有 Chrome 进程运行时 --remote-debugging-port 参数会被静默忽略。新启动的 Chrome 只会在现有实例中打开新标签页,调试端口不会开启。需要先用 taskkill /F /IM chrome.exe(Windows)或 pkill -f "Google Chrome"(macOS)关闭所有 Chrome 进程,再带参数重新启动。也可以用 --user-data-dir 指定独立配置目录避免冲突。

WSL2 怎么连接 Windows 上 Chrome 的调试端口?​

Chrome 的 --remote-debugging-port 默认绑定 Windows 的 127.0.0.1,而 WSL2 有独立的网络栈,localhost 指向 Linux 回环地址而非 Windows。两种解法:一是启动 Chrome 时加 --remote-debugging-address=0.0.0.0 让 CDP 监听所有网卡;二是在 Windows 上用 netsh interface portproxy 配置端口转发,将 WSL2 的请求转发到 Windows 的 CDP 端口。


需要数据采集或自动化工具开发?

联系我们