接着上一篇 gogocode AST 抽象语法树修改器使用例子 (一), 这次我们的场景是 Vuex 到 Pinia 的迁移
本文的最终目的是按照 Pinia 官方的迁移步骤 进行将 Vuex
转换为 pinia
我们最终的目的是将下面的代码
// vuex
const state: CounterState = {
count: 0,
};
const getters = {
getCount(state: CounterState) {
return state.count;
},
};
const mutations = {
setCount(state: CounterState, payload: number) {
state.count = payload;
},
};
const actions = {
changeCount(ctx: ActionContext<CounterState, any>) {
ctx.commit("setCount", ctx.state.count + 1);
},
};
export default {
namespaced: true,
state,
getters,
mutations,
actions,
};
转换为
// pinia
const state: CounterState = {
count: 0,
};
const getters = {
getCount(state: CounterState) {
return state.count;
},
};
const actions = {
setCount(payload: number) {
this.count = payload;
},
changeCount() {
this.setCount(this.count + 1);
},
};
export const useCounterStore = defineStore("test", {
state: (): LocaleState => state,
getters,
actions,
});
因为转换内容比较多,我们需要拆分成多个步骤进行转换(其实是因为我技术不行,无法一次达到)
实际操作过程并不是那么简单,因为代码规范不统一,不同的 store 经过不同的开发者编写,导致需要做很多兼容的地方
预处理部分
因为各个文件里面不一定能完全按照理想的代码转换,我们需要提前将代码进行预处理,以保证后续的代码能按预期的运行
我们同时需要将代码拆封下
let $source = $(source).root();
$source = preFormat($, $source).root();
$source.root().generate().root();
// 我们来实现下预处理
function preFormat($, $source) {
return (
$source
// 将 {state:{...initState} 替换成 {state:initState}
.replace(
"export default {state:{...$_$1}, $$$}",
`export default {state:$_$1, $$$}`
)
// 将 {getters:{}} 提出成 {getters}
.replace(
`export default {getters:{$$$},$$$1}`,
`const getters = {$$$};\n
export default {getters,$$$1}`
)
// 将 {mutations:{}} 提出成 {mutations}
.replace(
`export default {mutations:{$$$},$$$1}`,
`const mutations = {$$$};\n
export default {mutations,$$$1}`
)
// 将 {actions:{}} 提出成 {actions}
.replace(
`export default {actions:{$$$},$$$1}`,
`const actions = {$$$};\n
export default {actions,$$$1}`
)
// 将 { method:(state)=>{return state.name} } 和 { method: (state)=>state.name }
// 替换成 {method(state){return state.name}}
.find(`const $_$1 = { $$$ }`)
.each((item) => {
item.match["$$$$"].map((_item) => {
if (_item.type === "ObjectProperty") {
if (_item.value.type === "ArrowFunctionExpression") {
let fnBody = $($(_item.value).attr("body")).generate().trim();
fnBody = fnBody[0] !== "{" ? `{return ${fnBody}}` : fnBody;
const fnArgs = $(_item.value)
.attr("params")
.map((arg) => {
return $(arg).generate();
})
.join(",");
const fnName = _item.key.name;
// 这里不知道为什么 `${fnName}: () => "$_$2"` 能匹配到 () => {}
$(item.node).replace(`${fnName}: () => "$_$2"`, (match) => {
if (match[2][0].value[0] === "{") {
return `${fnName}(${fnArgs}) $_$2`;
} else {
return `${fnName}(${fnArgs}) {return $_$2}`;
}
});
}
}
});
})
);
}
接着我们来转换下导出引用
let $source = $(source);
$source = preFormat($, $source).root();
$source = transformExport($, $source).root();
$source.root().generate().root();
function transformExport($, $source) {
return $source.replace(
`export default {$$$}`,
`export const useTestStore = defineStore('test',{$$$})`
);
}
我们首先将导出部分替换成 defineStore('test',{$$$})
这样再次使用 find 方法时就能准确的命中需要处理的导出内容
在使用 find 之后,我们在 each 方法中,准备调用才分好的转换方法
先看下转换后的效果
// 上方代码没变动
export const useTestStore = defineStore("test", {
namespaced: true,
state,
getters,
mutations,
actions,
});
删除 namespaced,mutations 俩参数
let $source = $(source);
$source = preFormat($, $source).root();
$source = transformExport($, $source).root();
$source = deleteUnusedProp($, $source).root();
$source.root().generate();
function deleteUnusedProp($, $source) {
return $source.find(`defineStore('test', $$$)`).each((item) => {
// 删除 namespaced,mutations
item.replace("mutations:$_$1", "");
item.replace("namespaced:$_$1", "");
});
}
转换 state 部分
let $source = $(source);
$source = preFormat($, $source).root();
$source = transformExport($, $source).root();
$source = deleteUnusedProp($, $source).root();
$source = transformState($, $source).root();
$source.root().generate();
function transformState($, $source) {
return $source.find(`defineStore('test', $$$)`).each((item) => {
item.match["$$$$"][0].properties.map((prop, index) => {
const key = prop.key.name;
const value = prop.value;
// 找到 key 为 state 的进行转换
if (key === "state") {
// 转换下 state => state:() => state
{
// 找到 state 对应的变量
let stateNameStr = $(value).generate();
let stateName = stateNameStr.match(/^[\w_]+$/) ? stateNameStr : null;
// 命中 state: () :CounterState => ({...initState}) 情况
if (!!~stateNameStr.indexOf("...")) {
stateName = stateNameStr.split("...")[1].split("}")[0].trim();
}
let type = null;
// 这里如果是变量的话,我们来尝试获取下它的类型
$source.find(`const $_$1 = $$$`).each((_item) => {
if (_item.match[1][0].value === stateName) {
let stateType = _item.attr(
"declarations.0.id.typeAnnotation.typeAnnotation.typeName.name"
);
if (stateType) {
type = stateType;
}
}
});
if (value.type === "Identifier") {
$(item.match["$$$$"][0]).attr(
`properties.${index}`,
`${key}: () ${type ? `:${type}` : ""} => ${$(value).generate()}`
);
}
// 判断 state 的类型为对象表达式
// 命中这种情况 {state:{}}
if (value.type === "ObjectExpression") {
$(item.match["$$$$"][0]).attr(
`properties.${index}`,
`${key}: () ${type ? `:${type}` : ""} => (${$(value).generate()})`
);
}
// 判断 state 的类型为 fucntin 或者 箭头函数
// 命中这种情况 {state:function} | {state:()=>{}}
if (
value.type === "FunctionExpression" ||
value.type === "ArrowFunctionExpression"
) {
$(item.match["$$$$"][0]).attr(
`properties.${index}`,
`${key}: ${$(value).generate()}`
);
}
}
}
});
});
}
转换后的效果
没想到吧,转换个 state 既然那么复杂, 这里我们兼顾了代码中各种写法, state, state: initState, state:()=>state, state: function { return state }
export const useTestStore = defineStore("test", {
state: (): CounterState => state,
getters,
actions,
});
转换 getters
1 删除与 state 相同名称的 getter
删除任何以相同名称(例如 firstName: (state) => state.firstName)返回状态的 getter,这些不是必需的,因为您可以直接从商店实例访问任何状态
let $source = $(source);
$source = preFormat($, $source).root();
$source = transformExport($, $source).root();
$source = deleteUnusedProp($, $source).root();
$source = transformState($, $source).root();
$source = transformGetters($, $source).root();
$source.root().generate();
function transformGetters($, $source) {
return $source.find(`defineStore('test', $$$)`).each((item) => {
item.match["$$$$"][0].properties.map((prop, index) => {
if (prop.key?.name === "getters") {
// 转换 getters 与 state 相同的情况
{
const value = prop.value;
let stateName = "";
// 此时到这里, prop 中有个 state: () :CounterState => state 字符串
// 获取 state 的变量定义名称
let stateFnName = item.match["$$$$"][0].properties.find(
(item) => typeof item === "string"
);
if (!!~stateFnName.indexOf("=>")) {
stateName = stateFnName.split("=>")[1].trim();
// 命中 state: () :CounterState => ({...initState}) 情况
if (!!~stateName.indexOf("...")) {
stateName = stateName.split("...")[1].split("}")[0].trim();
}
}
if (stateName.length === 0)
throw new Error("无法找到对应的 state 对象名称,gutters 转换失败");
// 获取 state 的 keys
let stateKeys = [];
$source.find(`const $_$1 = {$$$}`).each((_item) => {
if (_item.match[1][0].value === stateName) {
_item.match["$$$$"].map((_) => {
stateKeys.push(_.key.name);
});
}
});
const gettersName = prop?.value?.name;
if (!gettersName)
throw new Error(
"无法找到对应的 gutters 对象名称,gutters 转换失败"
);
// 替换与 state 相同的 getter
$source.find(`const $_$1 = {$$$}`).each((_item) => {
if (_item.match[1][0].value === gettersName) {
stateKeys.map((stateKey) => {
_item.replace(`${stateKey}(){}`, (match) => {
console.log(
`检测到 getters.${stateKey} 与 state.${stateKey} 出现重复,执行删除`
);
return "";
});
});
}
});
}
}
});
});
}
转换 mutations 和 actions
let $source = $(source);
$source = preFormat($, $source).root();
$source = transformExport($, $source).root();
$source = deleteUnusedProp($, $source).root();
$source = transformState($, $source).root();
$source = transformGetters($, $source).root();
$source = transformActionsAndMutations($, $source).root();
$source.root().generate();
function transformActionsAndMutations($, $source) {
// 将 mutations 里面的第一个参数 state 删除,然后将所有 state. 修改成 this.
$source = transformMutations($, $source).root();
// 将 actions 里面的第一个参数删除,并且所有 ctx 的调用都切换为 this
$source = transformActions($, $source).root();
// 将 mutations 和 actions 进行合并
$source = transformMutationsIntoActions($, $source).root();
return $source;
}
function transformActions($, $source) {
return $source.find(`const actions = {$$$}`).each((item) => {
item.match["$$$$"].map((method) => {
// 如果 method.params.length === 0 代表着用户的 actions 什么也没接收
// userLogout() {
// ApiLogout().then((res: any) => {
// if (res.status == 204) {
// // 1. 清除本地缓存的 Token
// // 2. 重定向到登录界面
// window.$cookies.remove("token");
// router.replace({ name: "login" });
// }
// });
// },
if (method.params.length === 0) {
return;
}
const methodName = method.key.name;
let firstParamName = method.params[0].name;
// 这里如果是解构写法 {state,commit,dispatch} 的话,将 firstParamName 转换成一个数组
if (!firstParamName && method.params[0].type === "ObjectPattern") {
firstParamName = method.params[0].properties.map((_param) =>
$(_param).generate()
);
}
$(item.node).replace(`${methodName}($$$args){$$$body}`, (match) => {
const paramsLength = match["$$$args"].length;
const fnArgs = match["$$$args"]
.map((arg) => {
return $(arg).generate();
})
.slice(1)
.join(",");
if (paramsLength === 1) {
return `${methodName}(){$$$body}`;
} else {
return `${methodName}(${fnArgs}){$$$body}`;
}
});
// 替换 async
$(item.node).replace(`async ${methodName}($$$args){$$$body}`, (match) => {
const paramsLength = match["$$$args"].length;
const fnArgs = match["$$$args"]
.map((arg) => {
return $(arg).generate();
})
.slice(1)
.join(",");
if (paramsLength === 1) {
return `async ${methodName}(){$$$body}`;
} else {
return `async ${methodName}(${fnArgs}){$$$body}`;
}
});
// 处理 firstParamName 为 string 的情况
// 也就是 ctx
if (typeof firstParamName === "string") {
// 替换 ctx.state
$(item.node).replace(`${firstParamName}.state.$_$1`, `this.$_$1`);
// 替换
// return actions.requestUserTasks(ctx, {
// type: 'favorite',
// page: payload.page,
// filterGroup: payload.group,
// })
$(item.node).replace(`actions.$_$1`, `this.$_$1`);
$(item.node).replace(`this.$_$1(ctx, $$$)`, `this.$_$1($$$)`);
// 替换 ctx.getters
$(item.node).replace(`${firstParamName}.getters.$_$1`, `this.$_$1`);
// 替换 const state = ctx.state;
$(item.node).replace(`const state = ctx.state`, "");
$(item.node).replace(`state.$_$1`, `this.$_$1`);
// 替换 ctx.commit
$(item.node).replace(`${firstParamName}.commit($$$)`, (match) => {
let actionName = $(match["$$$$"][0])
.generate()
.match(/^"([\w_\/]+)"$/)[1];
const leftParams = match["$$$$"]
.map((arg) => {
return $(arg).generate();
})
.slice(1)
.join(",");
if (!!~actionName.indexOf("/")) {
console.log(
"监测到使用本文件以外的 commit",
actionName,
"请搜索 __USE_STORE_COMMIT__ 关键字后,自己替换"
);
actionName = actionName.replace("/", "__USE_STORE_COMMIT__");
}
return `this.${actionName}(${leftParams})`;
});
// 替换 ctx.dispatch
$(item.node).replace(`${firstParamName}.dispatch($$$)`, (match) => {
let actionName = $(match["$$$$"][0])
.generate()
.match(/^"([\w_\/]+)"$/)[1];
const leftParams = match["$$$$"]
.map((arg) => {
return $(arg).generate();
})
.slice(1)
.join(",");
if (!!~actionName.indexOf("/")) {
console.log(
"监测到使用本文件以外的 dispatch",
actionName,
"请搜索 __USE_STORE_DISPATCH__ 关键字后,自己替换"
);
actionName = actionName.replace("/", "__USE_STORE_DISPATCH__");
}
return `this.${actionName}(${leftParams})`;
});
// 替换 ctx.rootState
$(item.node).replace(`${firstParamName}.rootState.$_$1`, (match) => {
console.log(
"监测到使用本文件以外的 rootstate",
"请搜索 __USE_STORE_ROOT_STATE__ 关键字后,自己替换"
);
return `this.__USE_STORE_ROOT_STATE__.$_$1`;
});
// 替换 ctx.rootGetters
$(item.node).replace(`${firstParamName}.rootGetters[$_$1]`, (match) => {
console.log(
"监测到使用本文件以外的 rootGetters",
"请搜索 __USE_STORE_ROOT_GETTERS__ 关键字后,自己替换"
);
return `this.__USE_STORE_ROOT_GETTERS__[$_$1]`;
});
}
// 处理 firstParamName 为数组的情况,也就是 {state,commit,dispatch}
if (Array.isArray(firstParamName)) {
// 替换 state
$(item.node).replace(`state.$_$1`, `this.$_$1`);
// 替换 getters
$(item.node).replace(`getters.$_$1`, `this.$_$1`);
// 替换 commit
$(item.node).replace(`commit($$$)`, (match) => {
let actionName = $(match["$$$$"][0])
.generate()
.match(/^"([\w_\/]+)"$/)[1];
const leftParams = match["$$$$"]
.map((arg) => {
return $(arg).generate();
})
.slice(1)
.join(",");
if (!!~actionName.indexOf("/")) {
console.log(
"监测到使用本文件以外的 commit",
actionName,
"请搜索 __USE_STORE_COMMIT__ 关键字后,自己替换"
);
actionName = actionName.replace("/", "__USE_STORE_COMMIT__");
}
return `this.${actionName}(${leftParams})`;
});
// 替换 dispatch
$(item.node).replace(`dispatch($$$)`, (match) => {
let actionName = $(match["$$$$"][0])
.generate()
.match(/^"([\w_\/]+)"$/)[1];
const leftParams = match["$$$$"]
.map((arg) => {
return $(arg).generate();
})
.slice(1)
.join(",");
if (!!~actionName.indexOf("/")) {
console.log(
"监测到使用本文件以外的 dispatch",
actionName,
"请搜索 __USE_STORE_DISPATCH__ 关键字后,自己替换"
);
actionName = actionName.replace("/", "__USE_STORE_DISPATCH__");
}
return `this.${actionName}(${leftParams})`;
});
// 替换 rootState
$(item.node).replace(`rootState.$_$1`, (match) => {
console.log(
"监测到使用本文件以外的 rootstate",
"请搜索 __USE_STORE_ROOT_STATE__ 关键字后,自己替换"
);
return `this.__USE_STORE_ROOT_STATE__.$_$1`;
});
// 替换 rootGetters
$(item.node).replace(`rootGetters[$_$1]`, (match) => {
console.log(
"监测到使用本文件以外的 rootGetters",
"请搜索 __USE_STORE_ROOT_GETTERS__ 关键字后,自己替换"
);
return `this.__USE_STORE_ROOT_GETTERS__[$_$1]`;
});
}
});
});
}
function transformMutations($, $source) {
return $source.find(`const mutations = {$$$}`).each((item) => {
item.match["$$$$"].map((method) => {
const methodName = method.key.name;
const firstParamName = method.params[0].name;
// 删除第一个参数
$(item.node).replace(`${methodName}($$$args){$$$body}`, (match) => {
const paramsLength = match["$$$args"].length;
const fnArgs = match["$$$args"]
.map((arg) => {
return $(arg).generate();
})
.slice(1)
.join(",");
if (paramsLength === 1) {
return `${methodName}(){$$$body}`;
} else {
return `${methodName}(${fnArgs}){$$$body}`;
}
});
$(item.node)
.find(`${firstParamName}.$_$1`)
.each((_item) => {
_item.replaceBy(`this.${_item.match[1][0].value}`);
});
// 替换
// mutations.addSubTasks(state, [new TaskModel(task.data)])
$(item.node).replace(`mutations.$_$1(state,$$$)`, `this.$_$1($$$)`);
});
});
}
function transformMutationsIntoActions($, $source) {
return $source
.find(`const mutations = {$$$}`)
.each((item) => {
item.match["$$$$"].map((method) => {
$source.find(`const actions = {}`).each((_item) => {
$(_item.attr("declarations.0.init")).prepend("properties", method);
});
});
})
.replace("const mutations = {}", "");
}
添加下引用文件和导出方法
// 顶部导入这两个文件
import { defineStore } from "pinia";
import { store } from "/@/store";
// 底部导出这个方法
export function useTestStoreWithOut() {
return useTestStore(store);
}
let $source = $(source);
$source = preFormat($, $source).root();
$source = transformExport($, $source).root();
$source = deleteUnusedProp($, $source).root();
$source = transformState($, $source).root();
$source = transformGetters($, $source).root();
$source = transformActionsAndMutations($, $source).root();
$source = importAndExport($, $source).root();
$source.root().generate();
function importAndExport($, $source) {
return $source
.before(
`import { defineStore } from "pinia";
import { store } from "/@/store";`
)
.after(
`\n\n export function useTestStoreWithOut() {
return useTestStore(store);
}`
);
}
修修补补
代码可能会出现这种情况, 我们希望它能够按照 state
, getters
, actions
这样进行排序
export const useTestStore = defineStore("test", {
actions,
getters,
state: (): State => state,
});
让我们再来写个转换
let $source = $(source);
$source = preFormat($, $source).root();
$source = transformExport($, $source).root();
$source = deleteUnusedProp($, $source).root();
$source = transformState($, $source).root();
$source = transformGetters($, $source).root();
$source = transformActionsAndMutations($, $source).root();
$source = importAndExport($, $source).root();
$source = sortStoreParamsName($, $source).root();
$source.root().generate();
function sortStoreParamsName($, $source) {
let sortedCode = "";
$source.find(`defineStore('test', {$$$})`).each((item) => {
let match = item.match;
let newArr = [];
let sortArr = ["state", "getters", "actions"];
match["$$$$"].map((param) => {
if (typeof param === "string") {
newArr.push({
key: "state",
value: param,
});
return;
}
if (param.type === "ObjectProperty") {
newArr.push({
key: param.key.name,
value: $(param).generate(),
});
return;
}
newArr.push({
key: "unknown",
value: $(param).generate(),
});
});
newArr.sort((left, right) => {
let letIndex = sortArr.includes(left.key)
? sortArr.indexOf(left.key)
: 999;
let rightIndex = sortArr.includes(right.key)
? sortArr.indexOf(right.key)
: 999;
return letIndex - rightIndex;
});
sortedCode = `defineStore('test', {${newArr
.map((arg) => arg.value)
.join(",")}})`;
});
if (sortedCode.length > 0) {
$source.replace(`defineStore('test', {})`, sortedCode);
}
return $source;
}