介绍
gogocode,引用官网的介绍就是
GoGoCode 是一个基于 AST 的 JavaScript/Typescript/HTML 代码转换工具,你可以用它来构建一个代码转换程序来帮你自动化完成如框架升级、代码重构、多平台转换等工作。
举个场景来说,就是如果我们需要进行大量的重构或者框架升级,这些繁琐还容易出错的过程,可以通过写 gogocode 的脚本来让程序自动帮我们完成。也就说之前你可能需要到每个文件里面 ctrl+r 替换的过程,现在完全可以使用脚本来实现
哪有人就好奇了,那用正则替换不一样吗?no no no , gogocode 的操作是基于 AST 抽象语法树的,
这样代码的可控性就很高,另外鉴于 gogocode 极其便捷的 api 这开发成本要比用正则替换小太多了
本文并不介绍如何入门使用,只是记录一些代码替换的例子与坑 如果要看教程的话,官网的教程已经写的非常棒了 官网教程
例子 1
我们需要将下面的代码
export const ApiName1 = (user_uuid: string, op: PaginationParam) =>
get(`/api/${user_uuid}/msg?include=owner&page=${op.page}&limit=${op.limit}`);
export const ApiName2 = (withs = "", include = "") =>
get(`/api/ws?with=${withs},&include=${include}`);
替换为
export function ApiName1(user_uuid: string, op: PaginationParam) {
return get(
`/api/${user_uuid}/msg?include=owner&page=${op.page}&limit=${op.limit}`
);
}
export function ApiName2(withs = "", include = "") {
return get(`/api/ws?with=${withs},&include=${include}`);
}
坑 1
首先下面这个匹配方式是不生效的
$(source).replace(
`const $_$1 = ($$$) => $$$2 `,
`function $_$1($$$) { return $$$2 }`
);
原因是我们这里的箭头函数是直接返回的,没有 {}
来包裹,官方的例子里面也没有这样的案例。
这里如果我们需要正确的匹配 const foo = (bar) => bar + bar
这样的形势,需要按下面这样写
$(source).replace(
`const $_$1 = ($$$) => "$_$2" `,
`function $_$1($$$) { return $_$2 }`
);
这样就会将 const foo = (bar) => bar + bar
转换为
function foo(bar) {
return bar + bar;
}
坑 2
转换下面这个例子时报错
export const ApiName2 = (withs = "", include = "") =>
get(`/api/ws?with=${withs},&include=${include}`);
报错信息为
/**
Something goes wrong...
Error: replace failed: function ApiName2(withs = ""
include = "") { return get(`/api/ws?with=${withs},&include=${include}`) } cannot be parsed!
**/
从报错信息中,我们可以看到,代码将两个入参中间的 ,
给删除了(或者替换为 \n
了?)。导致语法解析错误。
这迫使我们放弃使用 replace
方法,而使用 replaceBy
来进行细节的处理
$(source)
.find(`export const $_$1 = ($$$) => "$_$2"`)
.each((item) => {
let args = item.match["$$$$"];
// 我们将 ast 中读取到的 入参 合并成字符串
let argsStr = args.map((arg) => $(arg).generate()).join(",");
let fnName = item.match[1][0].value;
let fnContent = item.match[2][0].value;
item.replaceBy(
$(`export function ${fnName}(${argsStr}) { return ${fnContent} }`)
);
});
问题可以跟踪 #143
坑 3
这个严格意义上来说,应该不算坑,当我们的代码中同时存在下面的情况时
export const ApiToken = (params?: object) => {
return post("/token", params);
};
export const ApiToken2 = (params?: object) => post("/token", params);
转换会报错,因为此时 ApiToken
会被转换为
export function ApiToken(
params?: object
) {{
return return post(
'/token',
params
)
}}
如果它 ApiToken
的箭头函数中是有 {}
包裹的,我们应该掠过它。
在原来的基础上加个判断即可
$(source)
.find(`export const $_$1 = ($$$) => "$_$2"`)
.each((item) => {
let args = item.match["$$$$"];
// 我们将 ast 中读取到的 入参 合并成字符串
let argsStr = args.map((arg) => $(arg).generate()).join(",");
let fnName = item.match[1][0].value;
let fnContent = item.match[2][0].value;
if (fnContent.trim()[0] === "{") {
item.replaceBy($(`export function ${fnName}(${argsStr}) ${fnContent}`));
} else {
item.replaceBy(
$(`export function ${fnName}(${argsStr}) { return ${fnContent} }`)
);
}
});
坑 4
转换后的代码,所有注释都没有了,原因是
$(source).find(`export const $_$1 = ($$$) => "$_$2"`);
find
表达式中包含了 export
,导致我们使用 replaceBy
时注释信息也被删掉了
解决方法是删除 export
$(source)
.find(`const $_$1 = ($$$) => "$_$2"`)
.each((item) => {
let args = item.match["$$$$"];
// 我们将 ast 中读取到的 入参 合并成字符串
let argsStr = args.map((arg) => $(arg).generate()).join(",");
let fnName = item.match[1][0].value;
let fnContent = item.match[2][0].value;
if (fnContent.trim()[0] === "{") {
item.replaceBy($(`function ${fnName}(${argsStr}) ${fnContent}`));
} else {
item.replaceBy(
$(`function ${fnName}(${argsStr}) { return ${fnContent} }`)
);
}
});
这里注意下,当我们这样处理后后续在使用
item
上面的 child 或者 find 方法时,需要调用下.parent()
例子 2
我们需要将下面的代码
export function ApiToken(params?: object) {
return post("/token", params);
}
export function ApiLogin(params?: LoginParam) {
return post("/login", params);
}
替换为
export function ApiToken(params?: object, mode: ErrorMessageMode = "message") {
return defHttp.post(
{
url: "/token",
params,
},
{
errorMessageMode: mode,
}
);
}
export function ApiLogin(
params?: LoginParam,
mode: ErrorMessageMode = "message"
) {
return defHttp.post(
{
url: "/login",
params,
},
{
errorMessageMode: mode,
}
);
}
首先来转换 调用方法
$(source)
.find(`function $_$1 ($$$) { $_$2 }`)
.each((item) => {
// 替换 post => defHttp.post
// 为了解决前后命名不同的问题,这里用了个映射
const methodsMap = {
post: "post",
get: "get",
http_delete: "delete",
put: "put",
patch: "patch",
};
item
.parent()
.child("declaration.body")
.find("$_$1($$$)")
.each((_item) => {
const method = _item.match[1][0].value;
if (methodsMap[method]) {
_item.attr("callee.name", `defHttp.${methodsMap[method]}`);
}
});
});
转换参数
首先我们来给每个方法的入参最后添加个 mode: ErrorMessageMode = "message"
$(source)
.find(`function $_$1 ($$$) { $_$2 }`)
.each((item) => {
item
.parent()
.child("declaration")
.append("params", 'mode: ErrorMessageMode = "message"');
});
修改方法的参数
$(source)
.find(`function $_$1 ($$$) { $_$2 }`)
.each((item) => {
item
.parent()
.find(`$_$1($$$)`)
.each((_item) => {
const newMethodName = _item.attr("callee.name");
// 这里无法保证用户传入的第一个参数是字符串还是模版标签,这里统一的转换为 文本代码
const url = $(_item.match["$$$$"][0]).generate();
const params = _item.match["$$$$"][1]
? $(_item.match["$$$$"][1]).generate()
: null;
_item.replaceBy(
$(`${newMethodName}(
{
url: ${url},
${params ? `params: ${params}` : ""}
},
{
errorMessageMode: mode,
}
)`)
);
});
});
例子 3
将
import { post, get, put, patch, http_delete } from "../methods";
转换为
import { defHttp } from "/@/utils/http/axios";
其实你也可以理解为删除之前的,添加一个后面的
这个比较简单
$(source).replace(
`import {$$$} from "../methods"`,
`import { defHttp } from "/@/utils/http/axios";`
);