很多人写 Electron 时对 require 的认知还停留在"装了包就能 require"。但 Electron 是一个多进程模型,一旦涉及主进程、渲染进程、utilityProcess、再叠加打包后的目录变化,require 能不能成功就变成了两个完全不同维度的问题:
- 路径维度:这个模块的文件到底在不在我能解析到的地方?(解析问题,可修)
- 身份维度:这个能力到底属不属于"我这个进程"?(进程边界问题,不可修,只能换进程)
这篇文档把 Electron 里会遇到的五类"库"逐一拆开讲清楚,并结合本 demo 的真实代码,讲透:把这些文件放到安装包之外(比如用户数据目录)后,怎么处理才能让它们跑起来 ,以及 webpack 打包时必须注意的点。
一、先建立正确的心智模型:三种进程不是"同一个 Node"
Electron 不是"一个 Node 进程里跑了个浏览器"。它是 Chromium 的多进程架构,每个进程内嵌了 Node 运行时(V8 + libuv),但进程身份不同,能拿到的 native 绑定就不同。
| 进程 | 本质 | Node 运行时 | Electron 主进程 API(app/net/session...) |
|---|---|---|---|
| 主进程 main | Chromium 的 browser process(根进程) | ✅ 完整 | ✅ 完整 |
| 渲染进程 renderer | Chromium 渲染进程 | ⚠️ 默认关闭,可开启 | ⚠️ 受限(主要是 ipcRenderer 等) |
| utilityProcess | Chromium Services 子进程 | ✅ 完整 | ❌ 几乎只有 process.parentPort |
记住一句话贯穿全文:
能不能用 Node 生态 → 看模块在不在能被解析到的
node_modules(路径问题)。
能不能用 Electron 能力 → 看你是不是那个持有 Chromium 子系统的进程(身份问题)。
二、五类库逐一拆解
1. Node.js 核心模块(内置模块)
例子:path fs http https net module crypto os url child_process...
特征 :它们被编译进了 Electron 二进制本身 ,require('path') 根本不碰文件系统,直接从二进制内部的模块表返回。
结论:
- 在任何进程(main / renderer 开了 nodeIntegration / utilityProcess)里都能用。
- 不管脚本文件被搬到哪(安装目录、asar、用户数据目录、临时目录),永远解析得到。
- 它跟
node_modules没有任何关系,也跟NODE_PATH无关。
本 demo 的 utility-server.js 里 require('http')、require('module')、require('path') 之所以"搬到用户目录也能用",就是因为它们是核心模块。
⚠️ 易混点:
net这个名字同时存在两套东西------Node 自带的require('net')(裸 TCP/IPC socket,核心模块,到处能用)和 Electron 的require('electron').net(基于 Chromium 网络栈的 HTTP 客户端,主进程专属)。名字一样,性质天差地别,见下一节。
2. Electron 扩展库(主进程专属 API)
例子:app BrowserWindow session net(electron 的) dialog protocol ipcMain Menu Tray globalShortcut...
特征 :这些不是普通可被搬运的 JS 库 ,而是指向"正在运行的 browser process"内部 native 对象的绑定(handle):
app绑定的是这个进程的生命周期和 Chromium 消息循环;net(electron)绑定的是 browser process 里那个唯一的网络服务实例,要走系统代理、复用 session 的 cookie;session/BrowserWindow绑定的是窗口与会话子系统。
这些对象只在主进程这一个进程里存在。
结论:
- 只能在主进程里用。
- 即使在 utilityProcess 里
require('electron'),也拿不到这些 handle ------ 因为它本身就不是那个持有这些对象的进程。这不是"包没装",搬库也搬不过来。 - 在 utilityProcess 里需要这些能力时:让主进程 调用,结果通过
process.parentPort回传。本 demo 的 deep-link 转发就是这个模式------所有 Electron 侧的事(fork、建管道、协议注册)都在主进程,utility 只管业务。
js
// ❌ 在 utilityProcess 里这样做,net 是 undefined
const { net } = require('electron')
// ✅ utilityProcess 里只能用 Node 自带的网络能力
const http = require('http') // 或 https / net(node)
3. 渲染进程可开启的库(nodeIntegration / contextIsolation)
渲染进程默认是一个纯浏览器环境,没有 require、没有 Node 。能否在 renderer 里 require 取决于 BrowserWindow 的 webPreferences:
| 配置 | 效果 | 安全性 |
|---|---|---|
nodeIntegration: false(默认) |
renderer 里没有 require/process/Buffer |
✅ 安全,推荐 |
nodeIntegration: true |
renderer 里可直接 require('fs') 等 |
❌ 危险,加载远程内容时等于把 Node 暴露给网页 |
contextIsolation: true(默认) |
preload 与页面 JS 隔离上下文 | ✅ 安全,推荐 |
推荐做法(也是 Electron 官方方向) :renderer 不开 nodeIntegration,需要的 Node/Electron 能力通过 preload + contextBridge 白名单暴露,或走 ipcRenderer → ipcMain 让主进程代劳。
js
// preload.js ------ 只把需要的能力按白名单暴露给渲染层
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('api', {
handleProtocol: url => ipcRenderer.invoke('protocol:handle', url)
})
关键认知:renderer 的"业务依赖"(Vue、axios 等)不是靠运行时
require('node_modules')来的,而是 webpack 在构建时直接打进了 renderer bundle 。所以 renderer 反而最"自包含",它不依赖NODE_PATH,依赖的是资源路径对不对(见第四节)。
4. 第三方库(npm 依赖,住在 node_modules)
例子:本 demo 的 express axios vue。
特征 :它们是真实存在于 node_modules 里的文件,require('express') 走 Node 的文件系统解析算法:从脚本所在目录开始,逐级向上找 node_modules/express。
结论:
- 这是纯路径问题 。脚本旁边能向上找到
node_modules/express就成功,找不到就报Cannot find module 'express'。 - 打包后这个目录关系经常被破坏(asar、用户目录热更新等),需要手动修复解析路径,见第三、四节。
但这里有个分叉,取决于 webpack 怎么处理它(见第五节):
- 如果 webpack 把第三方库打进 bundle (renderer 通常如此)→ 运行时根本不
require,无所谓node_modules。 - 如果 webpack 把它列为 external (本 demo 主进程/utility 如此)→ 产物里保留了运行时
require('express'),那就必须保证运行时能解析到node_modules。
5. utilityProcess 进程里的库
utilityProcess 是被主进程 utilityProcess.fork(scriptPath, args, options) 拉起的轻量 Node 子进程。它的依赖能力等于:
Node 核心模块(永远有) + 能被解析到的 node_modules 里的第三方库(要修路径) + 几乎为零的 Electron API(只有 parentPort)
本 demo 的 utility-server.js 是一个独立打包入口,它要用 express,于是问题就来了:当这个脚本被放到用户数据目录后,它旁边没有 node_modules,怎么 require('express')?
答案就是下一节的核心。
三、把文件放到"安装包之外"后,怎么让它跑起来
本 demo 已经实现了 utility-server 脚本的"用户目录优先 + 热更新"。核心代码在 src/main/index.js。拆开看它解决了两件事。
3.1 文件本身:脚本放哪、优先用哪个
utilityProcess.fork() 要的是一个真实可访问的 JS 文件路径。demo 的策略是"用户目录有就用用户目录的,否则用包内的":
js
function resolveUserInstallUtilityServerPath() {
// %APPDATA%/electron-demo/utility-process/utility-server.js
return path.join(app.getPath('userData'), 'utility-process', 'utility-server.js')
}
function resolveUtilityServerStartPath(bundledServicePath) {
const userServicePath = resolveUserInstallUtilityServerPath()
if (fs.existsSync(userServicePath)) {
return userServicePath // 用户目录热更新脚本优先
}
return bundledServicePath // 回落到安装包内 dist/electron/server/utility-server.js
}
包内脚本也要多路径兜底,因为开发态、--dir、asar 后的 __dirname 不一样:
js
function resolveBundledUtilityServerPath() {
const candidates = [
path.resolve(__dirname, UTILITY_SERVER_BUNDLE),
path.resolve(__dirname, '..', UTILITY_SERVER_BUNDLE),
path.resolve(app.getAppPath(), 'dist', 'electron', UTILITY_SERVER_BUNDLE),
path.resolve(app.getAppPath(), UTILITY_SERVER_BUNDLE)
]
const servicePath = candidates.find(c => fs.existsSync(c))
if (!servicePath) throw new Error(`utility server bundle not found...`)
return servicePath
}
3.2 依赖解析:用户目录脚本怎么找到包内的 node_modules
这是整个方案最关键、最容易踩坑的地方。脚本搬到用户目录后,它旁边没有 node_modules,但当前安装包里有(通常在 app.asar/node_modules)。主进程要把这个路径注入给子进程。
第一步,主进程多路径找到当前安装包的 node_modules:
js
function resolveUtilityServerNodePath() {
const candidates = [
path.resolve(app.getAppPath(), 'node_modules'), // 开发态 / app 解包
path.resolve(process.resourcesPath || '', 'app.asar', 'node_modules'),// 打包后 asar
path.resolve(process.resourcesPath || '', 'app', 'node_modules'), // asar 解包
path.resolve(__dirname, '..', '..', 'node_modules'),
path.resolve(__dirname, '..', 'node_modules')
]
return candidates.find(c => c && fs.existsSync(c))
}
第二步,fork 时通过 env 自定义变量 把这个路径传给子进程(注意 cwd 也指到脚本目录):
js
const nodePath = resolveUtilityServerNodePath()
if (!nodePath) {
throw new Error(`utility server node_modules path not found...`)
}
protocolServiceProcess = utilityProcess.fork(servicePath, [PIPE_NAME], {
cwd: path.dirname(servicePath),
env: {
...process.env,
UTILITY_SERVER_NODE_PATH: nodePath // ⭐ 业务自定义变量,不是直接塞 NODE_PATH
},
stdio: 'pipe'
})
第三步,子进程脚本在 require 第三方库之前 ,用 Node 核心模块 module 把这个路径注册进解析算法:
js
// dist/electron/server/utility-server.js 开头
const Module = require('module') // 核心模块,一定找得到
const path = require('path')
const utilityServerNodePath = process.env.UTILITY_SERVER_NODE_PATH
if (utilityServerNodePath) {
process.env.NODE_PATH = utilityServerNodePath
Module._initPaths() // ⭐ 重新初始化解析路径
}
const express = require('express') // 必须在 _initPaths 之后!
const http = require('http')
为什么这么绕?三个细节:
- 顺序不能反 :
require('express')必须在Module._initPaths()之后。webpack 产物里这段是手写在最前面、独立于 webpack 模块系统的引导代码,确保解析路径先就位。 - 不要只在 env 里直接传
NODE_PATH。实测发现,对"用户目录脚本"这个场景,fork 时直接给 env 设NODE_PATH并不可靠,子进程启动后require('express')仍可能找不到app.asar/node_modules。更稳的是传一个业务自定义变量 (UTILITY_SERVER_NODE_PATH),进程内自己process.env.NODE_PATH = ... + Module._initPaths(),时机完全可控。 - express 等依赖在 asar 里能被 require 。Electron 给 Node 的
fs/模块解析打了补丁,app.asar会被当成虚拟目录,所以app.asar/node_modules/express是能正常 require 的,不需要把依赖解包。
3.3 验证:用 /health 自证从哪启动、依赖路径是什么
排查这类问题,光看"能不能跑"不够,要让服务自己报告它的处境。demo 的 /health 返回了关键字段:
js
app.get('/health', (req, res) => {
res.json({
startedFrom: __filename, // 证明从用户目录还是包内启动
cwd: process.cwd(), // 证明 fork 的工作目录
utilityServerNodePath: process.env.UTILITY_SERVER_NODE_PATH || '' // 证明注入的依赖路径
})
})
- 用户目录热更新生效时,
startedFrom应指向%APPDATA%/electron-demo/utility-process/utility-server.js。 utilityServerNodePath应指向当前安装包的app.asar/node_modules。
3.4 渲染进程放到安装包之外,处理方式完全不同
注意:renderer 的"用户目录优先/热更新"和 utility-server 不是一回事。
- utility-server 是被
fork当 Node 脚本跑的 → 要解决"依赖在哪"(NODE_PATH)。 - renderer 是被主进程
win.loadFile(...)加载进 BrowserWindow 的页面 → 它的业务依赖已被 webpack 打进 bundle,不需要node_modules;要解决的是"资源相对路径/协议对不对"。
所以 renderer 的热更新形态是:把构建好的 renderer 目录放用户目录 → 主进程检测到就 loadFile(用户目录/index.html),否则 loadFile(包内/index.html)。两者结构同构(用户目录优先),但触发机制不同(fork vs loadFile),要解决的根因也不同(依赖解析 vs 资源路径)。并且无论哪种,utilityProcess 只能由主进程启动,renderer 不能自己 fork,主进程都绕不开。
四、一张表收口:遇到 require 报错该往哪查
| 报错/现象 | 属于哪类 | 根因 | 怎么修 |
|---|---|---|---|
Cannot find module 'path'/'http' |
核心模块 | 几乎不可能,除非进程类型不对 | 检查是不是在浏览器纯环境里跑 |
utilityProcess 里 electron.net 是 undefined |
Electron 扩展库 | 进程身份问题,utility 不是主进程 | 改用 Node 的 http/net,或主进程代劳 + parentPort |
renderer 里 require is not defined |
渲染进程库 | nodeIntegration 没开(且不该开) | 用 preload + contextBridge / ipc |
用户目录脚本 Cannot find module 'express' |
第三方库 | 旁边没有 node_modules,解析失败 | 注入 NODE_PATH + Module._initPaths() |
打包后 utility server bundle not found |
文件路径 | asar 后 __dirname 变了 |
多 candidate 路径兜底 fs.existsSync |
五、webpack 打包时的注意事项
本 demo 主进程和 utility 用 .electron-vue/configs/webpack.main.config.js 打包,几个点必须注意。
5.1 utility 脚本要作为独立入口打包
utilityProcess.fork() 需要一个真实文件。开发态直接指向 src/main/utility-server.js 可能能跑,但生产包目录结构变化后极易找不到。所以把它配成独立 entry,输出到固定子目录:
js
entry: {
main: path.join(__dirname, '../../src/main/index.js'),
'utility-server': path.join(__dirname, '../../src/main/utility-server.js')
},
output: {
filename: pathData => {
if (pathData.chunk?.name === utilityServerBundle.entryName) {
return utilityServerBundle.relativeOutputPath // server/utility-server.js
}
return '[name].js'
},
library: { type: 'commonjs2' }, // 产物是 CommonJS,能被 fork 当 Node 脚本跑
path: electronDistDir
}
入口路径还通过 DefinePlugin 注入成常量 __UTILITY_SERVER_BUNDLE__,主进程用它来定位脚本,避免硬编码字符串。
5.2 第三方依赖列为 externals,不要打进 bundle
这是主进程/utility 打包与 renderer 最大的区别:
js
const { dependencies } = require('../../package.json')
externals: [
...Object.keys(dependencies || {}) // express / axios / vue 全部 external
],
含义:webpack 不把 express 打进 bundle ,产物里保留运行时 require('express')(你能在产物里看到 module.exports = require("express") 这样的 external 包装)。
- 好处:bundle 小、原生模块(.node)不会被 webpack 破坏、依赖走 Node 真实解析。
- 代价 :运行时必须能解析到
node_modules------ 这正是第三节 NODE_PATH 方案存在的原因。 - 推论:凡是列进
dependencies的,运行时都得在node_modules里真实存在;凡是只在构建期用的,放devDependencies,不会进 externals。本 demo 里express/axios/vue/vue-electron在 dependencies,所以会随安装包进app.asar/node_modules。
5.3 target 要选对
js
target: 'electron-main' // 主进程/utility 用这个
- 主进程 / utility:
electron-main,webpack 会把 Node 核心模块和 electron 当外部依赖,不去 polyfill。 - 渲染进程:
electron-renderer或web,并把 Vue 等依赖打进 bundle(renderer 不用 externals 那套),所以 renderer 产物自包含,依赖的是资源路径而非 node_modules。
5.4 原生模块 / .node 文件
js
module: { rules: [{ test: /\.node$/, use: 'node-loader' }] }
涉及原生 .node 的依赖更要走 externals + node-loader,绝不能让 webpack 试图把二进制打进 bundle。
5.5 electron-builder 的 files 要包含产物
jsonc
"build": {
"files": ["dist/electron/**/*"] // 确保 server/utility-server.js 被打进安装包
}
否则 fork 时 resolveBundledUtilityServerPath() 在包内根本找不到脚本。
六、渲染进程"放哪都能用"的真正条件
经常有人得出这样一个结论:
"渲染进程开启了 node 集成、上下文不隔离、Vue 又打进了 renderer,那这个 renderer 放哪都能用。"
这个因果是错配的。 让 renderer 自包含/可搬运的,只有"打进 bundle"这一件事,跟 nodeIntegration、contextIsolation 没有任何关系。
| 条件 | 跟"放哪都能用"有关吗 |
|---|---|
| 开启 node 集成 | ❌ 无关,甚至有反作用(见下) |
| 上下文不隔离 | ❌ 无关,纯安全设置 |
| Vue 等打进 renderer bundle | ✅ 这才是自包含的唯一原因 |
Vue 能用,是因为 webpack 在构建时 把它的代码直接塞进了 renderer 的 JS bundle,运行时根本不发生 require('vue')、不碰 node_modules。这件事在 nodeIntegration 关闭时一样成立。
6.1 nodeIntegration 开启反而是新的"不可搬运"风险
很反直觉但很关键:开 node 集成的意义是"让页面能 require",而一旦你真的去 require 一个没被打进 bundle 的第三方 node 库,你就立刻把第四类库的"路径解析问题"引回了 renderer。
js
// renderer 里开了 nodeIntegration 后:
const fs = require('fs') // ✅ 核心模块,在二进制里,搬哪都行
const express = require('express') // ⚠️ 运行时解析!旁边没有 node_modules 就炸
- 只
requireNode 核心模块 → 还能搬。 - 运行时
require外部 npm 库(没打进 bundle 的)→ 又回到"必须能解析到 node_modules",反而变得不可搬运。
所以真正可搬运的 renderer,是"什么都打进 bundle、运行时不做任何外部 require "的 renderer------这种情况下你压根不需要开 node 集成。开了又去用外部库,是在给自己制造搬运障碍;何况 contextIsolation:false + nodeIntegration:true 是公认的安全反模式,加载任何不完全可信的内容时等于把 Node 暴露给页面。
6.2 即便 bundle 自包含,"放哪都行"还有两个边界
- renderer 不能自己跑。 它不是可执行程序,是一张要被 BrowserWindow 加载的页面。无论放哪,都必须由主进程
win.loadFile(那个路径)指过去。"放在用户目录"只改了文件位置,加载动作仍由主进程发起------主进程绕不开。 - 资源相对路径 / publicPath 要对。 通过
file://加载时,HTML 引用的 JS/CSS/图片/字体路径必须仍能解析到。如果 webpack 用了绝对publicPath或假设了固定 base,换目录就会白屏/404。这才是 renderer 搬到包外真正要处理的问题------资源路径,不是依赖解析。
准确表述:renderer 的可搬运性 = "依赖打没打进 bundle"(构建期决定)+ "资源路径对不对"(加载期决定),跟"进程身份/node 集成"是两套维度。
七、都打进 bundle,会不会包体大、影响性能
会让安装包变大,但"包体大"和"性能差"在 Electron 里不是同一件事 ,也不像网页那样强相关。关键差异:Electron 的 renderer 从本地磁盘走 file:// 加载,没有网络下载这一环。
| 成本 | 受 bundle 大小影响吗 | 在 Electron 里严重吗 |
|---|---|---|
| 网络下载耗时 | 网页里很关键 | ❌ 几乎不存在,本地磁盘读取 |
| 安装包 / 磁盘体积 | ✅ 直接变大 | ⚠️ 影响分发体积,不影响运行性能 |
| JS 解析+编译+执行耗时 | ✅ 真正影响启动 | ✅ 这才是要管的 |
7.1 不打进 bundle ≠ 更快
容易误以为"把 Vue 留在 node_modules、不打进 bundle"更轻,其实不是:
- 那些代码该被解析执行一行都不会少,只是文件换了位置。
node_modules是几百上千个小文件 ,运行时一堆fs查找 + asar 解析,往往比一个打好的 bundle 更慢。- 打进 bundle 还能吃到 tree-shaking + 压缩混淆,external 的 node_modules 这两样都享受不到。
所以 renderer 这边"打进 bundle"通常是更优 选择。external 那套是 utility-server 为了可解析/可热更才用的,不是为了性能。
7.2 体积/启动真的大了,正确解法是"延迟执行"而非"踢出 bundle"
- 代码分割 + 路由懒加载 (最主要的杠杆):动态
import()把非首屏页面/组件拆成独立 chunk,首屏只留必需,V8 启动要解析的 JS 就小了。 - production 模式构建:minify + scope hoisting + 去掉 dev 警告,Vue 2 运行时本身就只有几十 KB(gzip)。
- vendor 拆分:第三方库单独成 chunk,便于磁盘缓存 / V8 code cache 复用。
- V8 code cache / 快照(进阶):缓存"解析编译"结果,二次启动更快。
落到本 demo:renderer 依赖就是
vue@2+axios,体量很小,打进去完全无所谓。这类担忧通常在依赖树很重(大型 UI 库、地图、富文本、ECharts 全量)时才需要认真对待。
八、本 demo 的最优选择:第三方包直接打进 utility-server,省掉 NODE_PATH
第三节为了通用性讲了 UTILITY_SERVER_NODE_PATH + Module._initPaths() 的完整方案。但针对当前这个 demo ,有更简单的路子:utility-server 运行时唯一的第三方 require 就是 express(http/module/path 都是核心模块)。只要把 express 打进 utility-server 的 bundle ,运行时不再发生 require('express'),这个脚本就是单文件自包含 ------丢用户目录也好、丢哪都好,不依赖任何 node_modules,于是整套 NODE_PATH 机制可以全删掉。
对热更新反而更友好:用户目录里的脚本自带 express,不用再"复用当前安装包的 node_modules",少一个出错点。
8.1 具体改三处
- webpack 让 express 不再 external (
webpack.main.config.js):
js
externals: [
...Object.keys(dependencies || {}).filter(d => d !== 'express')
],
main 进程没 import express,所以只有 utility-server 入口会把它打进去,main 不受影响。
- 删掉
src/main/utility-server.js顶部的引导块(改源文件,不是 dist):
js
// 这一整段可以删了
const Module = require('module')
if (utilityServerNodePath) {
process.env.NODE_PATH = utilityServerNodePath
Module._initPaths()
}
直接 const express = require('express') 即可,webpack 会把它替换成内联代码。
- 删掉主进程的路径注入 (
src/main/index.js):resolveUtilityServerNodePath()、那个if (!nodePath) throw、fork 时env里的UTILITY_SERVER_NODE_PATH都不需要了,/health里的utilityServerNodePath字段也可一并去掉。
8.2 唯一要记住的边界
这条路只对"纯 JS 依赖"成立 。express、axios 都是纯 JS,能干净打包。但如果 utility-server 引入了带原生 .node 二进制的依赖 (如 better-sqlite3、serialport),webpack 没法把二进制打进 bundle,那就得退回 external + node_modules + NODE_PATH 那套。
纯 JS 依赖 → 打进 bundle,最省心;含原生模块 → 必须 external + 传 node 路径。
当前 demo 全是纯 JS,直接打进去即可。
九、总结
把 Electron 的"库引入"想成两条独立的判断线,绝大多数 require 问题都能秒定位:
- 这是 Electron 能力还是 Node/第三方能力?
- Electron 主进程 API(app/net/session...)→ 身份问题,只有主进程有,utility/renderer 拿不到,改路径无效,只能换进程或让主进程代劳。
- 如果是 Node/第三方能力,文件解析得到吗?
- 核心模块(path/http/net...)→ 在二进制里,永远 OK。
- 第三方(express...)→ 看 webpack 是打进 bundle 还是 external;若 external,运行时必须能解析到
node_modules,搬到安装包外时用UTILITY_SERVER_NODE_PATH+Module._initPaths()修复。
- renderer 特殊 :它默认没有 Node,业务依赖靠 webpack 打进 bundle,搬到包外要解决的是资源路径而非依赖解析,且必须由主进程
loadFile加载。renderer 的可搬运性来自"打进 bundle",跟 nodeIntegration / contextIsolation 无关------后者用错了反而引回依赖解析问题,还是安全反模式。
两条工程取舍:
- 打不打进 bundle? 纯 JS 依赖优先打进 bundle(更快、可 tree-shaking、自包含、可搬运);只有含原生
.node的依赖才必须 external + 传 node 路径。包体大主要影响安装包体积,不等于性能差,启动慢用懒加载/压缩治理,而不是踢出 bundle。 - 本 demo 落地 :utility-server 只依赖纯 JS 的 express,直接打进它的 bundle 即可,
UTILITY_SERVER_NODE_PATH+Module._initPaths()整套都能删掉(见第八节)。
一句话收尾:utilityProcess 复用的是 Node 生态和 app.asar 的 node_modules,但拿不到任何"主进程专属"的 Electron 能力;主进程才是那个持有全部 Chromium 子系统的根进程。
相关文档: