news 2026/9/30 3:49:24

Vue项目代码混淆实战:自定义插件与关键配置指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue项目代码混淆实战:自定义插件与关键配置指南

交付一个前端项目,最尴尬的时刻莫过于客户随口一句"你们这个包我看了下,接口地址和加密逻辑都写在里面了"。不少 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 模拟)逆向成本评估
仅 Terser1.6MB38s2.1s低,格式化即可读
标识符 + 字符串 base642.2MB62s2.4s中,能读但费劲
上一组 + 控制流 0.42.9MB98s3.0s高,静态分析基本失效
上一组 + 死代码 0.23.8MB156s3.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 里
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 3:49:15

MySQL数据出海同步方案:主从复制与CDC实战解析

做数据出海&#xff08;或者说跨地域数据同步&#xff09;这个方向时&#xff0c;我最深的感触是&#xff1a;数据库同步链路出问题&#xff0c;往往不是某一台机器宕机&#xff0c;而是“一切看起来都正常&#xff0c;但数据就是少了半小时”。尤其当源端是 MySQL&#xff0c;…

作者头像 李华
网站建设 2026/9/30 3:48:35

MATLAB搭建CNN-LSTM-SE注意力模型实现时序数据分类

Matlab里把CNN-LSTM-SE注意力机制串起来做数据分类预测&#xff0c;这件事我在不同数据集上反复折腾过好几轮。说实话&#xff0c;网上搜"CNN-LSTM-SE"十个结果九个是Python写的&#xff0c;剩下一个还是从PyTorch翻译过来的&#xff0c;MATLAB能直接跑的完整资料非常…

作者头像 李华
网站建设 2026/9/30 3:47:27

hindsight实战:基于MCP与Docker的LLM Agent记忆系统设计与部署

1. 从“hindsight”说起&#xff1a;为什么我们需要给Agent装上“后视镜”“hindsight”这个词&#xff0c;直译过来就是“后见之明”&#xff0c;或者更通俗一点——“事后诸葛亮”。放在人类身上&#xff0c;它指的是我们回顾过去、从经历中提炼教训的能力。而当我第一次看到…

作者头像 李华
网站建设 2026/9/30 3:46:48

2022年408真题解析:磁盘物理结构与DMA方式综合计算

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 3:46:37

MIMO卫星信道均衡:RLS算法原理与Matlab实现解析

我们需要先明确一件事&#xff1a;这篇博文我不会像教科书那样先列一大段“研究背景”&#xff0c;而是直接讲清楚这个项目到底在解决什么问题、代码怎么组织、踩过哪些坑。你搜到的“MIMO卫星信道均衡”“RLS算法”“Matlab代码”这些关键词&#xff0c;背后对应的是一类典型的…

作者头像 李华
网站建设 2026/9/30 3:45:47

快慢指针详解:从链表判环到数组找重复数

刷链表题刷到一定数量之后&#xff0c;你会发现有不少题目都在围着“遍历”打转——找中点、找倒数第几个、判断有没有环、判断是不是回文。这些题表面长得不一样&#xff0c;解法却共享同一个套路&#xff1a;让两个指针以不同速度往后走。这个套路在数据结构里叫快慢指针&…

作者头像 李华