大山哥AGI头像
关注
LLM 辅助前端重构:从代码坏味道检测到自动修复的工程实践封面图

LLM 辅助前端重构:从代码坏味道检测到自动修复的工程实践

LLM 辅助前端重构:从代码坏味道检测到自动修复的工程实践

cover

一、前端重构的"认知负担":看得出问题,改不动代码

前端项目的技术债积累到一定程度,重构就变成"看得出问题,改不动代码"的困境。坏味道到处都是——300 行的组件、三层嵌套的 useEffect、重复的状态逻辑——但重构一个组件可能牵连十几个文件,手动改容易引入 bug,不改又越来越难维护。

LLM 辅助重构的核心价值不是"让 AI 写代码",而是"让 AI 处理重复性的模式转换"。将 50 个类组件转为函数组件、将 100 处 useState 替换为 useReducer、将散落的 API 调用统一为自定义 Hook——这些模式化的重构任务,LLM 可以批量完成,人工只需审核结果。

二、LLM 辅助重构的工作流

graph TB
    subgraph 检测阶段
        A[AST分析器] --> B[坏味道报告<br/>大组件/重复逻辑/过时API]
        B --> C[重构优先级排序<br/>影响范围×修改难度]
    end

    subgraph 生成阶段
        C --> D[模式匹配<br/>归类重构类型]
        D --> E[LLM批量生成<br/>每类重构一个模板]
        E --> F[代码差异对比<br/>人工审核]
    end

    subgraph 验证阶段
        F --> G[自动化测试<br/>单元测试+快照测试]
        G --> H{测试通过?}
        H -->|否| I[回滚+调整Prompt]
        H -->|是| J[合并代码]
    end

重构工作流分三阶段:检测(AST 分析定位坏味道)、生成(LLM 按模式批量生成重构代码)、验证(自动化测试确认正确性)。LLM 只在生成阶段介入,检测和验证由确定性工具完成。

三、重构工具链实现

3.1 坏味道检测器

import * as ts from 'typescript';
import * as path from 'path';

interface SmellReport {
  file: string;
  type: 'large-component' | 'duplicate-logic' | 'outdated-api' | 'complex-effect';
  severity: 'high' | 'medium' | 'low';
  location: { line: number };
  description: string;
  suggestion: string;
}

class FrontendSmellDetector {
  detect(sourceFile: ts.SourceFile, filePath: string): SmellReport[] {
    const reports: SmellReport[] = [];

    ts.forEachChild(sourceFile, (node) => {
      // 检测大组件:函数体超过 150 行
      if (this.isReactComponent(node)) {
        const lines = this.getLines(node, sourceFile);
        if (lines > 150) {
          reports.push({
            file: filePath,
            type: 'large-component',
            severity: lines > 300 ? 'high' : 'medium',
            location: this.getLocation(node, sourceFile),
            description: `组件 ${this.getComponentName(node)} 有 ${lines} 行`,
            suggestion: '拆分为更小的子组件或自定义 Hook',
          });
        }
      }

      // 检测复杂 useEffect:依赖项超过 5 个
      if (ts.isCallExpression(node)) {
        if (this.isUseEffect(node) && node.arguments.length > 1) {
          const deps = node.arguments[1];
          if (ts.isArrayLiteralExpression(deps) && deps.elements.length > 5) {
            reports.push({
              file: filePath,
              type: 'complex-effect',
              severity: 'medium',
              location: this.getLocation(node, sourceFile),
              description: `useEffect 依赖项 ${deps.elements.length} 个`,
              suggestion: '提取为自定义 Hook,减少依赖项',
            });
          }
        }
      }
    });

    return reports;
  }

  private isReactComponent(node: ts.Node): boolean {
    if (!ts.isFunctionDeclaration(node) && !ts.isArrowFunction(node)) {
      return false;
    }
    const name = this.getComponentName(node);
    return name ? name[0] === name[0].toUpperCase() : false;
  }

  private getComponentName(node: ts.Node): string | null {
    if (ts.isVariableDeclaration(node.parent)) {
      return node.parent.name.getText();
    }
    if (ts.isFunctionDeclaration(node) && node.name) {
      return node.name.getText();
    }
    return null;
  }

  private getLines(node: ts.Node, sf: ts.SourceFile): number {
    const start = sf.getLineAndCharacterOfPosition(node.getStart());
    const end = sf.getLineAndCharacterOfPosition(node.getEnd());
    return end.line - start.line;
  }

