ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Vue dist反编译还原源码:sourcemap与反混淆实战

Vue dist反编译还原源码:sourcemap与反混淆实战 1. 先把期望值调对dist 反编译能做到什么程度拿到一个只有 dist 目录的 Vue 项目很多人第一反应是能不能一键还原成 src。我在实际项目里做过几次类似的活儿有顺利到十分钟搞定的也有啃了两天只还原出七成结构的。差别不在工具而在你对这件事的边界认不认清楚。vue 反编译 dist 包到源码这件事本质上不是解密而是信息重建——打包过程丢掉的信息你只能靠推断补回来补不回来的就是丢了。所以开工之前我一般会先问三个问题map 文件还在不在打包工具是 webpack 还是 Vite目标是能跑起来还是能看懂逻辑这三个答案直接决定后面走哪条路走错路会白白浪费时间。1.1 三种常见触发场景第一种是自己项目的源码丢了。比如早期版本没好好传仓库线上还在跑本地只剩一份 dist想改个文案都得重新啃一遍自己的代码。这种情况最舒服因为大部分时候 map 文件还能从服务器上捞到或者构建机器的缓存里还留着。第二种是接手别人的老项目。前同事离职交接只有一份 dist 和一句你自己看吧。这种项目往往连构建配置都没了package.json 里的依赖版本对不上你还原出来的代码能不能跑起来是另一回事。第三种是做技术审计或者学习。想搞清楚某个页面的实现思路或者想看看竞品前端是怎么组织的。这种场景其实不需要完整还原只要能读懂关键模块就够了投入产出比最高。三种场景对应三种不同的完成标准。第一种要可运行第二种要可维护第三种只要可读。别用第一种的标准去要求第三种那是自找苦吃。1.2 还原的天花板在哪里这里必须说实话。即使在最理想的条件下——dist 完整、map 文件齐全、还带 sourcesContent——你还原出来的也只是源码的文本不是工程的全部。丢的东西包括package.json里精确的依赖版本只能从产物特征反推大版本构建配置webpack.config.js / vite.config.ts基本全丢.env环境变量、CI 脚本、Dockerfile 这些跟构建产物无关的文件被 tree-shaking 摇掉的死代码这部分源码里本来就没用到丢了也无所谓被 CSS 压缩合并后丢失的注释和原始写法而最要命的丢失项是变量名的语义。压缩器会把userList变成e把handleSubmit变成n。就算你把代码格式化好、模块切分开满屏的e、t、n、r依然会让你读得头大。这个损失是无法完全补回的只能靠人工结合上下文重命名。至于 templateVue 的模板在编译阶段就被转成了 render 函数这个转换是有损且不可逆的。你可以写脚本把_createElementVNode(div, ...)机械地转回div但 v-if 的三元表达式、v-for 的渲染函数、动态组件这些东西机械转换出来的模板往往比原始写法别扭得多。我的经验是结构简单的小组件可以脚本转复杂组件老老实实人工重写反而更快。1.3 动手前必须确认的合规前提这一点我不想轻描淡写地带过。反编译这件事本身是个中性技术手段但用在哪里决定了它合不合规。自己写的代码、自己公司的代码、有书面授权的代码审计这些都没问题。但如果是对外部产品做逆向哪怕只是学习一下实现思路也要先确认授权范围别把技术能力用错地方。特别提醒一句不要把还原出来的代码直接用在生产项目里。压缩后的代码经过 AST 变换语义上可能有细微差别比如某些副作用顺序、this 指向直接拿来跑容易出玄学问题。还原的目的是理解和改造不是搬运。操作前先确认代码来源和授权边界这一步省不得。2. 解剖打包产物先认清 webpack 和 Vite 留下的指纹既然叫反编译那就得先知道编译干了什么。Vue 项目的 dist 包无非两种出身webpack 系的Vue CLI、webpack 手动配置和 Vite/Rollup 系的。这两者的产物结构差异非常大还原难度也不是一个量级。花十分钟认清指纹能帮你省掉几小时的瞎折腾。2.1 webpack 产物结构拆解打开一个 webpack 打包的app.js你大概率会看到这样一个骨架已格式化(() { use strict; var __webpack_modules__ { 123: (module, exports, __webpack_require__) { // 模块代码 }, 456: (module, exports, __webpack_require__) { // 模块代码 } }; var __webpack_module_cache__ {}; function __webpack_require__(moduleId) { var cachedModule __webpack_module_cache__[moduleId]; if (cachedModule ! undefined) return cachedModule.exports; var module (__webpack_module_cache__[moduleId] { exports: {} }); __webpack_modules__[moduleId](module, module.exports, __webpack_require__); return module.exports; } __webpack_require__(789); // 启动入口 })();这个结构对还原来说是好消息因为模块边界清清楚楚每个key: (module, exports, __webpack_require__) {}就是一个独立文件。你只要按大括号配对把每个函数体切出来就得到了文件级的粒度。关键线索在模块的key上。webpack 的optimization.moduleIds有四档配置配置值模块 ID 形态还原友好度natural0、1、2、3差完全无路径信息named./src/views/Home.vue极好路径直接暴露deterministic四位数字哈希如 5231一般稳定但无语义size按体积排序的数字差生产构建默认走deterministic所以你看到的大多是数字。但有个细节很多人不知道webpack 会把模块路径以注释形式保留下来形如/*!**************************!*\ !*** ./src/views/Home.vue ***! \**************************/这个注释叫 module comment只在optimization.minimizer里没开extractComments: true的时候保留。一旦你看到它等于白送一份目录结构。开工第一件事就是全文搜索/\*!这个特征串能捞多少捞多少。2.2 Vite/Rollup 产物结构拆解Vite 的产物读起来就舒服多了因为 Rollup 输出的是 ES Moduleimport语句会原样保留import { createElementVNode as _createElementVNode, openBlock as _openBlock } from vue; import { useRouter as _useRouter } from vue-router; function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) { // ... }看到没from vue、from vue-router这些信息全在。你一眼就知道这个文件依赖了什么包而 webpack 产物里这些全被__webpack_require__(123)吃掉了你得追着模块 ID 一个个找。但 Vite 也有它麻烦的地方代码分割更激进。一个项目可能被切成几十个 chunkindex-4f8a2c.js、Home-a3b9d1.js、vendor-8c2e0f.js……模块之间的依赖关系藏在import路径里你得写个脚本把所有 chunk 的 import 图跑一遍才能理清谁依赖谁。还有个 Vite 特有的痕迹值得记一下__vite__mapDeps函数。它把一个 chunk 依赖的静态资源列表编码成数组出现在动态 import 的场景里。看到这个函数说明这个 chunk 是懒加载路由的一部分通常能反推出路由结构。2.3 目录与文件指纹的判读除了 JS 骨架dist 目录本身的命名也有信息量。我整理了一个常见对应表实际用起来相当准产物文件名大概率对应判读依据app.[hash].js主入口含 App.vue 全局逻辑被 index.html 第一顺位引用chunk-vendors.[hash].jsnode_modules 依赖合集体积最大含 vue 运行时特征码chunk-common.[hash].js公共业务模块被多个 chunk 共享体积中等[数字].[hash].js路由懒加载页面webpack 动态 import 的默认命名Home.[hash].js具名路由页面构建时配了webpackChunkName魔法注释index.[hash].css全局样式被 index.html 的 link 引用assets/logo.[hash].png静态资源按 hash 去 JS 里搜引用即可定位归属index.html是天然的索引文件先读它能快速分出首屏必须加载和按需加载两批文件。这一步花不了几分钟但能让你后面分析模块依赖时心里有张地图。3. 最优解sourcemap 逆向还原完整流程如果 dist 里躺着.map文件恭喜你这活儿难度直接降一个数量级。sourcemap 是打包工具为调试而生成的生成代码 → 原始代码映射表它的设计初衷是让你在浏览器 DevTools 里看到源码——但同一份数据反过来用就是反编译的钥匙。我做过统计公司内部项目里大约七成的 dist 包会带 map 文件要么是同目录的app.js.map要么是内嵌在 JS 末尾的 data URL。剩下的三成要么构建时关了 sourcemap要么部署时被单独剥离hidden-source-map模式map 文件生成但不写注释。3.1 开工前先确认三件事在你写任何脚本之前先花两分钟做这三个检查能避免白忙一场。第一map 文件到底在不在。看 JS 文件末尾有没有这一行//# sourceMappingURLapp.4f8a2c.js.map有注释就顺着路径去找文件没有注释也别急着放弃很多构建配置会生成hidden-source-map文件在但不写引用直接去 JS 同目录ls *.map看看。第二map 里有没有 sourcesContent。这是决定性的。用上面那个脚本里的consumer.hasContentsOfAllSources()判断一下或者干脆打开 map 文件搜sourcesContent。webpack 的devtool: source-map默认会写入源码内容devtool: nosources-source-map则只写路径不写内容。有 content还原率接近 100%没 content就得走第 4 章的降级路线靠 mappings 自己拼。第三有多少个 map 要处理。大项目动辄几十个 chunk每个都有自己的 map。写脚本时记得遍历整个 dist 目录别只处理入口那一个。3.2 sourcemap 的 VLQ 编码与还原原理理解原理这件事直接决定你遇到异常时能不能自己排查。sourcemap 的核心字段是mappings一串由分号和逗号分隔的 Base64 VLQ 编码字符串AAAA,IAAM,KAAK;AACX,IAAM,KAAK分号分隔生成代码的行逗号分隔同一行内的映射段每段由 1、4 或 5 个数字组成分别表示生成代码列号第一个字段源文件索引对应sources数组下标源代码行号源代码列号变量名索引对应names数组可选这些数字不是绝对值而是相对于前一段的差值VLQ 的 delta 编码这样能大幅压缩体积。source-map这个 npm 库就是干解码这活的你不用自己手写 VLQ 解析。还原的算法思路很直白遍历sources里的每个源文件拿到它的所有映射段按源代码的行列位置排序然后把生成代码里对应位置的片段贴回去。但更省力的做法是——如果sourcesContent存在直接用sourceContentFor取内容就行压根不用碰 mappings。这也是为什么我前面反复强调先检查这一项。3.3 用 Node 脚本批量还原真正干活的时候我一般先试现成工具不行再自己写。这里推荐两个方案一reverse-sourcemap最快npx reverse-sourcemap -o ./restored -v ./dist/js/app.4f8a2c.js.map一条命令把 map 里能提取的源文件全部吐到./restored目录。它内部就是调source-map库自动处理 sourcesContent 和路径映射还能还原webpack://协议前缀。小项目基本这一条就够了。方案二自己写脚本可控性强当 map 文件有十几个、路径映射规则又复杂的时候自己写更灵活// restore.js const fs require(fs); const path require(path); const { SourceMapConsumer } require(source-map); const OUT_DIR path.resolve(./restored); // 清洗 webpack/vite 生成的伪协议路径 function normalizeSource(src) { return src .replace(/^webpack:\/\/[^/]*\//, ) // webpack:///./src/App.vue .replace(/^vite:\//, ) .replace(/^\//, ) .replace(/\.\.\//g, ); } async function restoreOne(mapPath) { const raw JSON.parse(fs.readFileSync(mapPath, utf8)); await SourceMapConsumer.with(raw, null, (consumer) { consumer.sources.forEach((src) { const content consumer.sourceContentFor(src, true); if (!content) return; // 没有内嵌内容就跳过 const target path.join(OUT_DIR, normalizeSource(src)); fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, content, utf8); }); }); } async function walk(dir) { for (const item of fs.readdirSync(dir, { withFileTypes: true })) { const full path.join(dir, item.name); if (item.isDirectory()) await walk(full); else if (item.name.endsWith(.map)) await restoreOne(full); } } walk(path.resolve(./dist)).then(() console.log(done));跑之前先npm i source-map。注意source-map在 v0.7 之后改成了 WASM 实现必须用SourceMapConsumer.with()这种异步写法老教程里的new SourceMapConsumer(raw)已经不能用了——这个坑我踩过报错信息还挺隐晦的。3.4 还原结果的校验与修正脚本跑完restored目录下应该出现了类似src/main.js、src/App.vue、src/views/Home.vue这样的结构。别急着高兴先做三步校验。第一步看文件数量对不对。一个中等规模 Vue 项目通常有 30 到 100 个源文件。如果只还原出 5 个大概率是 map 的 sourcesContent 不完整或者路径清洗规则把文件都合并了。第二步抽查关键文件内容。打开App.vue看是不是完整的单文件组件格式有template、script、style三个块。如果只有 script 内容没有 template说明 Vue 的 SFC 编译中间产物被单独标记了需要额外处理。第三步看有没有空文件被创建。有些构建会把被 tree-shaking 的模块也写进 sources 列表但内容为空。这种空文件要清掉否则你后面重建工程时会引入一堆无意义的空模块。校正环节最常见的问题是路径重复前缀。webpack 的路径可能是webpack:///./src/App.vueVite 可能是../src/App.vue如果你的清洗规则没写全还原出来就成了webpack_/src/App.vue这种畸形目录。遇到这种情况别手动改目录名回头改脚本的正则重跑一遍更靠谱。校验时优先比对文件总数和几个核心文件不要逐个文件肉眼扫效率太低。4. 没有 sourcemap 的降级路线从混淆代码里抠结构map 文件找不到的时候难度系数呈指数上升。但我得说这条路能走通只是慢。核心思路是分四轮推进先格式化把代码摊平再切模块把粒度做细然后上 AST 把变量名改回人样最后处理控制流混淆这种硬骨头。每一轮都有对应的工具没有哪个工具能一次搞定全部。4.1 第一轮格式化与体积统计拿到一个几 MB 的压缩 JS第一件事是格式化。工具选 Prettier 就行比 js-beautify 对现代语法的支持更好npx prettier --parser babel --print-width 100 dist/js/app.js app.pretty.js格式化的意义不只是好看。压缩代码往往整段挤在一行格式化后代码行数和文件体积能给你一堆线索一个 200KB 格式化的文件大概是 20 到 30 个业务模块合并的结果如果某个函数特别长超过 500 行它很可能是个复杂的业务组件值得优先攻。格式化完之后我习惯用这个命令统计一下函数数量grep -c function app.pretty.js # 或者针对箭头函数 grep -oE \s*\{ app.pretty.js | wc -l函数数量和项目实际模块数的比例能帮你判断压缩器用了多狠的合并策略。如果是 webpack 产物函数数量基本等于模块数加一两个运行时函数这个数字很有参考价值。4.2 第二轮webpack 运行时钩子与模块切分这一轮的核心目标是把大文件拆成多个小文件还原到一个模块一个文件的粒度。webpack 产物有天然的切分标志。回到第 2 章那个骨架每个模块都是模块ID: (module, exports, __webpack_require__) { ... }的形式。切分算法可以这样想找到__webpack_modules__这个对象的起始位置然后逐个扫描顶层键值对按大括号配对找到每个 value 函数的结尾。手工写这个扫描器容易出错字符串里有大括号、注释里有大括号都会干扰。所以我更推荐直接用现成工具webcrack它是专门干这个的npx webcrack dist/js/app.js -o ./unpackedwebcrack 会自动识别 webpack、browserify、esbuild 等多种打包格式把模块切成独立文件还会顺手把__webpack_require__调用改写成import把 CommonJS 转成 ESM。实测下来它对 webpack 5 的产物识别准确率很高Vite 产物就更不用说了本来就是 ESM。切分完之后unpacked目录里会出现1.js、2.js这样的文件。虽然文件名还是数字但至少每个文件是独立可读的了。接下来你要做的是给这些数字文件找身份。方法有三条线索可用看 import 表如果一个模块 import 了vue说明它是业务组件import 了一堆工具函数可能是 utils。看导出特征导出render或setup的是 Vue 组件导出普通函数的是工具模块。看字符串常量模块里出现的路由路径、接口地址、页面文案是判断归属的最强线索。我一般会写个简单的启发式脚本根据这三条线索给模块打标签然后人工确认一遍重要的几个。这个环节没有捷径但值得花时间因为标签打准了后面找文件就是秒级的事。4.3 第三轮AST 变量重命名与字符串解密到这一步代码能看了但满是e、t、n。这一轮要解决的就是可读性。先说变量重命名。诚实地讲自动恢复语义名是不可能的因为压缩过程丢失的就是语义。但我们可以做两件有用的事第一件用保持一致性的虚拟名。借助babel/traverse遍历每个模块的作用域收集所有 binding按出现顺序重命名为_v1、_v2、fn1、fn2这种。好处是同一个变量在整个模块里名字唯一你能用编辑器的查找引用功能追踪它比e这种遍地重复的名字强太多。// rename.js const parser require(babel/parser); const traverse require(babel/traverse).default; const generate require(babel/generator).default; const fs require(fs); const code fs.readFileSync(unpacked/1.js, utf8); const ast parser.parse(code, { sourceType: module }); traverse(ast, { Program(path) { let fnIdx 0; let varIdx 0; path.scope.bindings Object.values(path.scope.bindings).forEach((binding) { if (binding.kind hoisted binding.path.isFunctionDeclaration()) { binding.scope.rename(binding.identifier.name, func_${fnIdx}); } else { binding.scope.rename(binding.identifier.name, var_${varIdx}); } }); }, }); fs.writeFileSync(unpacked/1.renamed.js, generate(ast).code);第二件用类型推断辅助命名。如果一个变量被new调用过给它加个Class后缀如果它被赋值[]加个Arr后缀。这些微小的提示在阅读时很有帮助尤其是配合编辑器悬停时。再说字符串解密。混淆器常用的手段是把所有字符串常量藏进一个数组然后用一个解码函数按索引取const _0x4a2b [src, click, request, token]; function _0x1c3d(i) { return _0x4a2b[i]; } // 使用处 _0x1c3d(2) // 实际是 request破解办法是找到解码函数用 AST 把调用点全部替换成字面量。工具推荐obfuscator-io-deobfuscator它是专门针对这个混淆模式做的npx obfuscator-io-deobfuscator unpacked/1.js -o unpacked/1.decoded.js它内部会识别字符串数组 → 找到解码函数 → 内联所有调用 → 移除死代码。跑完之后那些_0x1c3d(2)会变成request可读性瞬间提升一个档次。要注意的是有些混淆器会在解码函数里加轮转逻辑字符串数组会被按偏移量旋转还有的加了多层嵌套调用。遇到这种情况工具可能只能解一层剩下的得手动处理。判断方法解密后如果还有_0x开头的调用残留说明只解了一层。4.4 第四轮控制流平坦化处理控制流平坦化是最恶心的一种混淆。原始逻辑function check(a) { if (a 0) return pos; return neg; }会被改写成function check(a) { const _order [5, 2, 4, 1]; let _idx 0; while (true) { switch (_order[_idx]) { case 1: return neg; case 2: if (a 0) { _idx 3; } break; case 4: return pos; case 5: break; } } }破解思路是识别while(true) switch这个 dispatcher 模式按_order数组的顺序重新排列 case 分支去掉状态变量还原成顺序控制流。这块可以用restringer这个库npx restringer unpacked/1.js -o unpacked/1.cf.js它能处理常见的控制流平坦化、死代码注入、对象属性改写等模式。但坦白说复杂的手写混淆它搞不定尤其是当作者在 dispatcher 里塞了动态计算的跳转索引时。这种最后只能靠人工阅读——好在真实项目里用到这种强度混淆的 Vue 项目其实很少见多数就是 UglifyJS 或者 Terser 的默认压缩加一层变量名混淆就完事了。5. 把 JS 碎片拼回 .vue 单文件组件前面四轮下来你应该得到了一堆能读的 JS 文件但它们还是JS 模块不是Vue 组件。这一步的目标是把这些碎片重新组织成.vue单文件。说实话这一步是工作量最大、最没法完全自动化的部分。5.1 从 render 函数反推 template 的现实做法Vue 的 template 会被编译成 render 函数。Vue 2 和 Vue 3 的编译产物长得不一样还原策略也略有差异。Vue 3 的编译产物典型长这样function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock(div, { class: container }, [ _createElementVNode(h1, { class: title }, _toDisplayString(_ctx.title), 1), _createElementVNode(ul, null, [ (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.list, (item) { return (_openBlock(), _createElementBlock(li, { key: item.id }, _toDisplayString(item.name), 1)); }), 128)) ]) ])); }Vue 2 的编译产物是这样的render(h) { return h(div, { staticClass: container }, [ h(h1, { staticClass: title }, [this.title]), h(ul, [this.list.map(item h(li, { key: item.id }, [item.name]))]) ]); }可以看到两个版本的 render 函数都是树形结构只是函数名不同。我一般会写一个半自动的转换脚本处理最常见的那几种调用render 调用对应 template转换难度_createElementVNode(div, null, ...)div.../div低直接替换_toDisplayString(x){{ x }}低_createElementBlock(_Fragment, null, ...)忽略视为透明容器低_renderList(arr, fn)v-for中需要识别循环变量三元表达式包住两个_createElementVNodev-if/v-else中需要模式识别_withDirectives(...)v-model/ 自定义指令高建议人工我的建议是先跑脚本生成一版草稿再人工校对。脚本能处理 60% 到 70% 的结构剩下的三元嵌套、插槽、动态组件人工重写反而比改脚本快。别指望 template 100% 自动还原这不现实。给脚本定个处理简单结构的目标复杂部分留给自己。5.2 style 归属与静态资源归位样式这块有个分水岭你的项目是通过什么方式引入 CSS 的。如果用了mini-css-extract-plugin或 Vite 默认配置样式会被抽成独立的.css文件。这时你需要按组件 hash 匹配回去。匹配的线索是 Vue 的 scoped 样式会生成[data-v-xxxxxxx]属性选择器而xxxxxxx这个 hash 是根据文件路径算出来的。虽然你不能反推路径但同一个 hash 的选择器一定来自同一个组件用这个把 CSS 分块归位准确率还不错。如果用了vue-style-loader老 Vue CLI 默认样式会被内联成 JS 字符串塞进document.head的style标签。这种就藏在模块代码里搜索document.createElement(style)或者insertBefore这种特征串能定位到样式注入代码。静态资源图片、字体相对好办——它们在 dist 里都是独立文件文件名带 hash。你只要在还原出的代码里搜索这个 hash就能知道哪个组件引用了哪个图片。写个脚本扫描一遍const assets fs.readdirSync(dist/assets); const code fs.readFileSync(unpacked/all.js, utf8); assets.forEach((file) { const hash file.match(/\.([0-9a-f]{8})\./)?.[1]; if (hash code.includes(hash)) { console.log(${file} 被引用于组件代码中); } });5.3 重建可运行的工程目录到这里你手上有一堆还原的 JS、CSS 和 assets。要让它跑起来还得做几步收尾第一步重建package.json。从还原出来的代码里 grep 所有import ... from xxx和require(xxx)去重后得到依赖列表。版本号得靠猜——去看依赖的 API 用法来判断大版本比如用了createRouter就是 vue-router 4用了new VueRouter就是 3。第二步补main.js和index.html。入口文件一般能从前面的模块切分里找到它就是那个没有被任何模块 import 的启动模块。index.html好说直接把 dist 的index.html改一下引用路径就能用。第三步重建vue.config.js或vite.config.js。这一步纯靠猜了。不过对大多数项目来说默认配置就能跑起来除非原项目用了特殊的 alias 或者自定义 loader。第四步逐组件验证。别一次性把所有组件都塞进去跑会报一堆错分不清主次。我的做法是先让入口和路由跑通然后一个页面一个页面地往工程里加每加一个就跑一次npm run dev看有没有报错。这样出问题容易定位。6. 常见问题与排查实录还原过程里踩过的坑我整理成一个速查表遇到问题先对着找大概率能命中。6.1 问题速查表现象大概率原因解决办法consumer.sourceContentFor返回 nullmap 是nosources-source-map生成的走第 4 章降级路线别在 map 上死磕还原目录出现webpack_前缀路径清洗正则没覆盖伪协议加一条replace(/^webpack:\/\/[^/]*\//, )SourceMapConsumer is not a constructorsource-map 版本 0.7改用SourceMapConsumer.with(raw, null, cb)还原出的文件内容全是空sources 列表里有被摇掉的模块过滤 content 为空的项不影响其他文件webcrack 解包后模块数对不上用了 splitChunks 拆成多 chunk对每个 chunk 分别跑一次再按 import 关系合并格式化时报Unexpected token源码里有 ES2020 新语法Prettier 加--parser babel别用 espree大文件处理时 Node 内存爆掉单文件超过 500MB启动时加--max-old-space-size8192变量重命名后代码报错重命名时改了全局变量或字符串里的引用只重命名 scope.bindings 里的局部变量别碰全局还原出的 template 打不开render 函数里有动态组件或 slot这部分交人工别硬跑脚本import路径全部报错原项目配了 webpack alias在新建的 vite.config.js / vue.config.js 里补齐 alias6.2 几条踩坑心得心得一先找 map别上来就啃混淆。我见过太多人拿到 dist 第一反应就是打开 JS 文件硬看结果两小时过去还在格式化。正确的顺序永远是先花两分钟找 map找到就直接通关找不到再去写降级脚本。心得二还原过程用 Git 管理。每完成一个轮次格式化、切分、重命名就 commit 一次。因为后面你可能会发现某一步改错了有 git 随时回滚比手动备份强一万倍。而且 diff 能帮你快速看清这一轮到底改了什么。心得三不要执着于 100% 还原。实际项目里能还原到 80% 可用就足够支撑后续维护了。剩下 20% 的边角用法比如某个奇怪的动态组件、某个自定义指令等你真的需要改那个功能的时候再针对性处理比一开始就死磕要高效得多。心得四还原出来的代码要重新跑一次 lint。自动变换过的代码经常留下未使用的变量、重复的 import、丢失的分号。跑一遍 ESLint 的 autofix能顺手清掉一堆噪音npx eslint restored/ --ext .js,.vue --fix心得五把还原脚本本身的成果保存下来。下次再遇到类似的活儿你会发现 80% 的脚本能直接复用。我现在的做法是把 restore.js、rename.js、split.js 三个脚本放在一个tools/目录里用参数控制输入输出路径成了一套反编译工具箱。第二次做这件事的时候从找 map 到跑出结构化代码半小时就能搞定。最后再分享一个小习惯还原别人的代码时我会在还原目录根下开个NOTES.md边看边记录这个模块是干什么的这个变量应该是传什么参数的。等还原做完这份笔记就是一份现成的模块说明文档交接给谁都好用。毕竟反编译的终点不是拿到代码而是让代码重新变得能被理解和维护。
返回列表