太长,不看 replace-package
起因
自己最近碰到的两个问题。
情景1,是一个是经过好几手流转的项目里,有一个处理 JSX 文件的工具函数库,第一手的时候,经典“社区实现的没我自己写的好”、“我偏要造轮子”,“4行代码实现 classnames”:
因为我不是吐槽代码(我也不希望我的代码在以后被人贴在社区公开处刑 ==),所以内容就不贴了,总之使用范围很局限,后面别人接手以后,干脆就改用社区的 classnames 库:
但老的逻辑一直保留下来,毕竟太多了,改起来也太麻烦了,在又经过几轮流转,项目自己的 classnames 方法和开源的 classnames 在不断的混用,最后这个项目流转到我手里了,我一看,不能忍,而且现在有更好的 clsx 库,我要把这两个都替换成 clsx。
情景2,和上面说的是相反的情况,如果你的公司体量中等偏上,那么内部一定有一些自己的轮子,比如 @xxx/popover, @xxx/popup, @xxx/carousel 等等等,一般来说,高质量的内部组件轮子(文档健全,公开维护,类型健全,bug 修复及时等等),能大大提升自己的幸福感,节约自己的时间,虽然给项目增加了依赖,但我觉得还是值得的,但是,你懂得,内部高质量的组件真的很少很少。
然后我就碰到问题了,local 反馈一个问题,Carousel组件在滑动的时候,会出现突然闪动的问题
没办法,去大群反馈这个问题,帮忙排查了,组件的结构大概是
<Carousel onIndexChange={(index) => setActive(index)}>
<div>Item1</div>
<div>Item2</div>
<div>Item3</div>
</Carousel>
然后群里反馈了,说是 onIndexChange 时候的 setActive 操作,导致组件重新 render,删除 onIndexChange 就好了...雀氏这样做没毛病,但肯定是没法删的。
后面又扯了半天,看截图代码还是 Class Component,心想估计是修不了了。
然后自己对着 Vant 抄了一份(不得不说,jiahan 写的 Vant 组件库质量还是很高的),把这个问题也解决了,然后我要做的就是把所有的 @xxx/carousel 替换成自己抄的 Carousel 组件。
总结上面两件事,我要实现一个能够替换包的脚本,因为人工去修改成本太大,也容易出错。
场景收集
方法替换
对应情景1,比如我需要把内部实现的 classnames 替换成 clsx,又或者现在都在做的去 lodash,我需要把 lodash 的 camelcase 方法换成 changecase 的 camelcase 方法。这在日常工作的都是比较常见。
组件替换
其实这也并不是一个偶发的需求,早期的组件库维护多多少少都会碰到类似的问题,比如说使用非常多的 antd,如果我没记错,antd 在 4.0 版本对 Form 组件做了重构,很多 Breaking Change,这会导致一个问题,用户没法比较平滑的从 V3 版本升级到 V4 版本,大部分的组件库处理逻辑都是把旧的组件移动到一个单独的仓库中,antd 当时就是把旧的 Form 组件移动到 @ant-design/compatible 中,印象中 antd 还是提供了相对应的 codemod 去帮助用户快速的处理迁移操作。
质疑
你放屁!!!你说的什么玩意,我直接正则大法好,岂不是更简单快捷。
主要考虑很多场景正则是不好处理的,打个比方,上文的 Form 组件,如果只是单个的 Form 组件,你可能搜 import { Form } from 'antd'; 替换成 import { Form } from '@ant-design/compatible';,但是项目中的可能是
import {
Form
} from 'antd';
// 又或者
import { Form, Input } from 'antd';
// 又或者
import {
Input,
Form
} from 'antd';
// 又或者
import {
// Form
} from 'antd';
// 又或者
import {
Input,
// emmm
Button, Form
} from 'antd';
// 又或者
import {
Form as SuperForm
} from 'antd';
所以还是使用 codemod 的方法去处理这些代码。
如何使用
源码:replace-package AST 树的操作是枯燥无聊的,我们只需要匹配所有的场景,做相对应的转换就可以了,所以怎么实现的其实不需要关心
初始化
执行
npx replace-package@latest init
会生成一份 codemod.json 配置的文件:
替换 classnames
以我们替换的 classnames 到 clsx 为例,首先获取项目中的 classnames 的路径(绝对路径,使用 VSCode 的 copy path 即可)
修改配置文件
{
"replace-package": {
"name": "clsx",
"source": "clsx",
"importDefault": true,
"legacyName": "classnames",
"legacySource": "拷贝的路径",
"legacyImportDefault": false
}
}
执行
npx replace-package@latest src/**/*.(ts|tsx) -f
如果不加
-f会检查 git 是否干净,因为这个操作会直接修改你的代码 结果
接着把开源版本的 classname 转成 clsx,修改配置文件
{
"replace-package": {
"name": "clsx",
"source": "clsx",
"importDefault": true,
"legacyName": "classnames",
"legacySource": "classnames",
"legacyImportDefault": true
}
}
结果
替换组件 Carousel
执行 init 后修改配置文件:
{
"replace-package": {
"name": "Carousel",
"source": "/Users/xxx/src/components/Carousel",
"importDefault": false,
"legacyName": "Carousel",
"legacySource": "@xxx/carousel",
"legacyImportDefault": true
}
}
执行
npx replace-package@latest src/**/*.(ts|tsx) -f
结果
我测试时考虑的一些场景
tsconfig alias
这块是考虑到了,会读取你的 tsconfig 配置,例子
legacyImportDefault 为 true
这种情况下 legacyName 的命名本质上没用
新老都是 ImportSpecifier,且使用了 as
类似这样的配置
{
"replace-package": {
"name": "clsx",
"source": "xxxxx",
"importDefault": false,
"legacyName": "classnames",
"legacySource": "classnames",
"legacyImportDefault": false
}
}
结果(报错忽略,只是跑个例子)
总结
我自己是用的比较顺利,毕竟有问题的我自己都修了,但脚本不一定匹配里所有 case,虽然我补了一些单测,也尝试在不同的场景用这个工具去处理,但不能保证没有 bug,所以执行命令后,还是需要检查一下。