@afterpack/electron 用一次调用混淆一个 Electron 应用所发布的全部三个程序(main、preload、renderer),并让它们共享同一个构建种子。如果手动接入,每一部分都是各自独立的打包工具构建,各自取一个随机种子,于是同一个应用的各个部分产出的结构彼此毫无关联。
electron-builder 不是打包工具。它把已经构建好的应用打包成安装程序,从不触碰你的 JavaScript,因此无论是否使用 AfterPack,它上游都需要一个打包工具。
安装
$ npm install -D @afterpack/electron// electron.vite.config.ts
import { withAfterpack } from "@afterpack/electron";
import { defineConfig, externalizeDepsPlugin } from "electron-vite";
export default withAfterpack(
defineConfig({
main: { plugins: [externalizeDepsPlugin()] },
preload: { plugins: [externalizeDepsPlugin()] },
renderer: {},
}),
);withAfterpack 包裹整个 electron-vite 配置(即 defineConfig 返回的对象、函数或 Promise 形式),并把 @afterpack/vite 接入该配置声明的每一部分,未声明的部分则不会接入。对等依赖:vite ^5 \| ^6 \| ^7 \| ^8。
构建
如何接入这一遍处理,取决于你的工具链是用一次调用配置全部三部分,还是每部分一个配置。
一次一个部分
分别配置每一部分的工具链(使用 @electron-forge/plugin-vite 的 Electron Forge)改为在每个配置中各调用一次 afterpackElectron:
// vite.main.config.ts (and vite.preload.config.ts / vite.renderer.config.ts)
import { afterpackElectron } from "@afterpack/electron";
export default { plugins: [afterpackElectron({ leg: "main" })] };leg 是必填项。
一个种子,三种方式
- **electron-vite(
withAfterpack)**和 Forge +plugin-vite:三个配置都在同一个 Node 进程中构建,因此各部分会自动共享一个种子。 vite-plugin-electron:同样是一个进程。把afterpackElectron({ leg: "renderer" })放在plugins的最后,位于vite-plugin-electron本身之后,这样 AfterPack 的generateBundle会对最终完成的 bundle 收尾。- 把每一部分作为单独命令运行的手写脚本:没有共享的进程,因此用环境变量在它们之间固定种子:
// package.json
"build": "AFTERPACK_seed=git vite build -c vite.main.ts && vite build -c vite.renderer.ts"AFTERPACK_seed 接受的取值与 seed 相同:一个整数、"git"(由 HEAD 派生),或者任何其他字符串(由引擎进行哈希)。显式的 seed 选项仍然优先于它。何时该固定种子、何时不该,参见种子策略。
验证
Protection Map 会在能找到 source map、且构建没有在 CI 中(CI=true 或 CI=1)运行时,按部分写入 .afterpack/<leg>/protectionMap.html。生产模式不会关闭它,但除非你要求,electron-vite 不会输出 source map,因此 Protection Map 默认关闭。请分别检查 main、preload 和 renderer。每份报告都包含你的完整源码。绝不要提交或对外提供它。
app.asar 中的一个 .map 就能直达你的原始源码。对于任何要打包的内容,都要保持 source map 输出关闭(生产环境的默认值)。
选项
每个配置键在这里同样可用,可以通过 withAfterpack(config, options) 应用到每一部分,或者通过 afterpackElectron(options) 按部分传入。以下选项只存在于这个包中:
| 选项 | 类型 | 默认值 |
|---|---|---|
leg | "main" | "preload" | "renderer" | 在 afterpackElectron 上必填 |
projectRoot | string | process.cwd()。electron-vite 把 renderer 构建的根目录设在 src/renderer/。如果你的构建从其他位置运行,请覆盖它 |
seed 默认为一个新值,由同一构建会话中的每一部分共享。protectionMap.enabled 遵循通常的 source map 规则,每一部分都会把自己的报告写入 .afterpack/<leg>/protectionMap.html。
// Heavier protection, reproducible per commit, applied to every leg:
withAfterpack(config, { preset: "hard", seed: "git" });指令
每一部分底层接入的都是 @afterpack/vite,因此指令捕获在这里的工作方式完全相同。
边界情况
支持矩阵
| 工具链 | 接入方式 | 自动共享一个种子? |
|---|---|---|
| electron-vite | withAfterpack(defineConfig({ … })),一次调用 | 是 |
Forge + plugin-vite | 在三个配置中各使用 afterpackElectron({ leg }) | 是 |
Forge + plugin-webpack | 在每个配置中使用 @afterpack/webpack | 是 |
vite-plugin-electron | afterpackElectron({ leg: "renderer" }),放在最后 | 是,只要放在该插件之后 |
| electron-builder | 它负责打包安装程序,不负责打包代码;请接入它上游的打包工具 | 不适用 |
| 手写脚本(独立进程) | 每部分一个插件,或 npx afterpack@latest <dir> | 否;请固定 AFTERPACK_seed |
它会拒绝什么
通过这个包接入的任何部分都会应用两项失败即中止的检查。
**bytecodePlugin。**electron-vite 自带的 bytecodePlugin 会把每个输出的 chunk 替换成一个 3 行的加载器桩代码,而把真正的应用以 V8 字节码(.jsc)形式发布。两者无法组合使用,因为字节码替换的正是 Rollup 生成的 chunk。在启用了 bytecodePlugin 的那一部分上,或者之后只要输出中出现 .jsc/.cjsc 文件,构建就会失败。二者只能择其一。
build.backup: true。备份会被直接拒绝:调用会抛出异常而不是继续执行,因为 .backup.<hash>.js 就是你完整的原始源代码,它被写在输出旁边,而这个目录树正是打包工具复制进 app.asar 的内容。每个安装该应用的用户都会拿到它。
同样的风险也适用于打包工具不知道要排除的任何产物。electron-builder 默认的 files glob 以 dot: true 匹配,且默认不排除 .afterpack/。请把以下内容从打包范围中排除:
// electron-builder
"files": ["**/*", "!.afterpack/**", "!**/*.backup.*", "!**/*.map"]
// electron-forge
packagerConfig: { ignore: [/^\/\.afterpack/, /\.backup\./, /\.map$/] }source map 只会收到警告而不会被拒绝:app.asar 中一个带有 sourcesContent 的 .map 就等于完全去混淆。把 sourceMap: { enabled: true, emitUrl: true } 限定在本地调试构建中,并把 *.map 从打包范围中排除。
IPC 通道
通道名在两个 bundle 之间必须逐字节一致。ipcMain.on("save-file", …) 和 ipcRenderer.send("save-file", …) 两端是同一个字符串。它们是普通输出 JavaScript 中的普通字符串字面量,因此让两部分使用相同的种子和相同的保护强度(preset / complexity),就能让两半保持一致。
如果某个通道名是在运行时拼出来的,而不是写成字面量,就把这段构造代码放进一个用 paths.exclude 排除的模块中,或者通过打包工具插件的 regions 条目降低该区域的保护强度(Pro)。
不使用插件:CLI
不想增加依赖,或者某一部分不是用 Vite 构建的?那就在构建之后,对每个输出目录各运行一次 npx afterpack@latest:
npx afterpack@latest out/main --seed=git
npx afterpack@latest out/renderer --seed=git不带路径参数时,CLI 会检测 out/,即 electron-vite 写入的目录;如果你的目录不同,请指定它。每次运行都会遍历目录,收集每一个 .js/.mjs/.cjs,并就地混淆它们;也可以把单个文件作为参数。在每次运行中固定同一个 --seed:由当前提交派生的 git 是最简单的保证方式。CLI 运行不会执行上述任何拒绝检查;它会混淆找到的所有 JavaScript。请在各次运行中保持相同的 --seed 和 --preset。
下一步
- 框架:完整矩阵。
- Vite:这个包所转发的选项语义。
- CLI 参考:备用路径的标志列表。
- 构建与 CI:种子、开发与生产环境的区别、原样提升同一份字节。
- 保护 Electron 应用的许可证逻辑:把这些应用到本地的许可证或试用期检查上。
- 最佳实践 → 种子策略:何时固定,何时不固定。
- Protection Map:如何阅读报告。
- 配置:每一个选项,以及哪个配置面可以设置它。