React Component Codemod

1,329 阅读3分钟

背景

项目的历史代码中用到了大量的旧组件,由于旧组件已经不再维护,需要升级成新组件。但是手动逐个替换费时费力,且开发人员需要对新旧组件有一定的了解,成本较高。因此可以参考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.

项目地址

uc_comp_codemod

总结

至此将codemod 的完整流程梳理了一遍。当然组件替换应该考虑的情况还有很多,以后会慢慢完善。 过程中比较困惑的点在于ast-typeapi文档不太友好,推荐参考babel-types

参考