交付一个前端项目,最尴尬的时刻莫过于客户随口一句"你们这个包我看了下,接口地址和加密逻辑都写在里面了"。不少 Vue 团队的第一反应是加个uglify就完事,结果发现变量名变成a、b、c之后,业务逻辑依然能被轻松还原——因为字符串还在、控制流还在、对象结构的键名还在。Vue 项目代码混淆这件事,难点从来不是"要不要做",而是"在 Vue 这套编译产物上,怎么做得干净、做得不误伤运行时"。这篇内容围绕 Vue 项目在构建阶段接入自定义插件做代码混淆这条路线展开,把每个配置项注释讲透,同时把我在多个中大型 Vue 项目上踩过的坑摊开来说。适合已经跑通npm run build、准备给产物上一道门槛的前端同学,也适合需要在 Vue CLI 与 Vite 两条链路上做取舍的技术负责人。全文的配置均可直接抄走改成自己项目的版本号与路径。
1. 交付包里那些"看起来很安全"的代码,其实一点不设防
1.1 从 dist 反推业务逻辑到底有多快
我拿一个典型的管理后台做过实验:vue-cli-service build出来的app.[hash].js,未经任何额外混淆处理,只开了 webpack 默认的TerserPlugin。用格式化工具还原后,整个文件从一行超长字符串变成两万多行可读代码,变量名虽然是e、t、n这种单字母,但函数之间的调用关系、axios.post('/api/v1/order/audit')这类接口路径、JSON.stringify前的字段组装、前端里写死的权限码判断,全部原样保留。整个过程不到十分钟。
这背后其实没什么黑科技,就是压缩工具的工作边界决定的。Terser的目标是减小体积,不是增加阅读成本。它做的事情是:删除空白与注释、缩短局部变量名、消除死代码、合并常量。它不做的事是:打乱控制流、把字符串抽走加密、给标识符随机化命名、把对象键转换成计算属性。所以只要你的代码结构本身就是清晰的,压缩之后它依然是清晰的。
真正让逆向成本上去的是另外几层:字符串抽离到数组并做编码、控制流扁平化、无用代码注入、标识符名生成器切换成十六进制或者字典模式。这几件事Terser都不管,得靠专门的混淆器来做——javascript-obfuscator是当前开源生态里最常用的一个,webpack-obfuscator和 Vite 侧的几个插件本质上都是它的壳。
1.2 混淆、压缩、加密,这三件事别再混为一谈
我在项目评审时经常听到"我们代码加密了"这种说法,一问才发现只是开了Terser的参数调优。这三件事必须分清楚,因为它们对应完全不同的成本与收益:
| 手段 | 典型工具 | 主要作用 | 对运行时的代价 | 能否还原 |
|---|---|---|---|---|
| 压缩 | Terser、esbuild | 减小体积、去掉无用代码 | 几乎为零 | 极易还原 |
| 混淆 | javascript-obfuscator | 提高可读性门槛、打乱控制流 | 体积增大、执行变慢 | 成本大幅上升,但理论可还原 |
| 加密 | 无标准前端方案 | 前端无法真正加密 | 不适用 | 密钥必然在前端 |
前端不存在"加密"这回事。任何需要浏览器执行的代码,密钥最终都要出现在运行环境里。混淆的价值是把逆向成本从十分钟拉到几天,而不是做到不可破解。真正的业务安全要靠服务端校验、签名、风控,这一点必须提前跟需求方对齐预期。
把预期讲清楚之后再动手,后面方案取舍就不会跑偏。比如"要不要开启debugProtection"这种问题,本质上问的是"我们愿不愿意为了防调试牺牲一部分低端设备上的体验",而不是"这个开关厉不厉害"。
2. 选型先看构建链路:Vue CLI 和 Vite 走的不是同一条路
2.1 现成插件能覆盖多少场景
如果你的 Vue 项目还是vue-cli体系,底子是 webpack 4/5,webpack-obfuscator可以直接用,挂到configureWebpack.plugins里,几十行配置就能跑起来。Vite 侧的选择相对零散一些,社区里有几个基于rollup钩子的封装插件,名字换来换去,但核心都是在renderChunk阶段对每个 chunk 调一次obfuscate。
我一开始也是直接用现成插件的,直到遇到三个绕不过去的问题:
第一个是豁免粒度不够。项目里总有些文件是不能混淆的,比如引用了webpack运行时辅助函数的 chunk、某些必须在全局作用域暴露名字的 SDK 桥接文件、还有 Vue 的runtime相关代码。现成插件通常只给一个exclude数组,匹配的是文件名,遇到vendor被拆成chunk-1a2b3c.js这种哈希命名时就抓瞎。
第二个是多 chunk 的reservedNames无法差异化。比如主包需要保留__vite__mapDeps,某个懒加载子包需要保留它自己动态引用的全局函数名。用一个全局配置去覆盖所有 chunk,要么保留得太多导致混淆失效,要么保留得太少导致子包运行时报xxx is not a function。
第三个是sourcemap 的传递断链。javascript-obfuscator在开启sourceMap时会产出新的 map,但部分封装插件没有把map正确回传给构建器,结果是构建日志里报了 sourcemap 生成成功,实际上传上去的是一份对不上的旧 map,线上报错的堆栈全是错的。
2.2 为什么最后还是决定自己写一个插件
这三个问题归结起来都是同一件事:我需要按 chunk 名字、按 chunk 大小、按构建环境来决定混淆策略,而不是一刀切。自己写插件的代码量其实很小,核心逻辑不到一百行,但换来的是完全可控:
- 可以针对
env.production和env.staging用不同的强度 - 可以按 chunk 体积动态降级(超大 chunk 关闭死代码注入,避免构建时间爆炸)
- 可以在插件内部统一注入保留名列表,业务代码里不用到处写
// obfuscator:disable - 出问题时能在插件里打日志,输出每个 chunk 混淆前后的体积对比,方便定位
另外一个附加好处是团队协作:配置文件集中在build/plugins/目录下,配置项旁边直接写注释,新人接手时不用去翻插件的文档站找参数含义。
3. 自定义插件骨架:把混淆挂到正确的构建钩子上
3.1 webpack 插件的 apply 时机选择
在 webpack 里做这件事,钩子的选择非常关键。有人图省事挂emit,那个阶段资源已经交给文件系统了,改起来别扭;有人挂optimizeChunkAssets,在 webpack 5 里已经被标记为废弃。正确的位置是processAssets,并且要指定阶段。
// build/plugins/webpack-plugin-obfuscate.js const JavaScriptObfuscator = require('javascript-obfuscator') const { Compilation, sources } = require('webpack') class WebpackPluginObfuscate { constructor(options = {}) { this.options = options } apply(compiler) { const { include = /\.js$/, exclude = /node_modules/, // 大于该体积(KB)的 chunk 自动降级,避免构建时间失控 sizeLimitKB = 1500, reservedNames = [], ...obfuscatorOptions } = this.options compiler.hooks.thisCompilation.tap('WebpackPluginObfuscate', (compilation) => { compilation.hooks.processAssets.tapPromise( { name: 'WebpackPluginObfuscate', // 必须放在压缩之后,否则会被后续 Terser 二次处理后破坏其结构 stage: Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE, }, async (assets) => { const tasks = Object.keys(assets) .filter((name) => include.test(name) && !exclude.test(name)) .map(async (name) => { const raw = assets[name].source() const code = typeof raw === 'string' ? raw : raw.toString('utf-8') const sizeKB = Buffer.byteLength(code, 'utf-8') / 1024 // 超大 chunk 降级:只做字符串数组,不做控制流扁平化 const isOversize = sizeKB > sizeLimitKB const finalOptions = isOversize ? { ...obfuscatorOptions, controlFlowFlattening: false, deadCodeInjection: false } : obfuscatorOptions const result = JavaScriptObfuscator.obfuscate(code, { sourceMap: false, reservedNames: [...reservedNames], ...finalOptions, }) compilation.updateAsset( name, new sources.RawSource(result.getObfuscatedCode()) ) compilation.getLogger('WebpackPluginObfuscate').info( `${name} ${sizeKB.toFixed(1)}KB -> ` + `${(Buffer.byteLength(result.getObfuscatedCode(), 'utf-8') / 1024).toFixed(1)}KB` + `${isOversize ? ' [降级]' : ''}` ) }) await Promise.all(tasks) } ) }) } } module.exports = WebpackPluginObfuscate这里有个细节值得单独说:PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE这个阶段的位置在默认的 Terser 压缩之后。也就是说,webpack 先用自己的压缩器把代码压小,我们再在此基础上做混淆。顺序反过来的话会出大问题——selfDefending这个开关会让代码的格式变得极其敏感,一旦混淆完再被 Terser 二次压缩,变量名和换行被重写,运行时会直接抛异常。
3.2 Rollup/Vite 插件的 renderChunk 与 generateBundle
Vite 底层是 Rollup,插件走的是 Rollup 的钩子体系。这里要重点提醒的是 Vite 的build.minify默认是esbuild,而renderChunk的执行顺序与enforce有关。
// build/plugins/vite-plugin-obfuscate.js import JavaScriptObfuscator from 'javascript-obfuscator' // 这些名字在 Vue 3 的产物里会出现,混淆后会导致运行时找不到全局标记 const BASE_RESERVED = [ '__vite__mapDeps', '__vite__injectQuery', '__VUE_OPTIONS_API__', '__VUE_PROD_DEVTOOLS__', '__VUE_PROD_HYDRATION_MISMATCH_DETAILS__', ] export default function vitePluginObfuscate(options = {}) { const { include = /\.js$/, exclude = /node_modules/, sizeLimitKB = 1500, reservedNames = [], ...obfuscatorOptions } = options return { name: 'vite-plugin-obfuscate', apply: 'build', // post 保证在 Vite 内部插件之后执行 enforce: 'post', renderChunk(code, chunk, outputOptions) { if (!include.test(chunk.fileName) || exclude.test(chunk.fileName)) { return null } const sizeKB = Buffer.byteLength(code, 'utf-8') / 1024 const isOversize = sizeKB > sizeLimitKB const finalOptions = isOversize ? { ...obfuscatorOptions, controlFlowFlattening: false, deadCodeInjection: false } : obfuscatorOptions const result = JavaScriptObfuscator.obfuscate(code, { sourceMap: false, reservedNames: [...BASE_RESERVED, ...reservedNames], ...finalOptions, }) this.warn( `[obfuscate] ${chunk.fileName} ${sizeKB.toFixed(1)}KB -> ` + `${(Buffer.byteLength(result.getObfuscatedCode(), 'utf-8') / 1024).toFixed(1)}KB` + `${isOversize ? ' [已降级]' : ''}` ) return { code: result.getObfuscatedCode(), map: null, } }, } }挂载方式很直接:
// vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import vitePluginObfuscate from './build/plugins/vite-plugin-obfuscate' export default defineConfig({ plugins: [ vue(), vitePluginObfuscate({ include: /assets\/.*\.js$/, exclude: /node_modules/, // 具体配置项见第 4 节 }), ], })注意:如果你在混淆配置里开启了
selfDefending: true,务必把build.minify设为false,或者改用terser并确认插件顺序在压缩之前。我遇到过最典型的故障就是"开发环境一切正常,生产环境特定页面白屏",最后定位到是 esbuild 压缩把自保护代码的结构改坏了。
3.3 为什么不用 inline 注释逐文件豁免
javascript-obfuscator支持在源码里写// obfuscator:disable这类注释来跳过特定文件的混淆。我在小项目里用过,大项目里果断放弃了——因为一旦这个机制存在,团队里就会有人图省事到处加,半年后你根本不知道哪些文件其实没被混淆。统一在插件里用include/exclude正则控制,配合一章一节的目录约定(比如必须混淆的算法模块统一放在src/core/secure/),可控性高得多。
4. 配置项逐条注释:哪些敢开,哪些开了就白屏
这一节是重点。下面每个配置项我都会写清楚它的作用、代价,以及在 Vue 项目里的实际建议值。
4.1 标识符与字符串相关的开关
标识符混淆是最基础的一层,成本也最低。identifierNamesGenerator决定了变量名被替换成什么样子:
hexadecimal:默认值,生成_0x4a3f1b这样的名字,可读性最低,代价最小mangled:生成a、b、c,看起来和 Terser 结果类似,体积最小但可读性回升mangled-shuffled:在第 2 种基础上打乱映射顺序,同一变量在不同构建里名字不同dictionary:从你提供的词典里取词,体积最大,适合确实想伪装成"正常代码"的场景
{ // 标识符命名策略,业务包建议 hexadecimal,对体积敏感的可选 mangled-shuffled identifierNamesGenerator: 'hexadecimal', // 给所有混淆标识符加统一前缀,多 chunk 场景下能显著降低变量冲突概率 identifiersPrefix: 'hx_', // 是否混淆全局变量名,Vue 项目里强烈建议 false // 开了之后 window 上挂载的自定义方法名会被改掉,跨包通信直接断 renameGlobals: false, // 字符串抽离到数组,这是提升逆向成本性价比最高的一项 stringArray: true, // 字符串编码方式,base64 性能好,rc4 体积小但每次取值都要解密运算 stringArrayEncoding: ['base64'], // 抽离比例,0.75 是默认值,调到 1 意味着所有字符串都进数组,体积涨得很快 stringArrayThreshold: 0.75, // 数组轮转与打乱,必须配合 stringArray 使用,否则无效 rotateStringArray: true, shuffleStringArray: true, // 字符串拆分,把长字符串切成小段拼接 // 注意:会破坏正则字面量和部分 URL 拼接逻辑,谨慎开启 splitStrings: false, splitStringsChunkLength: 10, }关于stringArrayEncoding的选择,我实测过一个 800KB 的产物包:
| 编码方式 | 混淆后体积 | 首屏可交互时间变化 | 说明 |
|---|---|---|---|
| 不开启字符串数组 | 800KB | 基准 | 逆向成本低 |
| base64 | 约 1080KB | +4% 左右 | 推荐默认值 |
| rc4 | 约 950KB | +11% 左右 | 每次取值都运算,低端机吃力 |
| base64 + rc4 混合 | 约 1150KB | +15% 左右 | 只在核心算法模块用 |
rc4看起来体积更小,但它是运行时解密,字符串读取从一次数组下标访问变成了一次函数调用加异或循环。业务代码里字符串读取的频率远超想象,尤其是在v-for里做判断的时候。
4.2 控制流与死代码:性能换强度
这两个是"强度陡增、代价也陡增"的开关。控制流扁平化会把顺序执行的if/else、for循环改写成while加switch的状态机结构,让静态分析工具很难画出流程图。死代码注入则是往代码里塞永远执行不到的分支和虚假字符串。
{ // 控制流扁平化,默认关闭;开启后反编译难度上升一个量级 controlFlowFlattening: true, // 扁平化比例,0.75 是默认值。实测低于 0.3 基本看不出效果,调到 1 构建时间会翻倍 controlFlowFlatteningThreshold: 0.5, // 死代码注入,不要轻易开,体积膨胀非常明显 deadCodeInjection: false, deadCodeInjectionThreshold: 0.2, // 死代码注入时一并注入干扰用的字符串,进一步增加分析成本 deadCodeInjectionIdentifiersPrefix: 'dc_', // 数字转表达式,比如把 1000 写成 0x3e8 + 24 - 12 // 对体积敏感的项目建议关闭 numbersToExpressions: true, // 简化代码,会做一些等价替换,通常保持开启 simplify: true, // 把对象的键转换成计算属性访问,兼容性风险较高 transformObjectKeys: false, }transformObjectKeys这一项我要单独强调。它会把{ foo: 1 }这种写法改造成等价的动态赋值形式。在纯业务对象上没问题,但如果你想通过Object.keys()、for...in去遍历,或者这个对象是传给 Vue 的props、provide/inject的响应式数据,行为可能会变。我在一个项目里就因为它导致watch的深度侦听失灵,排查了大半天。
4.3 必须拉黑的名字列表
保留名列表是混淆配置里最容易出事的地方。默认情况下,reservedNames是一个支持通配符的数组。我整理了一份 Vue 项目里比较稳妥的基线:
{ reservedNames: [ // 构建工具注入的全局标记 '^__vite__', '^__webpack_', '^__VUE_', // Vue 运行时核心导出,虽然一般不混淆 node_modules,但内联进业务包的辅助函数名要保 '^(createApp|defineComponent|nextTick|h|ref|reactive|computed|watch)$', // 自定义元素与指令注册相关的名字 '^v-', // 项目里通过 window.xxx 暴露给原生 App 或第三方 SDK 的桥接方法 '^(bridge|jsBridge|AndroidBridge|iOSBridge)$', // 错误监控与埋点 SDK 依赖的全局钩子 '^(reportError|trackEvent|sendBeacon)$', ], // 忽略 require/import 语句中的标识符,防止 UMD 包装代码被改坏 ignoreRequireImports: true, // 是否保留函数名(Function.prototype.name) // 依赖函数名做路由匹配、组件注册的项目必须开启 keepFnName: true, }这里有个容易忽略的点:Vue Router 的路由匹配、
<keep-alive>的include列表、动态组件的is属性,本质上是靠字符串匹配的,字符串本身不会被标识符混淆影响。但如果你的代码里写的是component.name或SomeComponent.name去取值,那函数名一旦被改,匹配就失效了。keepFnName就是给这类场景兜底的。
4.4 一份完整的参考配置
把上面几节拼起来,这是我在一个中型后台项目上稳定运行了两个版本的配置:
// build/obfuscate.config.js module.exports = { // ---------- 标识符 ---------- identifierNamesGenerator: 'hexadecimal', identifiersPrefix: 'hx_', renameGlobals: false, keepFnName: true, // ---------- 字符串 ---------- stringArray: true, stringArrayEncoding: ['base64'], stringArrayThreshold: 0.6, rotateStringArray: true, shuffleStringArray: true, splitStrings: false, // ---------- 控制流 ---------- controlFlowFlattening: true, controlFlowFlatteningThreshold: 0.4, deadCodeInjection: false, numbersToExpressions: true, simplify: true, transformObjectKeys: false, // ---------- 自我保护 ---------- // 开启后代码不能被二次格式化,同时体积会增加约 8% selfDefending: true, // 调试保护,会周期性触发 debugger,低端 WebView 上会造成明显卡顿 debugProtection: false, debugProtectionInterval: 0, // 是否禁用 console,会连带干掉 Vue 的 warn 输出,排查问题时很痛苦 disableConsoleOutput: false, // ---------- 其他 ---------- target: 'browser', // 固定 seed 让每次构建结果一致,方便做产物比对和增量缓存 seed: 20240101, // 生产环境不产出 sourcemap,如需产出也不要上传到公网 sourceMap: false, compact: true, unicodeEscapeSequence: false, }这份配置的前提是:exclude掉node_modules相关的 chunk,include只匹配业务代码产出的assets/*.js。核心算法模块单独走一份更重的配置,把stringArrayEncoding换成['rc4']、splitStrings打开。
5. 混淆之后的翻车现场与排查链路
5.1 白屏:从控制台第一条报错倒推
混淆开通第一天就白屏是常事,关键是要有一套固定的排查顺序,而不是盲目改配置。
第一步,本地用npx serve dist起一个静态服务,打开控制台看第一条报错。如果是Uncaught ReferenceError: _0x4a3f is not defined,说明某个被保留名字引用到的全局变量被改名了,去reservedNames里补漏。
第二步,看报错位置是否在vendorchunk。如果错误集中在第三方库所在的 chunk,八成是exclude没生效,把node_modules的内容也混淆了。第三方库里大量使用typeof define === 'function' && define.amd这种环境探测,标识符一改就废。
第三步,如果报错是TypeError: Cannot read properties of undefined (reading 'xxx'),并且堆栈指向 Vue 运行时,那大概率是controlFlowFlattening对某些依赖执行顺序的代码产生了影响。做法是把controlFlowFlatteningThreshold从 0.8 降到 0.3 再试,逐步二分定位。
第四步,如果刷新页面偶尔正常、偶尔白屏,那就要怀疑debugProtection或者动态代码注入在高并发下的时序问题。这类问题最阴,建议直接关掉相关开关。
5.2 路由与 keep-alive 失效的真实原因
有一类故障特别有迷惑性:页面不白屏,路由也能跳,但<keep-alive>的缓存彻底失效了,每次切换都重新挂载组件,接口被反复请求。表面看像是缓存逻辑写错了,实际上往往和混淆有关。
原因在于,很多项目会这么写缓存白名单:
const cachedViews = computed(() => tagsViewStore.cachedViews) // 模板中 // <keep-alive :include="cachedViews">cachedViews里存的是组件名。而组件名的来源常见有两种写法:一种是defineComponent({ name: 'UserList' }),这里的name是字符串常量,不受标识符混淆影响;另一种是通过路由配置route.name或者从组件对象上取component.name。后一种在keepFnName: false的情况下会拿到混淆后的名字,白名单自然对不上。
修复方式有两个:要么打开keepFnName,要么把所有缓存白名单统一改成从字符串常量维护,不依赖运行时反射。
5.3 打包体积、首屏时间与移动端 WebView 的实测
我把同一次构建的产物做了四组对比,环境是 4 核 8G 的构建机,产物为 Vue 3 + Vite 的企业管理后台,业务代码约 1.6MB(未压缩):
| 配置组合 | 业务包体积 | 构建耗时 | 首屏可交互(4G 模拟) | 逆向成本评估 |
|---|---|---|---|---|
| 仅 Terser | 1.6MB | 38s | 2.1s | 低,格式化即可读 |
| 标识符 + 字符串 base64 | 2.2MB | 62s | 2.4s | 中,能读但费劲 |
| 上一组 + 控制流 0.4 | 2.9MB | 98s | 3.0s | 高,静态分析基本失效 |
| 上一组 + 死代码 0.2 | 3.8MB | 156s | 3.6s | 很高,但性价比偏低 |
结论很明确:控制流扁平化是性价比的分水岭,它带来的强度提升最明显;死代码注入在最后一段收益递减得厉害,构建时间和体积双双翻脸。我们最终在生产环境采用的是第三组配置,只在核心算法模块上叠加死代码注入。
移动端 WebView 还有两个要注意的点。一是decodeURIComponent相关的字符串如果在数组里,每次解码都要走一遍 base64 解码,扫码页这种高频交互场景会掉帧。二是 Android 低端机的 JIT 对扁平化后的while-switch结构优化效果较差,同样逻辑的执行耗时可能翻倍。上线前务必在目标机型上跑一遍。
5.4 第三方库与 sourcemap 的二次泄露
即便你混淆了业务代码,如果vendor包里带着完整可读的第三方库源码,分析者依然能通过库的特征找到你的使用方式。我的做法是:第三方库不混淆,但用splitChunks把它们集中到固定的 vendor chunk,然后对这些 chunk 单独开一档轻量混淆(只做标识符 +stringArray),不做控制流。
sourcemap 这块坑更直接。sourceMap: true会让混淆器同时产出一份.map文件,如果构建流程里没做处理,它会被一起打包进dist目录。而这份 map 里的sourcesContent通常包含原始未混淆的源码。等于说你辛辛苦苦混淆了一通,攻击者打开 DevTools 的 Sources 面板,一切都原样呈现。
我的处理方式是双层保险:生产构建里sourceMap: false;如果确实需要错误监控的堆栈还原,就在 CI 阶段生成 map 后单独上传到监控平台,然后在构建产物里删掉.map文件,并且给监控平台的 accessKey 做权限隔离。
6. 分阶段落地:什么项目该上多重的混淆
不是所有项目都值得上全套混淆。按我的经验分三档更实际。
对外交付、需要装进客户设备的项目,比如部署在内网的老旧管理后台、需要交付源码给甲方二次开发的定制系统,这类场景值得上重配置:控制流扁平化开到 0.5、字符串数组全开、必要时对核心模块开死代码注入。这里放弃一点性能换来的保护是划算的。
公开访问的 C 端页面,比如营销活动页、在线工具站,重点应该放在性能上。混淆只用轻量档:标识符 +stringArray就够了,控制在 30% 以内的体积增幅。这类项目的核心资产通常不在前端代码里。
开源或已在 GitHub 上的项目,混淆基本没意义,反而会拖累社区贡献者的调试体验。这类项目应该把精力放在服务端的鉴权和限流上。
另外还有两个和混淆没有直接关系、但经常被一起提到的配置:vite的env.production环境变量文件管理,以及打包后的资源前缀配置。这两块如果配错,表现和混淆出错非常像(都是生产环境特有、本地正常),排查时容易混淆。我的习惯是把构建产物用npx serve dist本地跑一遍,先排除环境变量和 base 路径的问题,再怀疑混淆。
最后分享两个我在插件里加的小功能,非常实用。一个是构建完成后打印体积增长报告,列出体积增幅超过 50% 的 chunk,方便判断是不是某个正则或者大段字符串被抽进了数组。另一个是提供一个dryRun开关,打开后只统计不实际改写产物,用来评估新配置的影响面——尤其是在改stringArrayThreshold这类数值参数时,先 dry run 一遍能省不少反复构建的时间。
广告时间
划重点
- 混淆不是加密,它只提高逆向成本,真正的业务校验必须在服务端
- 自定义插件的价值在于按 chunk、按体积、按环境做差异化策略
selfDefending和二次压缩互斥,开了它就要关掉build.minify- 控制流扁平化是强度与代价的分水岭,死代码注入性价比最低
keepFnName、reservedNames、transformObjectKeys这三个开关是白屏事故高发区- 生产环境的 sourcemap 要么不出,要么出完就删,绝不留在 dist 里