尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

AI写的项目能跑但不敢改?我的6步重构顺序

AI写的项目能跑但不敢改?我的6步重构顺序 发布元信息发布前删掉此区块标题备选《AI 写的项目能跑但不敢改我的 6 步重构顺序》《接手一个 AI 生成的项目我做了这 6 件事》关键词代码重构、AI 辅助编程、技术债、Tauri、Rust、单一数据源配图4 张图 1–图 4位于 images/ 目录正文已带编号图注可直接沿用系列位置第 4 篇共 9 篇本系列① Tauri 桌面应用上线前必须先查的 3 个安全细节② 不用 Git如何在 SQLite 里实现版本控制与回滚③ Tauri 应用自动更新从签名密钥到三平台发布的完整流程④AI 写的项目能跑但不敢改我的 6 步重构顺序本篇⑤ 本地优先应用的 SQLite 表设计5 个让我返工的取舍⑥ FTS5 真比 LIKE 快吗我把两万条中文数据压测了一遍⑦ 小工具别急着上 React万行数据下原生渲染的性能账⑧ 手写词级 Diff从 O(n²) 到可用的 6 行代码⑨ 本地应用的数据迁移schema 演进的坑与一条能回退的路读完这篇你能拿到什么一套用 grep 就能跑的量化清单十分钟摸清项目家底三个最典型的结构问题以及它们各自的改法一个让 AI 帮你重构而不会越改越乱的任务描述模板一份有先后顺序的改造路线避免中途返工引言AI 交给你的是草稿不是成品用 AI 从零生成一个项目速度确实惊人——几轮对话就能得到一个能跑起来的应用。但当你第二天打开它想加个功能时往往会愣住同一个功能在两处实现改了一处另一处没变每个接口都在重复同一段初始化代码状态藏在全局变量里谁改的不知道想删一个函数发现有十几个地方在调它这些问题不是 AI 的错。AI 优化的是本次回答是否解决了当前问题而工程优化的是下一个改动成本是否可控。目标函数不同产出自然不同。最近接手了一个这样堆出来的桌面应用几千行代码功能完整但不敢动。下面是收拾它的完整顺序可以直接套用到任何 AI 生成的项目上。一、先止血搞清楚它到底在做什么不要上来就重构。第一步是建立事实清单靠工具而不是靠读# 重复度同名函数出现几次grep-rnfunction loadItemssrc/|wc-l# 规模分布找出需要重点关注的巨型文件wc-lsrc/*.ts src-tauri/src/*.rs|sort-rn# 可疑写法直接操作 DOM、明文凭据、被注释掉的代码块grep-rninnerHTML\|API_KEY\|TODOsrc/三个信号一出现基本就能锁定问题区信号说明同名函数 / 同类 SQL 出现多次逻辑被复制过改一处会漏另一处单个文件超过 800 行职责混杂往往是全项目的垃圾桶文件初始化代码在每个函数里重复缺少统一生命周期管理资源没被复用产出这份清单只需要十几分钟但它决定了后面几天的工作量边界——先量化再动手。二、消重双入口是最先要解决的问题AI 生成项目里最常见的一幕main.js和main.ts并存一个是从 JS 迁移过来的旧版本一个是 TS 新版两个都在渲染同一片 DOM。判断谁是活着的很简单看构建入口引用了哪个。!-- index.html --scripttypemodulesrc/src/main.ts/script确认之后不要直接删先改名再删给回滚留退路gitmvsrc/main.js src/_deprecated_main.js.bak跑一周没问题再真正删除。重构的第一原则是随时能退回去尤其在你还不完全理解这份代码的时候。同理如果发现同一套逻辑在后端和前端各实现了一遍比如都做了一遍过滤筛选要确定唯一的执行方。一般原则是数据量大或需要复用的逻辑放后端一次做完前端只负责按结果渲染。两份实现迟早不一致而且修复 bug 时你永远只会修其中一份。三、资源复用别在每个入口都开一次连接典型症状是后端每个命令里都在重复这段#[tauri::command]asyncfnget_all(app:tauri::AppHandle)-ResultVecItem,String{letdirapp.path().app_data_dir().map_err(|e|e.to_string())?;letdbDatabase::new(dir.join(app.db)).map_err(|e|e.to_string())?;db.get_all().map_err(|e|e.to_string())}有十几个命令就有十几份。代价不只是重复每次调用都重新打开一次数据库文件还要重跑建表语句高频操作下这是实打实的性能损耗也会让并发写入更容易出问题。正确做法是用 Tauri 的状态管理机制连接只建立一次图 1同一个需求两种生命周期管理。左边的代价不只是代码重复每次调用都要重新打开数据库文件、重跑建表检查高频操作下这就是实打实的开销也让并发写入更容易出问题。右边把连接收到一处命令本身降到了三行。对应的改造只需要三步——定义状态、在启动时注入、在命令里取用usestd::sync::Mutex;userusqlite::Connection;pubstructAppState{pubdb:MutexConnection,}pubfnrun(){letdirapp_data_dir();std::fs::create_dir_all(dir).expect(创建数据目录失败);letconnConnection::open(dir.join(app.db)).expect(打开数据库失败);conn.execute(PRAGMA foreign_keys ON,[]).expect(启用外键失败);tauri::Builder::default().manage(AppState{db:Mutex::new(conn)}).invoke_handler(tauri::generate_handler![get_all,create_item]).run(tauri::generate_context!()).expect(启动失败);}命令里直接用注入的状态#[tauri::command]fnget_all(state:tauri::State_,AppState)-ResultVecItem,String{letconnstate.db.lock().map_err(|e|e.to_string())?;query_all(conn).map_err(|e|e.to_string())}一个命令从十行降到三行而且初始化时机变得明确——应用的生命周期里有且只有一个连接。改完记得验证一件事这些命令是否真的被正确注册了。漏注册一个运行时才会报错。四、状态收敛让每一份数据只有一个来源第三类债是状态漂移一份数据存在全局变量里同时 DOM 里也存着一份还有一份在 DOM 的data-*属性上。改完数据要记得同步三处忘了就出现界面和数据不一致。典型的坏味道letitems:Item[][];// 全局缓存asyncfunctionloadItems(){itemsawaitinvoke(get_all);render(items);// 渲染的同时也依赖了外部变量}asyncfunctionhandleSearch(q:string){constfiltereditems.filter(/* ... */);// 又从全局读render(filtered);}先看改造前后的结构差别再往下读代码图 2从三处同步到一处投影。左边的问题不是代码写得丑而是同一份事实被存了三遍于是每次修改都变成一次多点同步操作而多点同步一定会漏。右边把事实收进一个地方界面退化成它的一个投影漏的可能性就消失了。改造方向是让渲染变成一个纯函数只依赖入参不读外部状态。letitems:Item[][];// 唯一数据源letview{query:,category:all};// 唯一的视图状态functionselectVisible():Item[]{returnitems.filter(byCategory(view.category)).filter(byKeyword(view.query));}functionrender(){renderList(selectVisible());// 任何时刻都能从当前状态重算出界面}functionsetState(patch:Partialtypeofview){view{...view,...patch};render();// 状态变了就重画不需要手动同步}这个模式的好处是幂等不管调用几次render()结果都一样。你不再需要记住改了数据之后还要更新哪个 DOM 节点界面永远只是状态的一个投影。顺带说一句这里不引入框架也完全可行。数据量在几百条以内时全量重绘的成本可以忽略真正需要 diff 算法的场景比想象中少得多。五、安全兜底把渲染和凭据一起收口AI 生成的前端代码里这两类写法出现频率很高都属于必须处理的// 危险用户输入直接拼进 HTMLel.innerHTMLdiv classcard${item.content}/div;// 危险第三方密钥写死在前端constAPI_KEYxxxxxxxx;前者属于前文说过的 XSS 入口改成textContent或做转义即可后者的正确处置是把密钥挪出前端——注意是挪出前端不是挪到后端文件里就完事了编译进二进制的字符串常量照样能被提取真正的解法是让用户自带凭据或走自己的服务端代理。这一步之所以放在第五而不是第一步是因为它改动面小、风险可控但必须在发布前完成。六、怎么给 AI 下重构任务才不会越改越乱这些改造工作量不小完全可以交给 AI 做。但提问方式决定结果质量不要这样说“帮我重构这个项目。”它会自作主张重写一大堆东西你 Review 的成本比自己写还高。要这样说当前文件src-tauri/src/lib.rs已附 目标把每个命令里重复的 Database::new() 改为使用注入的 AppState 约束 1. 只改动函数签名和获取连接的那几行不要动业务逻辑 2. 输出 unified diff不要输出整个文件 3. 如果某处改动会影响并发安全单独标注出来让我确认三个要点指定范围、输出 diff、要求它主动标注风险。让它输出 diff 而不是整文件的理由很实在——diff 你能一眼看出改了什么整文件你得自己找。图 3三个约束缺一不可。范围决定它敢改多大一片diff 决定你要花多少时间 Review风险标注决定你会不会在合并之后才发现问题。第三条最容易被省掉也最贵——它相当于让 AI 主动把我不确定的地方交给你拍板。七、推荐的改造顺序最后给一个顺序按这个来不容易中途崩掉图 4六步重构顺序。顺序本身是有讲究的前两步先让你看得见并且退得回中间两步减少改动点第五步才动安全——如果一上来就改安全和性能你会同时面对改坏了不知道哪坏的和也不知道原来长什么样两件事。阶段动作目标1量化现状行数、重复度、可疑写法知道要改什么2补一层最小测试或手动冒烟清单有回归基准3消除重复双入口、重复实现减少改动点4资源与生命周期收敛连接、状态减少不确定性5安全兜底输出编码、凭据达到可发布标准6性能相关索引、分页、增量渲染有余力再做每完成一个阶段就提交一次单独 commit方便二分定位问题。这一点在重构别人的代码时尤其重要。结语AI 把写代码这件事的边际成本压到了近乎为零于是多写一份变成了默认选择——双入口、重复实现、每个函数独立初始化都是这种倾向的产物。人的价值恰恰在相反的方向上收敛。把重复的合并把分散的集中把隐含的显式化。这些事 AI 不会主动做因为每一次单条回答看上去都没问题只有站在整个项目的尺度上问题才浮现出来。这也正是我们接手 AI 产出之后真正要干的活。前四篇小结前四篇是一条完整的链路① 先把安全底线守住 —— CSP、输出编码、最小权限② 再把数据存稳 —— 主表 修订表 指针回滚用追加③ 然后把发布跑通 —— 签名、更新源、三平台自动出包④ 最后把代码收干净 —— 量化、消重、收敛、兜底如果你的项目正准备上线建议按 ①→③ 的顺序检查一遍如果已经上线正在维护从 ④ 开始收益最直接。后面还有五篇⑤ 表设计取舍、⑥ 全文搜索选型实测、⑦ 原生渲染性能账、⑧ 手写词级 Diff、⑨ 数据迁移与 schema 演进。看完这四篇如果对你有用收藏一下当作 checklist也欢迎在评论区说说你接手过最不敢动的项目是什么样我很想知道是不是同一个模式。
返回列表