# JavaScript 混淆常见问题

关于 AfterPack 最常被问到的问题，每个都用几段简短的文字作答。

Source: https://www.afterpack.dev/zh/docs/faq

关于 AfterPack 是什么、是否免费、是否会破坏你的应用这类总体问题，答案在[首页常见问题](https://www.afterpack.dev/zh/#faq)中。本页是构建层面的细节。

## Free 和 Pro 有什么区别？

Free 会对整个代码包统一运行完整流程。Pro 增加了瞄准能力：你可以[标记一个区域](https://www.afterpack.dev/zh/docs/directives)，只有那部分代码会得到高强度处理。[层级](https://www.afterpack.dev/zh/docs/tiers)有完整的对比表和价格。

## 我需要框架插件吗？

如果是[一等框架](https://www.afterpack.dev/zh/docs/frameworks)，需要。把[插件安装](https://www.afterpack.dev/zh/docs/quickstart#pick-your-framework)为开发依赖，再加一行配置，你**平时的构建**就会产出已混淆的输出。引擎在哪里运行取决于打包工具：Vite、Rollup、webpack 和 Parcel 在打包工具自身的流程中混淆，[Next.js](https://www.afterpack.dev/zh/docs/frameworks/nextjs) 从 `next build` 内部的一个钩子中混淆，esbuild 和 Angular 则在各自的构建写入完成后再运行一遍。

```bash
npm install -D @afterpack/vite
```

对于纯打包工具或[零插件技术栈](https://www.afterpack.dev/zh/docs/cli)，改为把 [CLI](https://www.afterpack.dev/zh/docs/cli) 指向你的构建输出：

```bash
npx afterpack@latest dist/
```

无论哪种方式，AfterPack 都严格在构建之后运行，作用于你已构建好的输出。[构建目录](https://www.afterpack.dev/zh/docs/cli#usage)是可选的：不指定时，CLI 会检测你的构建输出。对于已支持插件的框架，CLI 同样是 [CI](https://www.afterpack.dev/zh/docs/builds) 和手动一次性运行的路径。

## 为什么同一份代码每次构建产出的结果都不同？

这就是[逐构建多态性](https://www.afterpack.dev/zh/docs/glossary#per-build-polymorphism)，也是首要的防御手段。在默认的随机[种子](https://www.afterpack.dev/zh/docs/config#seed)下，同一份输入的两次构建在结构上会各不相同。一个学会了第 N 次构建形态的静态去混淆器，在第 N+1 次构建上得不到任何可复用的东西，所以定期重新构建生产环境会不断重置攻击者的进度。

当你需要完全相同的字节，比如为了 CI 缓存命中或金标准快照测试时，用 [`--seed`](https://www.afterpack.dev/zh/docs/config#seed) 固定种子：

```bash
npx afterpack@latest dist/ --seed=git
```

发布模型是：开发构建保持未混淆以便快速迭代；生产环境**只**构建一次已混淆的产物，并把同一份字节依次推广到 qa、preprod 和 prod。参见[构建与 CI](https://www.afterpack.dev/zh/docs/builds)。

## Free 只是改名吗？

不是。Free 运行的是完整的引擎。默认的 `light` 预设会重命名标识符，并编码它能重写的每一个字符串字面量。从 `medium` 起，Free 还会在文件内部扁平化控制流、融合并膨胀逻辑，并重建常量。这样的输出是被重新组织过的，而不是仅仅换了标签，它停留在多项式单射的[还原类](https://www.afterpack.dev/zh/docs/reversal-classes)中：只有付出真正的、每次构建都要重来的功夫，才能恢复原始结构。

## 一条指令只影响一行吗？

不是。一条指令作用于一整个**[区域](https://www.afterpack.dev/zh/docs/directives#what-you-can-set)**，而不仅仅是它自己所在的那一行。在函数上方单独一行写 [`/* @afterpack preset=hard */`](https://www.afterpack.dev/zh/docs/config#preset) 来打开一个代码块，再在闭合花括号之后用 [`/* @afterpack end */`](https://www.afterpack.dev/zh/docs/directives#what-you-can-set) 关闭它，就能覆盖整个函数，包括函数体：

```ts
/* @afterpack preset=hard */
export function checkLicense(token: string) {
  // the entire function body is covered
}
/* @afterpack end */
```

这就是[代码块形式](https://www.afterpack.dev/zh/docs/directives#writing-one)：开启标记单独占一行，由配对的 `/* @afterpack end */` 关闭。对于单个值，一条行内的 [`/* @afterpack preset=extreme */`](https://www.afterpack.dev/zh/docs/config#preset) 会覆盖它这一行剩余的部分，也就是紧跟在它后面的字面量或表达式。完整语法见[指令](https://www.afterpack.dev/zh/docs/directives#writing-one)。

## 如果 Pro 云端不可用会怎样？

它会带退避地重试，然后[失败即中止](https://www.afterpack.dev/zh/docs/diagnostics#fail-closed-no-exceptions)：云端故障会让 CI 失败，而不是交付一个比你要求的保护更弱的版本。

如果你的构建路径完全不能依赖任何第三方，就在本地运行 [Free](https://www.afterpack.dev/zh/docs/pro#what-free-already-gives-you)——它从不发起网络请求。参见[部署模式](https://www.afterpack.dev/zh/docs/deployment-modes)。

## source map 是如何工作的？

AfterPack 的 source map 会把混淆后的输出链回你的原始源码。它们对本地调试很有用。永远不要把它发布到生产环境：一个公开的 `.map` 会给攻击者一条直通你刚混淆过的源码的路径。

AfterPack 默认会把它们挡在你的生产代码包之外：[生产环境](https://www.afterpack.dev/zh/docs/config#build-mode)构建下[生成 source map](https://www.afterpack.dev/zh/docs/config#sourceMap-enabled) 是关闭的，[`//# sourceMappingURL=`](https://www.afterpack.dev/zh/docs/config#sourceMap-emitUrl) 注释会被去掉，打包工具写出的 source map 也会被删除，包括 CSS map。除非声明为开发构建，否则每次构建都是生产构建，本地运行 `npx afterpack` 也是如此。参见[最佳实践](https://www.afterpack.dev/zh/docs/best-practices)。

## 我可以在 CI 中使用 AfterPack 吗？

可以。对于一等框架，插件会作为你平时构建的一部分运行，所以你现有的构建步骤已经会产出已混淆的输出。对于其他技术栈，把 [`npx afterpack@latest <path>`](https://www.afterpack.dev/zh/docs/cli#usage) 添加为构建后步骤。

把[种子](https://www.afterpack.dev/zh/docs/config#seed)固定到你的发布标识符以获得可复现的产物，只构建一次已混淆的输出，并把同一份字节推广到你的各个环境。对每次发布进行[审计](https://www.afterpack.dev/zh/docs/audit)，并把 [Protection Map](https://www.afterpack.dev/zh/docs/protection-map) 作为 CI 产物附上。完整的流水线示例见[构建与 CI](https://www.afterpack.dev/zh/docs/builds)。

## 混淆会拖慢我的代码吗？

会有一些，但不多。重新组织控制流和重建常量会增加运行时开销，但在典型的 Web 应用中，这个代价相对于网络和渲染时间通常很小。

[预设](https://www.afterpack.dev/zh/docs/presets#the-ladder)越重，代价越高：每一档对应更高的 [`complexity`](https://www.afterpack.dev/zh/docs/config#complexity)，默认是 `light`。更重的预设施加更多变换，但这并不是经过测量的保护提升。除非更重的预设值得它的代价，否则全局预设保持 `light`，并把 `extreme` 留给你标记的[区域](https://www.afterpack.dev/zh/docs/directives)。预设阶梯及其数值见[预设](https://www.afterpack.dev/zh/docs/presets#the-ladder)。

## 我该如何保护 API 密钥和许可证逻辑？

标记它们。对密钥字面量使用行内的 [`/* @afterpack preset=extreme */`](https://www.afterpack.dev/zh/docs/config#preset)，并把负责许可证校验的函数包在一个 [`/* @afterpack preset=hard */`](https://www.afterpack.dev/zh/docs/config#preset) … [`/* @afterpack end */`](https://www.afterpack.dev/zh/docs/directives#what-you-can-set) 代码块中：

```ts
const LICENSE_SECRET = /* @afterpack preset=extreme */ "YOUR_SECRET";

/* @afterpack preset=hard */
export function checkLicense(token: string) {
  /* ... */
}
/* @afterpack end */
```

[`preset=extreme`](https://www.afterpack.dev/zh/docs/config#preset) 会把该区域的[复杂度目标](https://www.afterpack.dev/zh/docs/config#complexity)提高到 25，并强制开启[字符串下限](https://www.afterpack.dev/zh/docs/config#strings-encode)，所以该字面量会被编码，而不是以明文形式发布。它仍然存在于输出中，程序会在使用时把它重建出来。要把你的代码用于比较的字面量完全排除在输出之外，打开 [`transforms.comparisonHardening.enabled`](https://www.afterpack.dev/zh/docs/config#transforms-kind-enabled)：比较会变成一次单向摘要校验，只有摘要会被发布。无论哪种方式，一个在被插桩的运行时下运行这段代码的攻击者仍然能够拿到那个值（见下一个问题）。这些指令提高的是每次发布静态还原的代价，并不能把一个高价值的密钥变成一个留在客户端也安全的东西。完整模式见[最佳实践](https://www.afterpack.dev/zh/docs/best-practices)。

## 运行我代码的人能提取出密钥吗？

能。一个坚定的攻击者只要在被插桩的运行时下运行你的代码，就能拿到代码本身用到的任何密钥。没有任何构建期混淆工具能改变这一点：如果运行中的程序需要这个值才能完成工作，那么对一个打了运行时补丁、勾住了正确调用，或在恰当时机对堆做了快照的人来说，它就是可观察的。

AfterPack 让这个值对代价更低的攻击不可见：静态阅读者，以及不运行代码、只靠解释代码的盲眼 LLM。[非物化](https://www.afterpack.dev/zh/docs/glossary#non-materialization)让受门控的字面量完全不出现在产物中，[字符串下限](https://www.afterpack.dev/zh/docs/config#strings-encode)让其余内容远离明文，[逐构建多态性](https://www.afterpack.dev/zh/docs/glossary#per-build-polymorphism)则让针对某次构建的模式编写的工具到下一次就会失效。运行代码的人仍然能读到它用到的值。

混淆提高的是一个客户端密钥的静态分析代价和重新提取代价。让高价值密钥真正脱离攻击者触及范围的唯一办法，是从不把它发布到客户端：把它留在服务端，由服务端做出判断。对那些确实无法迁移到服务端的密钥使用 [`preset=extreme`](https://www.afterpack.dev/zh/docs/config#preset)，并把它理解为提高代价，而不是当成一个保险柜。

## 支持哪些框架？

以下技术栈已发布插件：[Next.js](https://www.afterpack.dev/zh/docs/frameworks/nextjs)、[Vite](https://www.afterpack.dev/zh/docs/frameworks/vite)、[Webpack](https://www.afterpack.dev/zh/docs/frameworks/webpack)、[Rollup](https://www.afterpack.dev/zh/docs/frameworks/rollup)、[esbuild](https://www.afterpack.dev/zh/docs/frameworks/esbuild)、[Astro](https://www.afterpack.dev/zh/docs/frameworks/astro)、[Svelte](https://www.afterpack.dev/zh/docs/frameworks/svelte)、[SvelteKit](https://www.afterpack.dev/zh/docs/frameworks/sveltekit)、[Vue 3](https://www.afterpack.dev/zh/docs/frameworks/vue)、[Nuxt 3](https://www.afterpack.dev/zh/docs/frameworks/nuxt)、[Angular v17+](https://www.afterpack.dev/zh/docs/frameworks/angular)、[Electron](https://www.afterpack.dev/zh/docs/frameworks/electron) 和 [Parcel](https://www.afterpack.dev/zh/docs/frameworks/parcel)。插件知道自己的输出目录，所以不需要指向任何地方。

[Remix](https://www.afterpack.dev/zh/docs/frameworks/remix) 和[纯 JS](https://www.afterpack.dev/zh/docs/frameworks/plain) 在构建后通过 [CLI](https://www.afterpack.dev/zh/docs/cli#usage) 运行，由你自己指定目录（`npx afterpack@latest dist/`；Next.js 写入 `.next/`，Nuxt 写入 `.output/`）。完整对比表和各技术栈的配置见[框架](https://www.afterpack.dev/zh/docs/frameworks)；如果想看看真实代码包上的保护效果，可以看公开的[安全扫描器](https://www.afterpack.dev/zh/security-scanner)。

## 我该去哪里提问、报告 bug 或提出功能建议？

在 GitHub 上。问题和建议请提交到 [GitHub Discussions](https://github.com/afterpack-dev/afterpack/discussions)；bug 请提交到 [GitHub Issues](https://github.com/afterpack-dev/afterpack/issues)。安全问题请发送到 **security@afterpack.dev**，而不是发到公开讨论区，参见 [SLA、SLO 与支持](https://www.afterpack.dev/zh/docs/sla#security-disclosure)。

## 下一步

- [快速上手](https://www.afterpack.dev/zh/docs/quickstart)：几分钟内完成安装、构建、发布
- [层级](https://www.afterpack.dev/zh/docs/tiers)：Free 与 Pro，逐项功能对比
- [配置](https://www.afterpack.dev/zh/docs/config)：每一个选项，以及哪些形式可以设置它
- [指令](https://www.afterpack.dev/zh/docs/directives)：`@afterpack` 语法，以及哪些配置项适用
- [预设](https://www.afterpack.dev/zh/docs/presets)：五级阶梯
- [最佳实践](https://www.afterpack.dev/zh/docs/best-practices)：密钥、source map 和 CI