  private getLocation(node: ts.Node, sf: ts.SourceFile) {
    const { line } = sf.getLineAndCharacterOfPosition(node.getStart());
    return { line: line + 1 };
  }

  private isUseEffect(node: ts.CallExpression): boolean {
    const expr = node.expression;
    return ts.isIdentifier(expr) && expr.text === 'useEffect';
  }
}

3.2 LLM 批量重构器

interface RefactorTask {
  type: string;
  files: string[];
  prompt: string;
}

class LLMRefactorer {
  /** 批量重构:按类型分组,同类型共享 Prompt */
  async refactor(tasks: RefactorTask[]): Promise<Map<string, string>> {
    const results = new Map<string, string>();

    for (const task of tasks) {
      for (const file of task.files) {
        const source = await this.readFile(file);
        const refactored = await this.refactorFile(
          source, task.type, task.prompt
        );
        results.set(file, refactored);
      }
    }

    return results;
  }

  private async refactorFile(
    source: string,
    type: string,
    instruction: string
  ): Promise<string> {
    const prompt = `重构以下 React 代码。

重构类型:${type}
重构要求:${instruction}

规则:
1. 保持功能完全不变
2. 保持所有导出接口不变
3. 不要添加新的依赖
4. 保持代码风格一致

原始代码:
${source}

输出重构后的完整代码,不要省略任何部分。`;

    return await this.callLLM(prompt);
  }
}

3.3 重构验证器

import { execSync } from 'child_process';

class RefactorValidator {
  /** 验证重构结果:类型检查 + 测试 */
  validate(filePath: string, original: string, refactored: string): {
    valid: boolean;
    errors: string[];
  } {
    const errors: string[] = [];

    // 1. TypeScript 类型检查
    try {
      execSync(`npx tsc --noEmit ${filePath}`, { encoding: 'utf-8' });
    } catch (e: any) {
      errors.push(`类型检查失败: ${e.stdout}`);
    }

    // 2. 导出接口一致性检查
    const originalExports = this.extractExports(original);
    const refactoredExports = this.extractExports(refactored);
    if (originalExports.join(',') !== refactoredExports.join(',')) {
      errors.push(`导出接口不一致: ${originalExports} vs ${refactoredExports}`);
    }

    // 3. 运行相关测试
    try {
      execSync(`npx jest ${filePath.replace('.tsx', '.test.tsx')}`, {
        encoding: 'utf-8',
      });
    } catch (e: any) {
      errors.push(`测试失败: ${e.stdout}`);
    }

    return { valid: errors.length === 0, errors };
  }

  private extractExports(code: string): string[] {
    const exportRegex = /export\s+(?:default\s+)?(?:function|const|class)\s+(\w+)/g;
    const exports: string[] = [];
    let match;
    while ((match = exportRegex.exec(code)) !== null) {
      exports.push(match[1]);
    }
    return exports.sort();
  }
}

四、LLM 辅助重构的 Trade-offs 分析

批量重构的一致性:LLM 对相同模式的代码可能生成不同的重构方案。50 个类组件的转换,可能出现 3-4 种不同的函数组件写法。解决方案是为每种重构类型提供示例代码(few-shot),约束 LLM 的输出格式。

上下文窗口限制:超过 500 行的组件,LLM 可能截断或遗漏部分代码。解决方案是将大组件拆分为多个片段分别重构,或使用支持长上下文的模型。

测试覆盖率依赖:LLM 重构的正确性依赖自动化测试验证。如果项目测试覆盖率低(<50%),重构引入的 bug 可能无法被测试捕获。建议先补充关键路径的测试,再进行 LLM 辅助重构。

人工审核成本:LLM 生成的代码仍需人工审核。50 个文件的重构,审核时间可能需要 2-3 小时。但相比手动重构的 1-2 天,效率提升仍然显著。

五、总结

LLM 辅助重构的核心价值是"批量处理模式化的代码转换",而非替代人工判断。AST 分析定位坏味道,LLM 按模式批量生成重构代码,自动化测试验证正确性,人工审核做最终把关。三阶段流水线将重构效率提升 5-10 倍,同时保证质量。

落地建议:先建立坏味道检测器和自动化测试,确保"发现问题"和"验证修复"的能力;然后从最简单的重构类型开始(如类组件转函数组件),验证 LLM 生成的质量;最后逐步扩展到更复杂的重构类型,全程保持人工审核环节。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/cannonmonster01/article/details/161787867

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--