借助AI使用babel重构项目

152 阅读2分钟

事情的起因是我写了一个小工具gencode4swagger,这个工具的作用是使用后端swagger文档,自动生成前端api方法;完成这个工具之后,肯定就迫不及待的在项目中使用啦,三下五除二就给项目配置上了;项目原本就有定义好的api方法,然后加上工具生成的api方法,一个项目中存在相同作用的方法,实在是太不优雅了,于是就有了重构的想法。

工欲善其事

首先介绍一下babel,作为前端开发,你可能没有听过babel,但是你不太可能没有使用过babelbabel是一个转译器,将高版本的js代码转译为低版本、兼容性更好的js代码,基本使用的步骤如下:

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;

// Step1: 使用parser生成ast
const ast = parser.parse(sourceCode, {
    sourceType: 'unambiguous', // 指定是否支持解析模块语法
});
// Step2: 使用traverse遍历ast,操作修改ast
traverse(ast, {});
// Step3: 使用generate生成目标代码
const { code } = generate(ast);

这个简单的例子会直接将源代码输出,既然是重构,我们肯定是需要修改代码的,我们再来实现一个小Demo,使用babel在console信息中添加行号和列号:

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const types = require('@babel/types');

const ast = parser.parse(sourceCode, {
    sourceType: 'unambiguous',
    plugins: ['jsx']
});

const targetCalleeName = ['log', 'info', 'error', 'debug'].map(item => `console.${item}`);

traverse(ast, {
    CallExpression(path, state) {
        const calleeName = generate(path.node.callee).code;
        
        if (targetCalleeName.includes(calleeName)) {
            const { line, column } = path.node.loc.start;
            path.node.arguments.unshift(types.stringLiteral(`filename: (${line}, ${column})`))
        }
    }
});

const { code } = generate(ast);

输入代码为console.log("hello babel")时,程序会输出console.log(filename: (${line}, ${column}), "hello babel"),到这里你就已经学会babel的使用,让我们开始重构吧! Pasted image 20230706213737.png

此刻的你是不是一脸懵逼,发生了啥?这就开始重构了,给了两个Demo,连解释都没有就开始写代码?还记得我们的标题吗?《借助AI使用babel重构项目》,是的,此时就轮到我们的好伙伴AI登场了。

我使用的是VSCode插件DevChat,代码可以右键直接发送给AI Pasted image 20230706214834.png 然后再向AI询问:

Pasted image 20230706214912.png

AI很快就能给出答案:

Pasted image 20230706215107.png

完整回答如下:

这段代码使用了 Babel 的几个库来解析、遍历和生成 JavaScript 代码。主要的目标是在所有的 `console.log``console.info``console.debug``console.error` 调用中添加额外的参数,这个参数是一个字符串,包含了当前文件名和调用的行号和列号。

以下是这段代码的详细解释:

1. 首先,导入了几个 Babel 的库,包括 `@babel/parser`(用于解析 JavaScript 代码生成 AST)、`@babel/traverse`(用于遍历 AST)、`@babel/generator`(用于从 AST 生成 JavaScript 代码)和 `@babel/types`(用于创建、比较和验证 AST 节点)。

2. 然后,定义了一个字符串 `sourceCode`,这个字符串包含了一些 JavaScript 代码。

3. 使用 `parser.parse` 函数解析 `sourceCode`,生成 AST(抽象语法树)。在解析选项中,设置了 `sourceType``'unambiguous'`(表示代码可以是模块或脚本),并启用了 `jsx` 插件(因为代码中包含了 JSX 语法)。

4. 使用 `traverse` 函数遍历 AST。在遍历函数中,对每个 `CallExpression`(调用表达式)节点进行检查。如果这个节点是一个 `console.log``console.info``console.debug``console.error` 调用,就在调用的参数前面添加一个新的参数。这个新的参数是一个字符串,包含了当前文件名和调用的行号和列号。

5. 使用 `generate` 函数从修改后的 AST 生成新的 JavaScript 代码。

6. 最后,打印出新的 JavaScript 代码。

这段代码的主要用途可能是在开发或调试过程中,为 `console.log` 等调用添加额外的上下文信息,如文件名和行号,以帮助定位问题。

学会使用DevChat,我们就可以真正开始重构了。

需求分析

一句话说清楚我们需求:使用工具生成的api方法,替换老的api方法。然后我们来分析一下,如何实现这个需求: 首先,新的api方法与旧的api方法,方法名不一定保持一致,但是请求URL还是不会变的,所以我们可以读取api方法的url,然后进行配置,就能够得到新老方法名的映射关系。 然后我们就可以读取项目的文件,找到引入api方法的文件,处理import语句和替换调用方法名。

映射api方法

我们先来看下原api方法:

import req from "@/utils/request.js";

export default {
  // 获取token
  getToken(data) {
    return req("/auth/v1/login:wechat", "post", data);
  },
  // 手机号登录
  iphoneLogin(data) {
    return req("/auth/v1/login:verificationCode", "post", data);
  },
  // 品牌数据
  getBrand() {
    return req("/bss/v1/c/platform/brandDesign");
  },
  // 获取ossToken
  getOssPolicy() {
    return req("/bss/v1/oss:token");
  },
};

然后向AI询问,如何使用babel获取导出对象的方法名和req的第一个参数,得到下面代码

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;

const sourceCode = `...`;  // 这里应该是你的 JavaScript 代码

const ast = parser.parse(sourceCode, {
  sourceType: 'module',
  plugins: ['jsx']
});

traverse(ast, {
  ExportDefaultDeclaration(path) {
    const properties = path.node.declaration.properties;
    properties.forEach(property => {
      const methodName = property.key.name;
      const reqFirstArg = property.value.body.body[0].argument.callee.arguments[0].value;
      console.log(`Method name: ${methodName}, req first argument: ${reqFirstArg}`);
    });
  }
});

执行输出的代码,发现报错了:

TypeError: Cannot read properties of undefined (reading 'body')

看来AI并不是百分百正确,还需要我们进行纠正,在AST explorer输入我们的代码,我们就可以看到对应的AST结构,然后就能发现req第一个参数的路径应该为property.body.body[0].argument.arguments[0].value,然后就能够得到,URL与方法名的映射关系:

Method name: getToken, req first argument: /auth/v1/login:wechat
Method name: iphoneLogin, req first argument: /auth/v1/login:verificationCode
Method name: getCode, req first argument: 
Method name: getBrand, req first argument: /bss/v1/c/platform/brandDesign
Method name: getOssPolicy, req first argument: /bss/v1/oss:token

参照这个方法,我们处理新的api方法,过程基本一致,就省略了,现有我们有个新旧api方法与URL的映射,通过请求URL我们就可以建立新旧api方法的映射了。然后就可以开始我们的下一步:

重构文件

我们先读取项目中可能使用api方法的文件,然后使用正则判断是否引入api方法,然后使用babel处理文件:

  1. 删除原本的import方法,增加新的import方法
  2. 将api方法从旧的方法名修改为新方法名 咨询我们的AI小助手,得到方法:
const parser = require("@babel/parser");
const generator = require("@babel/generator").default;
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");

const sourceCode = `...`; // Your source code here

const ast = parser.parse(sourceCode, {
  sourceType: 'module'
});

let importedMethod = null;

// First, collect all CallExpression nodes
traverse(ast, {
  CallExpression(path) {
    if (path.node.callee.type === 'MemberExpression' && path.node.callee.object.name === 'Api') {
      importedMethod = path.node.callee.property.name;
      path.node.callee = t.identifier(importedMethod);
    }
  }
});

// Then, modify ImportDeclaration nodes
traverse(ast, {
  ImportDeclaration(path) {
    if (path.node.source.value === "@/http/index.js") {
      path.remove();
    }
  }
});

// Finally, add new ImportDeclaration
ast.program.body.unshift(
  t.importDeclaration(
    [t.importSpecifier(t.identifier(importedMethod), t.identifier(importedMethod))],
    t.stringLiteral("@/api/user")
  )
);

