背景
项目的历史代码中用到了大量的旧组件,由于旧组件已经不再维护,需要升级成新组件。但是手动逐个替换费时费力,且开发人员需要对新旧组件有一定的了解,成本较高。因此可以参考react-codemod 的思想,像react 升级旧版api 一样升级旧组件,减少人力的重复劳动,从而提高生产力。
前排介绍
- AST (Abstract Syntax Tree)
抽象语法树(AST)是源代码的抽象语法结构的树状表现形式,通过astexplorer 可以方便地分析代码的结构。对代码的codemods 其实就是对AST 进行遍历及修改的结果。
- jscodeshift
jscodeshift 是一种对JavaScript 或 TypeScript 文件执行codemods 的工具。
/**
* This replaces every occurrence of variable "foo".
*/
module.exports = function(fileInfo, api) {
return api.jscodeshift(fileInfo.source) // 将字符串源文件转换为一个可遍历/操作的Collection
.findVariableDeclarators('foo')
.renameTo('bar')
.toSource();
}
举个例子🌰
react-codemod-create-element-to-jsx.js
这个文件是将
React.createElement()转换成JSX格式。比如:
React.createElement("h1", {className: "main"}, "Hello React");将会被转换成
<h1 className="main">Hello React</h1>;
先来看下 React.createElement()的结构:
/**
* Find nodes of a specific type within the nodes of this collection.
*/
module.exports = function (fileInfo, api) {
const j = api.jscodeshift;
const root = j(file.source);
// ...
return root
.find(j.CallExpression, {
callee: {
object: {
name: "React",
},
property: {
name: "createElement",
},
},
})
.replaceWith(convertNodeToJSX)
.toSource();
};
运行及调试
运行:
jscodeshift -t PATH_A/your_transform_file.js PATH_B/file_to_be_transformed.jsx
运行结束会输出:
Processing 1 files...
Spawning 1 workers...
Sending 1 files to free worker...
All done.
Results:
0 errors
0 unmodified
0 skipped
1 ok
调试:
node --inspect-brk ./node_modules/jscodeshift/bin/jscodeshift.sh -t fileA fileB --run-in-band
此时会打印出:
Debugger listening on ws://127.0.0.1:9229/742d0e8a-93a5-4e05-ad7e-316a6943492d
For help, see: https://nodejs.org/en/docs/inspector
Debugger attached.
Processing 1 files...
打开浏览器访问chrome://inspect,点击图中位置,即可打开debugger 窗口。
单元测试
jscodeshift 提供了一个基于Jest 的单元测试方法。
- 将测试文件放在子目录(
__tests__)下 - 将输入和输出文件放在子目录(
__testfixtures__)下
/MyTransform.js
/__tests__/MyTransform-test.js
/__testfixtures__/MyTransform.input.js
/__testfixtures__/MyTransform.output.js
执行
jest
若测试通过,会打印出测试成功信息:
以上即是codemod 的一般流程。
组件替换
让我们回到项目中的旧组件替换问题,替换的大概思路如下:
1. 获取从旧组件库中引入的组件;
2. 遍历组件,检验是否有对应的新组件;
- 2.1 有新组件,将该组件从uc_components 中的引入删除,新增从 react_components 中的引入
- - 2.1.1 检测是否改名,若是,则引用中使用新名称,并修改render 中组件名称
- - 2.1.2 检测是否有api 修改,若是,则修改对应api。
- 2.2 无对应新组件,不动。
3. 若旧组件被全部替换,则删除空import
e.g.
1. import { Tip } from "uc_components";
2. Tip 为旧组件,对应新组件为 Tooltip;
- 2.1 删除从旧组件库中引入的Tip,新增 import { Tooltip } from "react_components";
- - 2.1.1 将代码中的Tip 改名
- - 2.1.2 修改对应属性。(t_zIndex -> zIndex, t_title -> popup, 新增属性arrow)
3. 旧组件被全部替换,则删除空import
// 新旧组件转换方法数组
const oldCompTransMapArr = [
{
old: "Tip", // 旧组件名称
new: "Tooltip", // 新组件名称
transAttribute: (attributes) => {
// 属性转换方法
},
transChildren: (children) => {
// 子组件转换方法
},
},
// ...
]
替换前:
<Tip t_zIndex={99} t_title="tip">
This is a tip.
</Tip>
替换后:
<Tooltip zIndex={99} popup="tip" arrow={false}>
<div>This is a tip.</div>
</Tooltip>
由于新版Tooltip 组件要求children必须为single element,因此这里将children用一个<div>包裹,否则会报错:
React.Children.only expected to receive a single React element child.
项目地址
总结
至此将codemod 的完整流程梳理了一遍。当然组件替换应该考虑的情况还有很多,以后会慢慢完善。
过程中比较困惑的点在于ast-type的api文档不太友好,推荐参考babel-types。