const output = generator(ast, {}, sourceCode);

console.log(output.code);

粘贴复制,so easy!

处理Vue文件

如果我们的项目只有js或者jsx,按照上面的方法就直接处理完了,但是如果我们是Vue项目,就需要做些额外的处理; 在Vue文件中,api方法也只可能在script标签内使用,所以我们只需要将script标签包含的内容提取出来作为js去处理,然后再替换原本的内容就可以了:

let script = data.match(/<script>([\s\S]*)<\/script>/)[1]
const newData = refactor(script)
// 覆盖原文件
fs.writeFile(file, data.replace(/<script>([\s\S]*)<\/script>/, `<script>${newData.replace(/\$(?=\d)/g, '$$$$')}</script>`), (err) => {
  if (err) {
	console.log(err)
  }
})

在这里我碰到了一个AI反复出错的点,最初我的代码是:

let script = data.match(/<script>([\s\S]*)<\/script>/)[1]
const newData = refactor(script)
// 覆盖原文件
fs.writeFile(file, data.replace(/<script>([\s\S]*)<\/script>/, `<script>${newData}</script>`), (err) => {
  if (err) {
	console.log(err)
  }
})

直接用生成的文件替换script标签的内容,但是在处理一个文件时,出现了代码混乱的情况,我把出错的问题提交给AI

你的替换结果看起来像是在尝试替换一段 Vue 组件的 `<script>` 部分的代码,但是替换的结果出现了重复和混乱的代码。这可能是因为你的替换操作没有正确地定位到你想要替换的部分,或者替换的内容没有正确地格式化。

在你的代码中,你使用了 `data.replace(/<script>([\s\S]*)<\/script>/, `<script>${newData}</script>`)` 这样的替换操作。这个操作会匹配到 `<script>` 和 `</script>` 之间的所有内容,并将其替换为 `<script>${newData}</script>`。这里的问题可能出在两个地方:

1. `${newData}` 变量的内容可能没有正确地格式化。如果 `${newData}` 的内容包含了 `<script>` 和 `</script>`,那么替换后的结果就会出现重复的 `<script>` 和 `</script>`。

2. 正则表达式 `/<script>([\s\S]*)<\/script>/` 是贪婪匹配,它会匹配到最后一个 `</script>` 之前的所有内容。如果你的 `data` 字符串中有多个 `<script>` 和 `</script>`,那么这个正则表达式就会匹配到所有的 `<script>` 和 `</script>` 之间的内容,而不仅仅是第一个 `<script>` 和 `</script>` 之间的内容。

你可以尝试以下的解决方案:

1. 确保 `${newData}` 的内容是正确的,并且不包含 `<script>` 和 `</script>`。

2. 使用非贪婪匹配的正则表达式 `/<script>([\s\S]*?)<\/script>/`,这样就只会匹配到第一个 `<script>` 和 `</script>` 之间的内容。

3. 如果你想要替换所有的 `<script>` 和 `</script>` 之间的内容,你可以在正则表达式后面添加一个 `g` 标志,如 `/<script>([\s\S]*?)<\/script>/g`。

AI认为是贪婪模式的问题,因为我的文件中存在多个script导致的,但是实际上并不是,而是因为代码中出现$1 ,在 replace 函数中,$1 会被视为一个特殊的替换模式,它表示匹配到的第一个括号中的内容所以导致newData替换了$1,然后我向AI说了问题,它又告诉我:

data.replace(/<script>([\s\S]*?)<\/script>/, `<script>${newData.replace(/\$(?=\d)/g, '$$')}</script>`)

但是这里又错了,在使用 replace 方法时,需要使用 $$ 表示要替换成一个字面量的 $ 字符。然后才有最终的代码。

AI写代码的能力确实强,借助它的帮助我们能够快速开发,在之前我们只能边学习babel,边百度粘贴复制,出码效率高了不少;但是它一本正经说谎的能力也是毫不逊色,如果没有一定的分辨和纠错能力也是被它耍的团团转。